写在前面

kubebuilder 是一个集成工具,为了更好的理解k8s工作流程。这里使用k8s tools实现一个operator。

创建CRD

Custom Resource

以Resource的角度来审视k8s,可以发现k8s实际上做的事情是对Resource的CRUD操作。

Custom Resource是一种用户定义的k8s resource数据,可以通过k8s api对定义的Resource进行CRUD操作。

对于resource本身而言,只是存储结构化的数据,如果期望资源数据改变时集群中状态发生对应变化,就需要有一个Controller来对数据变化感知,然后修改集群内的状态。

Custom Controller

custom controller就是用户定义的controller。完成对customer resource内容变更的对应操作。

将Custom Resource 和Custom Controller结合起来,就是operator模式。

聚合API和独立API

官方文档很详细,引用一下

考虑 API 聚合的情况 优选独立 API 的情况
你的 API 是声明式的

你的 API 不符合声明式

模型。
你希望可以是使用 kubectl 来读写你的新资源类别。 不要求 kubectl 支持。
你希望在 Kubernetes UI (如仪表板)中和其他内置类别一起查看你的新资源类别。 不需要 Kubernetes UI 支持。
你在开发新的 API。 你已经有一个提供 API 服务的程序并且工作良好。
你有意愿取接受 Kubernetes 对 REST 资源路径所作的格式限制,例如 API 组和名字空间。(参阅 API 概述

你需要使用一些特殊的 REST 路径以便与已经定义的 REST API 保持兼容。
你的资源可以自然地界定为集群作用域或集群中某个名字空间作用域。 集群作用域或名字空间作用域这种二分法很不合适;你需要对资源路径的细节进行控制。
你希望复用 Kubernetes API 支持特性

你不需要这类特性。

简单解释就是CRD方式可以使用k8s已有api,当k8s已有api不满足条件时,才使用聚合层api来扩展。

因此大部分情况都是使用CRD而不必使用聚合层API。

CRD

创建一个简单的CRD用来部署nginx

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  # 名字必需与下面的 spec 字段匹配,并且格式为 '<名称的复数形式>.<组名>'
  name: nginxes.ldq.test.com
spec:
  # 组名称,用于 REST API: /apis/<组>/<版本>
  group: ldq.test.com
  # 列举此 CustomResourceDefinition 所支持的版本
  versions:
    - name: v1
      # 每个版本都可以通过 served 标志来独立启用或禁止
      served: true
      # 其中一个且只有一个版本必需被标记为存储版本
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                image:
                  type: string
                replicas:
                  type: integer
  # 可以是 Namespaced 或 Cluster
  scope: Namespaced
  names:
    # 名称的复数形式,用于 URL:/apis/<组>/<版本>/<名称的复数形式>
    plural: nginxes
    # 名称的单数形式,作为命令行使用时和显示时的别名
    singular: nginx
    # kind 通常是单数形式的驼峰命名(CamelCased)形式。你的资源清单会使用这一形式。
    kind: Nginx
    # shortNames 允许你在命令行使用较短的字符串来匹配资源
    shortNames:
    - ng

部署

apiVersion: "ldq.test.com/v1"
kind: Nginx
metadata:
  name: new-nginx
spec:
  image: nginx
  replicas: 1

查看部署的资源

root@lidongqi:~/k8s/operator/steps/nginx# kubectl  get ng
NAME                 AGE
my-new-cron-object   30s

编写controller

code-generator

K8s code generator 在 k8s 源码中大量使用,主要是生成API代码、Client代码。

git clone https://github.com/kubernetes/code-generator.git
cd code-generator/cmd
go install ./*

doc.go

doc.go是代码生成的重要配置,为代码生成提供声明

// +k8s:deepcopy-gen=package deepcopy-gen配置
// +k8s:defaulter-gen=TypeMeta defaulter-gen配置
// +groupName=ldq.test.com

package v1

types.go声明类型

package v1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

type NginxSpec struct {
	// +optional
	Image    string `json:"image,omitempty" protobuf:"bytes,2,opt,name=image"`
	Replicas int32  `json:"replicas" protobuf:"varint,1,opt,name=replicas"`
}

type NginxStatus struct {
	// +optional
	Message string `json:"message,omitempty" protobuf:"bytes,1,opt,name=message"`
	// +optional
	Reason string `json:"reason,omitempty" protobuf:"bytes,2,opt,name=reason"`
}

// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// Nginx type
type Nginx struct {
	metav1.TypeMeta `json:",inline"`

	// +optional
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	// +optional
	Spec NginxSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`

	// +optional
	Status NginxStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// NginxList is a list of Nginx resources
type NginxList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata"`

	Items []Nginx `json:"items"`
}

update-codegen.sh

生成脚本,根据code-generator 中的update-codegen.sh修改而来。

#!/usr/bin/env bash

# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -o errexit
set -o nounset
set -o pipefail

SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
CODEGEN_PKG=/home/lidongqi/work/k8s/k8s.io/code-generator

source "${CODEGEN_PKG}/kube_codegen.sh"

# generate the code with:
# --output-base    because this script should also be able to run inside the vendor dir of
#                  k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir
#                  instead of the $GOPATH directly. For normal projects this can be dropped.
export KUBE_VERBOSE=2

#-i 是一个package路径,不要最后的 '/'
register-gen -v ${KUBE_VERBOSE} -i github.com/lidongqi/demo-controller/apis/ldq.test.com/v1 \
--go-header-file "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \
-o "$(dirname "${BASH_SOURCE[0]}")/../../../.." 
#此处的目录层级和go.mod中的package路径对应。如果module github.com/lidongqi/demo-controller
#则需要/../../../..向上寻找4级目录(因为当前实在hack目录下)

kube::codegen::gen_helpers \
    --input-pkg-root github.com/lidongqi/demo-controller/apis \
    --output-base "$(dirname "${BASH_SOURCE[0]}")/../../../.." \
    --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt"

kube::codegen::gen_client \
    --with-watch \
    --input-pkg-root github.com/lidongqi/demo-controller/apis \
    --output-pkg-root github.com/lidongqi/demo-controller/generated \
    --output-base "$(dirname "${BASH_SOURCE[0]}")/../../../.." \
    --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt"