Operator-SDK:结合基于GO构建的官方样例相关文档阅读及部分命令解释

Operator-SDK:结合基于GO构建的官方样例相关文档阅读及部分命令解释

阅读并翻译operator-sdk官方文档中的部分内容,使用go语言结合官方文档中Building Operators的样例Quickstart for Go-based Operators:一套简单的指令设置和运行一个基于go的operator,对搭建出的operator框架和命令的用途等做了部分解释和说明,同时对在实际部署中碰到的部分问题进行了解决。

文章目录

1 Operator Scope

命名空间范围的operator监视和管理单个命名空间中的资源,而集群范围的operator监视和管理整个集群范围内的资源。

如果operator可以在任何命名空间中创建资源,那么它应该是集群范围的。如果要灵活部署operator,则应该将其限定为命名空间范围。命名空间范围允许解耦升级(upgrades)、故障(failures)和监视(minitoring)的命名空间隔离以及不同的API定义。

默认情况下,operator-sdk init构建集群范围内的operator,以下内容详细介绍了默认的集群范围内的operator项目到命名空间范围的operator的转换。一般情况下,operator可能更适合集群范围。

main.go中创建Manager实例时,名称空间(Namespaces)通过Manager Options设置。 Manager应该为客户端提供这些名称空间的监视和缓存。只有集群范围的Managers才能为客户端提供集群范围的CRDs的管理。

1.1 Manager Watching Options

1.1.1 监听整个命名空间的资源(默认)

main.go中初始化的Manager默认是不带名称空间的选项,或者Namespace: "",表示将会监听整个名称空间。

...
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
    Scheme:             scheme,
    MetricsBindAddress: metricsAddr,
    Port:               9443,
    LeaderElection:     enableLeaderElection,
    LeaderElectionID:   "f1c5ece8.example.com",
})
...
1.1.2 监听单个命名空间内的资源

为了限制Manager缓存一个特定的命名空间的范围,需要设置Options中的Namespace的值:

...
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
    Scheme:             scheme,
    MetricsBindAddress: metricsAddr,
    Port:               9443,
    LeaderElection:     enableLeaderElection,
    LeaderElectionID:   "f1c5ece8.example.com",
    Namespace:          "operator-namespace",	//设置Namespace监听单个命名空间
})
...
1.1.3 监听多个命名空间内的资源

可以使用Options中的MultiNamespacedCacheBuilder 选项来监听和管理一个命名空间的集合内的资源:

...
namespaces := []string{"foo", "bar"} // List of Namespaces
...
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
    Scheme:             scheme,
    MetricsBindAddress: metricsAddr,
    Port:               9443,
    LeaderElection:     enableLeaderElection,
    LeaderElectionID:   "f1c5ece8.example.com",
    NewCache:           cache.MultiNamespacedCacheBuilder(namespaces),
})
...

在上面的示例中,在传递给选项集之外的命名空间中创建的CR将不会由其控制器进行协调,因为管理器不管理该命名空间。

1.2 限制role和权限

operator作用域定义其Manager的cache的作用域,但不定义访问资源的权限。在将Manager的作用域更新为Namespaced之后,基于角色的访问控制(Role-Based Access Control, RBAC)的service account应该受到限制。

这些权限可以在目录 config/rbac/下找到, ClusterRole in role.yaml and ClusterRoleBinding in role_binding.yaml 用于授予operator访问和管理其资源的权限。

image-20220112134812812

改变operator的scope,只需要更新 role.yaml and role_binding.yaml。其他的RBAC清单,例如<kind>__editor_role.yaml<kind>_viewer_role.yamlauth_proxy_*.yaml 与改变operator的resource权限无关。

1.2.1 改变RBAC权限至特定命名空间

要将RBAC权限的范围从集群范围更改为特定命名空间,需要做两点:

  • 使用 Roles 代替 ClusterRoles,比如说将config/rbac/role.yaml更新为:

    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      creationTimestamp: null
      name: manager-role
      namespace: memcached-operator-system
    

    然后运行 make manifests 来更新 config/rbac/role.yaml.

  • 使用 RoleBindings 代替 ClusterRoleBindings。 config/rbac/role_binding.yaml 需要被手动更新:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: manager-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: manager-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: system

1.3 动态配置监视命名空间

不要在main.go文件中硬编码任何Namespaces,一个好的做法是使用环境变量来允许限制性配置。这里建议使用的名称空间是WATCH_NAMESPACE,这是一个在部署时传递给manager的以逗号分隔的名称空间列表。

1.3.1 配置operator的命名空间Namespace

main.go中添加一个函数:

// getWatchNamespace returns the Namespace the operator should be watching for changes
func getWatchNamespace() (string, error) {
    // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE
    // which specifies the Namespace to watch.
    // An empty value means the operator is running with cluster scope.
    var watchNamespaceEnvVar = "WATCH_NAMESPACE"

    ns, found := os.LookupEnv(watchNamespaceEnvVar)
    if !found {
        return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar)
    }
    return ns, nil
}

函数getWatchNamespace返回operator应该监视更改的Namespace。watchNamespaceEnvVar是专门用来表示需要监视的命名空间的环境变量的常量。如果值为空,则意味着operator运行在集群范围内。

main.go中使用环境变量,添加以下代码:

...
//调用上面定义的函数
watchNamespace, err := getWatchNamespace()
//如果返回的是一个nil,则表示将将监控所有命名空间(集群范围)
if err != nil {
    setupLog.Error(err, "unable to get WatchNamespace, " +
       "the manager will watch and manage resources in all namespaces")
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
    Scheme:             scheme,
    MetricsBindAddress: metricsAddr,
    Port:               9443,
    LeaderElection:     enableLeaderElection,
    LeaderElectionID:   "f1c5ece8.example.com",
    Namespace:          watchNamespace, // 值不是nil时的作用域
})
...

config/manager/manager.yaml中定义环境变量:

spec:
  containers:
  - command:
    - /manager
    args:
    - --leader-elect
    image: controller:latest
    name: manager
    resources:
      limits:
        cpu: 100m
        memory: 30Mi
      requests:
        cpu: 100m
        memory: 20Mi
    env:
      - name: WATCH_NAMESPACE
        valueFrom:
          fieldRef:
            fieldPath: metadata.namespace
  terminationGracePeriodSeconds: 10

WATCH_NAMESPACE将在这里设置operator部署的命名空间。例如定义为ns1和ns2:

...
    env:
      - name: WATCH_NAMESPACE
        value: "ns1,ns2"
  terminationGracePeriodSeconds: 10
...

使用环境变量值并检查它是否为多命名空间方案,使用上面的例子:

...
watchNamespace, err := getWatchNamespace()
if err != nil {
    setupLog.Error(err, "unable to get WatchNamespace, " +
        "the manager will watch and manage resources in all Namespaces")
}

// 定义options
options := ctrl.Options{
    Scheme:             scheme,
    MetricsBindAddress: metricsAddr,
    Port:               9443,
    LeaderElection:     enableLeaderElection,
    LeaderElectionID:   "f1c5ece8.example.com",
    Namespace:          watchNamespace, 	// 值不是nil时的作用域
}

// Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2)
// 如果watchNamespace包含“,”则说明是多命名空间方案
if strings.Contains(watchNamespace, ",") {
    setupLog.Info("manager set up with multiple namespaces", "namespaces", watchNamespace)
    // 使用 MultiNamespacedCacheBuilder 配置集群范围
    options.Namespace = ""
    options.NewCache = cache.MultiNamespacedCacheBuilder(strings.Split(watchNamespace, ","))
}

// NewManager中的参数使用定义的options
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), options)
...

2 CRD Scope

自定义资源定义Custom Resource Definitions (CRDs) 包含一个scope字段,用于确定生成的自定义资源Custom Resource(CR)是集群作用域还是命名空间作用域。编写operator的时候可以使用命名空间范围的CRD将对CR的访问限制在某些命名空间中,或者在不同的命名空间中访问不同版本的CR。编写operator的时候可能需要集群范围的CRD,以便查看和访问所有命名空间的CRs。

使用operator-sdk create api命令生成CRD清单,生成的CRD将会在config/crd/bases目录下。CRD的spec.scope字段控制API的范围,有效值为ClusterNamespaced。在一个Operator-sdk项目中,这个值将由operator-sdk create api --namespaced确定,这将会编辑types.go文件中的资源,在其他operator类型中,这个命令将会直接修改CRD的YAML清单中的spec.scpoe字段。

2.1 设置-namespaced flag

当创建一个新的API的时候,--namespaced标记控制生成的CRD是集群范围还是命名空间范围,默认情况下,--namespaced被设置为true,这将范围设置为Namespaced。创建集群范围的API的一个简单的例子:

operator-sdk create api --group cache --version v1alpha1 --kind Memcached --resource=true --controller=true --namespaced=false

2.2 在type.go中设置scope标记

可以手动在GO中的types.go文件中增加和改变kubebuilder scope marker来设置我们资源的scope。这个文件在api/<version>/<kind>_types.go或者api/<group>/<version>/<kind>_types.go位置。

下面是一个将标记设置为集群范围的API类型的示例:

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Cluster

// Memcached is the Schema for the memcacheds API
type Memcached struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   MemcachedSpec   `json:"spec,omitempty"`
	Status MemcachedStatus `json:"status,omitempty"`
}

如果要设置为命名空间范围,要将标记替换为//+kubebuilder:resource:scope=Namespace

2.3 在CRD YAML文件中设置scope

scope可以手动在CRD的Kind的YAML中直接修改。

文件一般位于config/crd/bases/<group>.<domain>_<kind>.yaml中。一个简单的使用YAML文件修改命名空间范围的CRD的样例:

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.2.5
  creationTimestamp: null
  name: memcacheds.cache.example.com
spec:
  group: cache.example.com
  names:
    kind: Memcached
    listKind: MemcachedList
    plural: memcacheds
    singular: memcached
  scope: Namespaced
  subresources:
    status: {}
...   

3 使用EnvTest测试

Operator SDK项目建议使用controller-runtime中的envtest为Operator项目编写测试。Envtest有一个更活跃的贡献者社区,它比Operator SDK的测试框架更成熟,并且它不需要实际的集群来运行测试,这在CI场景中可能是一个巨大的好处。

可以看到构建项目时controller会自动创建一个controllers/suite_test.go文件,这个文件包含了一个使用环envtest和gomega执行集成测试的样板文件。

这个测试可以使用原生GO命令进行测试:

go test controllers/ -v -ginkgo.v

使用SDK工具生成的项目有一个Makefile,其中包含一个在运行make test时的目标测试。当执行make docker-build IMG=<some-registry>/<project-name>:<tag>时,也将执行这个目标测试。

4 高级

4.1 管理CR状态条件

一个常用的模式是在CR的状态中包含Conditions,一个Condition表示了对象状态的最新的可用的观测值。

添加到MemcachedStatus结构的Conditions字段简化了CR条件的管理。它有以下几点功能:

  • 允许调用者添加和删除条件。
  • 确保没有重复项。
  • 对条件进行决定性排序,以避免不必要的调整。
  • 自动处理每个条件的 LastTransitionTime.
  • 提供一个帮助方法,一边轻松确定条件的状态。

要在CR中使用条件状态,添加一个Conditions字段到_type.go结构体中:

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

type MyAppStatus struct {
    // Conditions represent the latest available observations of an object's state
    Conditions []metav1.Condition `json:"conditions"`
}

然后,可以在controller中使用Conditions方法使得设置和移除条件或查看他们目前的值更加简单。

4.2 向Operator中添加第三方资源

Operator的管理器支持client-go方案包中所有的Kubernetes的核心资源类型,并且会在项目中注册所有自定义的资源类型。

import (
    cachev1alpha1 "github.com/example/memcached-operator/api/v1alpha1
    ...
)

func init() {

    // Setup Scheme for all resources
    utilruntime.Must(cachev1alpha1.AddToScheme(scheme))
    //+kubebuilder:scaffold:scheme
}

要添加第三方资源到operator中,必须把它加入到Manager的方案中。通过创建一个AddToScheme()方法或者重用方法可以很轻松地把一个资源加入到方案中。上面的样例展示了定义一个函数然后使用runtime包来创建一个SchemeBuilder

4.2.1 在Manager方案中注册

为第三方资源调用AddToScheme()函数,并在main.go中通过mgr.GetScheme()scheme来将其传递到Manager方案中。

import (
    routev1 "github.com/openshift/api/route/v1"
)

func init() {
    ...

    // Adding the routev1
    utilruntime.Must(clientgoscheme.AddToScheme(scheme))

    utilruntime.Must(routev1.AddToScheme(scheme))
    //+kubebuilder:scaffold:scheme

    ...
}

4.2.2 如果第三方资源没有AddToScheme()函数

使用controller-runtime中的SchemeBuilder包来初始化一个新的scheme builder,使其能够用来将第三方资源注册到Manager的方案中。

例如从external-dns中注册NSEndpoints第三方资源:

import (
    ...
    // 需要用到controller-runtime等包
    "k8s.io/apimachinery/pkg/runtime/schema"
    "sigs.k8s.io/controller-runtime/pkg/scheme"
    ...
    // DNSEndoints
    externaldns "github.com/kubernetes-incubator/external-dns/endpoint"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )

func init() {
    ...

    log.Info("Registering Components.")

    // 使用scheme.Builder初始化一个新的schemeBuilder
    schemeBuilder := &scheme.Builder{GroupVersion: schema.GroupVersion{Group: "externaldns.k8s.io", Version: "v1alpha1"}}
    schemeBuilder.Register(&externaldns.DNSEndpoint{}, &externaldns.DNSEndpointList{})
    if err := schemeBuilder.AddToScheme(mgr.GetScheme()); err != nil {
        log.Error(err, "")
        os.Exit(1)
    }

    ...
}

在添加新导入路径到operator项目以后,如果在项目的根目录下存在vendor/目录,则需要运行go mod vendor来满足这些依赖。

Setup all Controllers中添加控制器之前,需要添加第三方资源。

4.3 在删除时进行清理

Operators可能创建一些对象作为其操作内容的一部分。这些对象的积累会消耗掉一些不必要的资源,拖慢API的速度并且杂乱用户的接口。所以保持operators良好的运行环境并且清理不必要的资源是非常重要的,以下是一些常见的场景。

4.3.1 内部资源

资源清理的一个正确的例子是Jobs的应用。当一个Job被创建的时候,一个或多个Pod作为子资源也被创建。当一个Job被删除的时候,关联的Pods也被随之删除。通过设置父对象(Job)到子对象(Pod)的引用,这是一个非常常用的模式。以下是执行该操作的代码字段,其中“r”表示reconcilier,“ctrl”表示controller-runtime运行库。

ctrl.SetControllerReference(job, pod, r.Scheme)

注意:级联删除的默认行为是后台传播,这意味着子对象的删除请求发生在父对象的请求之后。

4.3.2 外部资源

有时,在删除父资源时,需要清理外部资源或不属于自定义资源(例如跨命名空间的资源)的资源。在这种情况下,可以利用 Finalizers。对具有Finalizers的对象的删除请求将会变成一个更新,在更新期间设置删除时间戳;当Finalizers存在时,不会删除该对象。然后,自定义资源控制器的协调循环需要检查是否设置了删除时间戳,执行外部清理操作,然后删除Finalizers以允许对象的垃圾收集。一个对象上可能存在多个Finalizers,每个Finalizers都有一个键,该键应指示控制器需要删除哪些外部资源。

以下是一个理论上的controller文件,在controllers/memcached_controller.go应用finalizer处理:

import (
    ...
    "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const memcachedFinalizer = "cache.example.com/finalizer"

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    reqLogger := r.log.WithValues("memcached", req.NamespacedName)
    reqLogger.Info("Reconciling Memcached")

    // 获取 Memcached 实例
    memcached := &cachev1alpha1.Memcached{}
    err := r.Get(ctx, req.NamespacedName, memcached)
    if err != nil {
        if errors.IsNotFound(err) {
            // 找不到请求对象,可能已在协调请求后删除
            // Owned对象会自动被垃圾收集。对于其他清理逻辑,需要使用finalizers。
            // return且不重新请求
            reqLogger.Info("Memcached resource not found. Ignoring since object must be deleted.")
            return ctrl.Result{}, nil
        }
        // 读取对象时出错-重新请求
        reqLogger.Error(err, "Failed to get Memcached.")
        return ctrl.Result{}, err
    }

    ...

    // 检查 Memcached 实例是否被标记为删除,这将会由一个删除时间戳指示
    // 如果删除时间戳不空,说明被标记
    isMemcachedMarkedToBeDeleted := memcached.GetDeletionTimestamp() != nil
    if isMemcachedMarkedToBeDeleted {
        if controllerutil.ContainsFinalizer(memcached, memcachedFinalizer) {
            // 运行 finalization 逻辑来执行memcachedFinalizer。
            // 如果 finalization 逻辑失败,不要移除 finalizer 以便我们可以在下一次协调期间重试
            if err := r.finalizeMemcached(reqLogger, memcached); err != nil {
                return ctrl.Result{}, err
            }

            // 移除 memcachedFinalizer ,一旦所有的 finalizers 都被删除,对象就会被删除
            controllerutil.RemoveFinalizer(memcached, memcachedFinalizer)
            // 更新
            err := r.Update(ctx, memcached)
            if err != nil {
                return ctrl.Result{}, err
            }
        }
        return ctrl.Result{}, nil
    }

    // 将 finalizer 添加到CR
    // 如果该实例还未包含 memcachedFinalizer ,则需要添加一个
    if !controllerutil.ContainsFinalizer(memcached, memcachedFinalizer) {
        controllerutil.AddFinalizer(memcached, memcachedFinalizer)
        // 更新
        err = r.Update(ctx, memcached)
        if err != nil {
            return ctrl.Result{}, err
        }
    }

    ...

    return ctrl.Result{}, nil
}

func (r *MemcachedReconciler) finalizeMemcached(reqLogger logr.Logger, m *cachev1alpha1.Memcached) error {
    // TODO(user): 添加operator在删除CR之前需要执行的清理步骤
    // 例如:包括执行备份和删除不属于此CR的资源,如PVC
    reqLogger.Info("Successfully finalized memcached")
    return nil
}

4.3.3 复杂清除逻辑

与前面的场景类似,finalizers可以用于实现复杂清理逻辑。以CronJobs为例,controller维护由CronJob控制器创建的Job的有限列表,以检查是否需要删除。这些列表的大小由CronJob中.spec.successfulJobsHistoryLimit.spec.failedJobsHistoryLimit的字段控制,这两个字段指定应该保留多少已完成的和失败的Job。

4.3.4 敏感资源

需要保护敏感资源,防止意外删除。保护资源的直观示例是PV和PVC的关系。首先创建一个PV,然后用户可以通过创建绑定到PV的PVC请求访问该PV的存储。如果用户试图删除当前由PVC绑定的PV,则不会立即删除该PV。相反,PV的删除被推迟,知道该PV不与任何PVC绑定。

因此,可以再次利用finalizers为自己类似PV的CR来实现类似行为:通过在对象上设置finalizer,我们的controller可以确保在删除finalizer和删除对象之前没有剩余的对象绑定到它身上。此外,创建PVC的用户可以指定当通过回收策略删除PVC时,PV中分配的底层存储发生的情况,有几个选项可以使用,每个选项都定义了通过使用finalizer再次实现的行为。关键的概念是,operator可以让用户决定如何通过finalizer来清理他们的资源,这可能很危险,但也会很有用,具体取决与工作负载。

4.3.5 Leader选举

在operator的生命周期中,在任何给定的时间(例如当operator升级的时候),可能会有多个实例在运行。在这种情况下,有必要通过Leader选举避免多个operator之间的争用,以便只有一个leader实例处理reconciliation,此时其他实例处于非活动状态,但准备在leader下台时接管。

有两种不同的leader选举实现可供选择,每种实现都有自己的负载均衡。

  • leader-with-lease:这个leader pod定期续约leader租约,当无法续订租约时放弃领导权。当现有的leader被隔离时,这种实现允许快速地过度到新的leader。但在某些情况下可能会出现脑裂。
  • leader-for-life:leader pod只有放弃领导权(通过垃圾收集)时,它才会被删除。此实现排除了两个实例错误地作为leader运行的可能性(脑裂)。然而,这种方法可能会推迟选举新的leader。例如,当leader pod位于无响应或者已经分离的node上时,pod-eviction-timeout指示了leader pod从节点上删除并且退出所需要的时间(默认为5min)。

SDK默认启动leader-with-lease的实现。

4.3.5.1 Leader for life

以下示例调用leader.Become()将会阻塞operator,直到它可以通过创建名称为memcached-operator-lock的configmap成为leadaer:

import (
    ...
    "github.com/operator-framework/operator-lib/leader"
)

func main() {
    ...
    err = leader.Become(context.TODO(), "memcached-operator-lock")
    if err != nil {
        log.Error(err, "Failed to retry for leader lock")
        os.Exit(1)
    }
    ...
}

如果operator没有运行在集群内,leader.Become()将会直接返回而不会出错,以跳过leader选举,因为它无法检测operator的namespace。

4.3.5.2 Leader with lease

Leader-with-lease可以通过Manager Options来实现leader选举:

import (
    ...
    "sigs.k8s.io/controller-runtime/pkg/manager"
)

func main() {
    ...
    opts := manager.Options{
        ...
        LeaderElection: true,
        LeaderElectionID: "memcached-operator-lock"
    }
    mgr, err := manager.New(cfg, opts)
    ...
}

如果operator运行在集群范围内,Manager将在启动时返回一个错误,因为它不能识别operator的namespace,以用于创建用于leader选举的configmap。可以通过设置LeaderElectionNamespace选项来覆盖此命名空间。

5 相关

5.1 Controller Runtime Client API

controller-runtime运行库提供了各种抽象,已通过CRUD(create、Update、Delete以及Get和List等)操作监视和协调kubernetes集群中的资源。operator使用至少一个controller在集群内执行一组连贯的任务,通常通过CRUD操作的组合,Opeartor SDK使用controller-runtime的客户端接口,为这些操作提供接口。

controller-runtime 定义了几个用于集群交互的接口:

  • client.Client: 实现在Kubernetes集群上执行CRUD操作。
  • manager.Manager: 管理共享依赖项,例如Caches 和 Clients。
  • reconcile.Reconciler: 将提供的状态和实际的集群状态进行比较,并在发现状态差异时使用Client来更新集群。

5.1.1 Client的使用

5.1.1.1 默认Client

SDK依赖于 manager.Manager 来创建一个 client.Client 接口执行 Create, Update, Delete, Get, and List 操作。Reconcile函数 reconcile.Reconciler。SDK将生成代码来创建一个Manager,该管理器包含一个Cache和一个Client,用于CRUD操作并于与API server通信。

下列代码为controllers/memcached_controller.go,展示了Manager的client是如何传递给reconciler的:

import (
	appsv1 "k8s.io/api/apps/v1"
	ctrl "sigs.k8s.io/controller-runtime"

	cachev1alpha1 "github.com/example/memcached-operator/api/v1alpha1"
)

func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr). // mgr's Client is passed to r.
		For(&cachev1alpha1.Memcached{}).
		Owns(&appsv1.Deployment{}).
		Complete(r)
}

type MemcachedReconciler struct {
    client.Client // Populated above from a manager.Manager.

    Log    logr.Logger
    Scheme *runtime.Scheme
}

分离的client从Cache中读(Get、List),并向API server中写(Create、Update、Delete)。从Cache中读取大大减少了API server上的请求负载,只要缓存由API server更新,读写操作最终是一致的。

嵌入了controller-runtime/pkg/client下的接口:

// Client knows how to perform CRUD operations on Kubernetes objects.
type Client interface {
	Reader
	Writer
	StatusClient

	// Scheme returns the scheme this client is using.
	Scheme() *runtime.Scheme
	// RESTMapper returns the rest this client is using.
	RESTMapper() meta.RESTMapper
}
5.1.1.2 非默认的Client

operator开发人员可能希望创建自己的Client来为从API server中的读取请求(Get、List)服务,而不是为来自Cache中的读取请求服务,controller-runtime为Clients提供了一个构造函数:

// New returns a new Client using the provided config and Options.
func New(config *rest.Config, options client.Options) (client.Client, error)

client.Options允许调用方指定新的Client应该如何与API server通信:

// Options 是创建 Client 的选项
type Options struct {
    // Scheme, 如果提供, 将会被用来把go的结构体映射到 GroupVersionKinds
    Scheme *runtime.Scheme

    // Mapper, 如果提供, 将会被用来把 GroupVersionKinds 映射到 Resources
    Mapper meta.RESTMapper
}

例如:

import (
    "sigs.k8s.io/controller-runtime/pkg/client/config"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

cfg, err := config.GetConfig()
...
c, err := client.New(cfg, client.Options{})
...

当Options为空时,默认值由client.New设置。默认scheme将注册核心Kubernetes资源类型,调用者必须为一个新的Client注册自定义operator类型已识别这些类型。

创建一个新的Client通常没有必要,也不建议这样做,因为默认Client对大多数用例来说以及足够了

5.1.2 Reconcile和Client API

Reconciler实现了 reconcile.Reconciler 的接口,该接口暴露了Reconcile的方法。Reconciler被添加到某一种类的相应的Controller中;Reconcile被调用来响应由 reconcile.Request 对象参数的集群或外部事件,通过控制器来读写集群状态,并返回ctrl.Result。SDK Reconcilers可以访问Client来进行Kubernetes API的调用。

// MemcachedReconciler 调谐一个 Memcached 对象
type MemcachedReconciler struct {
    // client, 由上面的 mgr.Client() 进行初始化
    // 是一种从cache中读取对象并写入apiserver的split client
    client.Client

    Log    logr.Logger


    // scheme定义了用于序列化和反序列化API对象的方法,
    // 用于将组(group)、版本(version)和种类(kind)信息和Go模式之间的相互转换,
    // 以及不同版本的Go模式之间的映射。scheme是版本化API和版本化配置的基础。
    Scheme *runtime.Scheme
}

// Reconcile 监测事件并将集群状态与方法体中定义的所需状态进行协调。
// 如果error为非空或者Result.Requeue为true,Controller将重新请求
// 否则将从队列中删除
func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)

Reconcile 是 Controller 业务逻辑存在的地方,即通过 MemcachedReconciler.client进行Client API调用。 client.Client 执行以下操作:

GET
// Get从Kubernetes集群检索给定对象密钥的API对象,并将其存储在obj中。
func (c Client) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error

注意: client.ObjectKey 只是 types.NamespacedName的别名。

示例:

import (
    "context"
    ctrl "sigs.k8s.io/controller-runtime"
    cachev1alpha1 "github.com/example/memcached-operator/api/v1alpha1"
)

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    ...

    memcached := &cachev1alpha1.Memcached{}
    err := r.Get(ctx, request.NamespacedName, memcached)

    ...
}
List
// List检索给定命名空间和列表选项的对象列表,并将该列表存储在list中。
func (c Client) List(ctx context.Context, list client.Object, opts ...client.ListOption) error

client.ListOption 是一个设置 client.ListOptions 字段的接口。client.ListOption 通过使用提供的实现之一创建:MatchingLabels, MatchingFields, InNamespace

示例:

import (
    "context"
    "fmt"
    "k8s.io/api/core/v1"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    ...

    // 返回请求命名空间中,标签为“instance=<name>”且阶段为“Running”的所有pod。
    podList := &v1.PodList{}
    opts := []client.ListOption{
        client.InNamespace(request.NamespacedName.Namespace),
        client.MatchingLabels{"instance": request.NamespacedName.Name},
        client.MatchingFields{"status.phase": "Running"},
    }
    err := r.List(ctx, podList, opts...)

    ...
}
Create
// Create 将对象obj保存在Kubernetes群集中
func (c Client) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error

client.CreateOption 是一个设置 client.CreateOptions字段的接口。client.CreateOption 通过使用提供的实现之一创建:DryRunAllForceOwnership。通常不需要这些选项。

示例:

import (
    "context"
    "k8s.io/api/apps/v1"
    ctrl "sigs.k8s.io/controller-runtime"
)

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    ...

    dep := &v1.Deployment{ // 任何你想要创建的集群对象
        ...
    }
    err := r.Create(ctx, dep)

    ...
}
Update
// Update 更新Kubernetes集群中给定的obj。
// obj必须是一个结构体的指针,以便可以使用API server返回的内容来更新
// Update不会更新资源状态的子资源
func (c Client) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error

client.UpdateOption 是一个设置 client.UpdateOptions 字段的接口。client.UpdateOption 通过使用提供的实现之一创建:DryRunAllForceOwnership。通常不需要这些选项。

示例:

import (
    "context"
    "k8s.io/api/apps/v1"
    ctrl "sigs.k8s.io/controller-runtime"
)

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    ...

    dep := &v1.Deployment{}
    // 获取想要更新的对象
    err := r.Get(ctx, request.NamespacedName, dep)

    ...

    // 选择正在运行的对象进行更新
    dep.Spec.Selector.MatchLabels["is_running"] = "true"
    err := r.Update(ctx, dep)

    ...
}
Patch
// Patch 修订集群中给定的obj
// obj必须是一个结构体的指针,以便可以使用API server返回的内容来更新
func (c Client) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error

client.PatchOption 是一个设置 client.PatchOptions 字段的接口。client.PatchOption 通过使用提供的实现之一创建:DryRunAllForceOwnership。通常不需要这些选项。

示例:

import (
    "context"
    "k8s.io/api/apps/v1"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    ...

    dep := &v1.Deployment{}
    // 获取想要更新的对象
    err := r.Get(ctx, request.NamespacedName, dep)

    ...

    // merge patch将会保留运行时修改的其他字段
    patch := client.MergeFrom(dep.DeepCopy())
    dep.Spec.Selector.MatchLabels["is_running"] = "true"
    err := r.Patch(ctx, dep, patch)

    ...
}
更新状态子资源

从Client中更新 status subresource 时,必须使用 StatusWriter 。使用 Status()检索状态子资源,并使用 Update() 更新或者 Patch()修补。

Update() 接受 client.UpdateOptionPatch() 接受 client.PatchOption

Status
// Status() 返回一个StatusWriter对象,该对象用于更新它的状态子资源
func (c Client) Status() (client.StatusWriter, error)

示例:

import (
    "context"
    ctrl "sigs.k8s.io/controller-runtime"
    cachev1alpha1 "github.com/example/memcached-operator/api/v1alpha1"
)

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    ...

    mem := &cachev1alpha1.Memcached{}
    err := r.Get(ctx, request.NamespacedName, mem)

    ...

    // Update
    mem.Status.Nodes = []string{"pod1", "pod2"}
    err := r.Status().Update(ctx, mem)

    ...

    // Patch
    patch := client.MergeFrom(mem.DeepCopy())
    mem.Status.Nodes = []string{"pod1", "pod2", "pod3"}
    err := r.Status().Patch(ctx, mem, patch)

    ...
}
Delete
// Delete 从集群中删除给定的obj
func (c Client) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error

client.DeleteOption 是一个设置 client.DeleteOptions字段的接口。client.DeleteOption 通过使用提供的实现之一创建:GracePeriodSeconds, Preconditions, PropagationPolicy.

示例:

import (
    "context"
    "k8s.io/api/core/v1"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    ...

    pod := &v1.Pod{}
    err := r.Get(ctx, request.NamespacedName, pod)

    ...

    if pod.Status.Phase == v1.PodUnknown {
        // Delete the pod after 5 seconds.
        err := r.Delete(ctx, pod, client.GracePeriodSeconds(5))
        ...
    }

    ...
}
DeleteAllOf
// DeleteAllOf 删除与给定选项匹配的给定类型的所有对象
func (c Client) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error

client.DeleteAllOfOption 是一个设置client.DeleteAllOfOptions 字段的接口。client.DeleteAllOfOption 包裹了 client.ListOptionclient.DeleteOption

示例:

import (
    "context"
    "fmt"
    "k8s.io/api/core/v1"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    ...

    // Delete 延迟五分钟后,删除请求命名空间中标签为“instance=<name>”且阶段为“Failed”所有的Pod
    pod := &v1.Pod{}
    opts := []client.DeleteAllOfOption{
        client.InNamespace(request.NamespacedName.Namespace),
        client.MatchingLabels{"instance", request.NamespacedName.Name},
        client.MatchingFields{"status.phase": "Failed"},
        client.GracePeriodSeconds(5),
    }
    err := r.DeleteAllOf(ctx, pod, opts...)

    ...
}

5.2 Logging

Operator SDK生成的operators使用 logr 接口进行 日志记录。这个日志接口有几个后端,比如 zap,SDK默认在生成的代码中使用它。 logr.Logger 暴露 structured logging 方法帮助创建机器可读的日志,并向日志记录添加大量信息。

5.2.1 默认 zap logger

Operator SDK 在构建项目时使用基于 zaplogr 后端。为了帮助配置和使用此logger,SDK包含几个帮助程序函数。

在下面的简单示例中,我们使用 BindFlags()将zap标志集添加到operator的命令行标志中,然后使用 zap.Options{}设置controller-runtime的logger。

默认情况下, zap.Options{} 将会返回一可供生产使用的logger。它使用JSON编码器,从 info 级别开始记录日志。要自定义默认行为,用户可以使用zap标志集并在命令行上至指定flags。zap标志集包括可用于配置logger的以下标志:

  • --zap-devel: 开发模式默认值(encoder=consoleEncoder,logLevel=Debug,stackTraceLevel=Warn)
    生产模式默认值(encoder=jsonEncoder,logLevel=Info,stackTraceLevel=Error);
  • --zap-encoder: Zap日志编码(‘json’ or ‘console’);
  • --zap-log-level: 配置日志详细成都的Zap级别。可以是 ‘debug’, ‘info’, ‘error’, 或任何 > 0 的整数值之一,该整数值对应与增加详细度的自定义调试级别;
  • --zap-stacktrace-level: 捕获stacktraces的Zap级别 ( ‘info’ or ‘error’)
A simple example

Operators在main.go中为所有的operator设置了logger。为了说明其工作原理,查看以下示例:

package main

import (
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
)

//添加了一个名为global的日志记录
var globalLog = logf.Log.WithName("global")

func main() {
	// Add the zap logger flag set to the CLI. The flag set must
	// be added before calling flag.Parse().
	opts := zap.Options{}
	opts.BindFlags(flag.CommandLine)
	flag.Parse()

	logger := zap.New(zap.UseFlagOptions(&opts))
	logf.SetLogger(logger)

	scopedLog := logf.Log.WithName("scoped")

	globalLog.Info("Printing at INFO level")
	globalLog.V(1).Info("Printing at DEBUG level")
	scopedLog.Info("Printing at INFO level")
	scopedLog.V(1).Info("Printing at DEBUG level")
}

运行结果:

2022-01-13T15:56:16.982+0800    INFO    global  Printing at INFO level
2022-01-13T15:56:16.982+0800    DEBUG   global  Printing at DEBUG level
2022-01-13T15:56:16.982+0800    INFO    scoped  Printing at INFO level
2022-01-13T15:56:16.982+0800    DEBUG   scoped  Printing at DEBUG level

5.3 使用Predicates对Operator SDK进行事件筛选

Events 由分配给controller正在监视的资源的 Sources 生成。这些事件由 EventHandlers 转换为请求并传递给Reconcile()Predicates 允许控制器在事件提供给 EventHandlers 之前过滤事件。筛选非常有用,因为controller可能只希望处理特定类型的事件,过滤由助于减少与API server的交互次数,因为仅对EventHandlers转换的事件调用 Reconcile()

5.3.1 Predicate 类型

Predicate 实现以下方法,这些方法接受特定类型的事件,如果事件应由 Reconcile() 处理,则返回true:

// Predicate filters events before enqueuing the keys.
type Predicate interface {
  Create(event.CreateEvent) bool
  Delete(event.DeleteEvent) bool
  Update(event.UpdateEvent) bool
  Generic(event.GenericEvent) bool
}

// Funcs implements Predicate.
type Funcs struct {
  CreateFunc func(event.CreateEvent) bool
  DeleteFunc func(event.DeleteEvent) bool
  UpdateFunc func(event.UpdateEvent) bool
  GenericFunc func(event.GenericEvent) bool
}

例如,任何监视资源的所有创建事件都将传递给 Funcs.Create() ,并在方法计算结果为 false时过滤掉,如果没有为特定类型注册 Predicate 方法,则不会筛选该类型的事件。

所有事件类型都包含关于触发事件的对象和对象本身的Kubernetes metadata 。Predicate 逻辑使用这些数据来决定应该过滤哪些内容。某些事件类型包括与该事件语义相关的其他字段,例如, event.UpdateEvent 包含新旧metadata和对象:

type UpdateEvent struct {
  // ObjectOld is the object from the event.
  ObjectOld runtime.Object

  // ObjectNew is the object from the event.
  ObjectNew runtime.Object
}
使用 Predicates

可以通过builder WithEventFilter() 方法为controller设置任意数量的 Predicates。如果其中任何 Predicates 的计算结果为 false,则该方法将过滤事件。第一个示例是 memcached-operator controller 的一个实现,控制器接收所有发生的删除事件,但是我们可能只关心尚未完成的事件,所以该控制器仅过滤已确认删除的pod上的删除事件:

import (
	...
	corev1 "k8s.io/api/core/v1"
	...
	cachev1alpha1 "github.com/example/app-operator/api/v1alpha1"
)

...

func ignoreDeletionPredicate() predicate.Predicate {
	return predicate.Funcs{
		UpdateFunc: func(e event.UpdateEvent) bool {
			// Ignore更新metadata.Generation状态没有改变的CR
			return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
		},
		DeleteFunc: func(e event.DeleteEvent) bool {
			// 如果已确认删除对象,则计算结果为false。
			return !e.DeleteStateUnknown
		},
	}
}

func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&cachev1alpha1.Memcached{}).
		Owns(&corev1.Pod{}).
		WithEventFilter(ignoreDeletionPredicate()).
		Complete(r)
}
  ...
}

6 Go Operator Tutorial

构建和运行基于Go的operator的深入演练。

创建一个示例项目,了解其工作原理,此示例将:

  • 如果 Memcached Deployment 不存在,就创建它
  • 确保 Deployment 的大小与 Memcached CR spec的大小相同
  • 使用 status writer 根据CR的名称来更新 Memcached CR 状态

创建一个新的项目

使用 CLI 来创建一个新的 memcached-operator 项目

mkdir -p $HOME/projects/memcached-operator
cd $HOME/projects/memcached-operator
# we'll use a domain of example.com
# so all API groups will be <group>.example.com
operator-sdk init --domain example.com --repo github.com/example/memcached-operator

命令init

查看operator-sdk init命令:

operator-sdk init --help

Initialize a new project including the following files:
  - a "go.mod" with project dependencies
  - a "PROJECT" file that stores project configuration
  - a "Makefile" with several useful make targets for the project
  - several YAML files for project deployment under the "config" directory
  - a "main.go" file that creates the manager that will run the project controllers

Usage:
  operator-sdk init [flags]

Flags:
      --component-config         create a versioned ComponentConfig file, may be 'true' or 'false'
      --domain string            domain for groups (default "my.domain")
      --fetch-deps               ensure dependencies are downloaded (default true)
  -h, --help                     help for init
      --license string           license to use to boilerplate, may be one of 'apache2', 'none' (default "apache2")
      --owner string             owner to add to the copyright
      --project-name string      name of this project
      --project-version string   project version (default "3")
      --repo string              name to use for go module (e.g., github.com/user/repo), defaults to the go package of the current working directory.
      --skip-go-version-check    if specified, skip checking the Go version

Global Flags:
      --plugins strings   plugin keys to be used for this subcommand execution
      --verbose           Enable verbose logging

样例:

Examples:
  # Initialize a new project with your domain and name in copyright
  operator-sdk init --plugins go/v3 --domain example.org --owner "Your name"

  # Initialize a new project defining an specific project version
  operator-sdk init --plugins go/v3 --project-version 3

官方:

operator-sdk init --domain example.com --repo github.com/example/memcached-operator

如果不是在$GOPATH/src下做的init,则必须指定一个repo,这个repo的地址实际上是不存在的,实际开发中可以先指定这个github的地址,后续可以push上github做开源。

命令create

create命令是创建kubernetesAPI和webhook的脚手架,可用的参数为api和webhook两种。

查看operator-sdk create命令:

operator-sdk create --help
Scaffold a Kubernetes API or webhook.

Usage:
  operator-sdk create [command]

Available Commands:
  api         Scaffold a Kubernetes API
  webhook     Scaffold a webhook for an API resource

Flags:
  -h, --help   help for create

Global Flags:
      --plugins strings   plugin keys to be used for this subcommand execution
      --verbose           Enable verbose logging

Use "operator-sdk create [command] --help" for more information about a command.

选择api,查看operator-sdk create api详细命令:

通过编写资源定义resource和controller构建Kubernetes API。

如果没有明确提供关于资源resource和控制器controller的信息,它将提示用户是否应该提供。

脚手架写好后,依赖项将被更新,并运行make generate。

operator-sdk create api --help

Scaffold a Kubernetes API by writing a Resource definition and/or a Controller.

If information about whether the resource and controller should be scaffolded
was not explicitly provided, it will prompt the user if they should be.

After the scaffold is written, the dependencies will be updated and
make generate will be run.

Usage:
  operator-sdk create api [flags]

Flags:
      --controller           if set, generate the controller without prompting the user (default true)
      --force                attempt to create resource even if it already exists
      --group string         resource Group
  -h, --help                 help for api
      --kind string          resource Kind
      --make make generate   if true, run make generate after generating files (default true)
      --namespaced           resource is namespaced (default true)
      --plural string        resource irregular plural form
      --resource             if set, generate the resource without prompting the user (default true)
      --version string       resource Version

Global Flags:
      --plugins strings   plugin keys to be used for this subcommand execution
      --verbose           Enable verbose logging

样例:

Examples:
  # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate
  operator-sdk create api --group ship --version v1beta1 --kind Frigate

  # 编写api方案
  nano api/v1beta1/frigate_types.go

  # 编写控制器Controller
  nano controllers/frigate/frigate_controller.go

  # Edit the Controller Test
  nano controllers/frigate/frigate_controller_test.go

  # Generate the manifests
  make manifests

  # 使用kubectl apply命令将CRDs部署到Kubernetes集群上
  make install

  # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config
  make run

官方:

#创建一个domain为example.com
operator-sdk init --domain example.com --repo github.com/example/memcached-operator

#指定group为cache,version为v1alpha1
operator-sdk create api --group cache --version v1alpha1 --kind Memcached --resource --controller

–controller如果设置,则在不提示用户的情况下生成控制器controller(默认为true)。

controller:会在memcached-operator/controllers下生成memcached_controller.go

image-20220112134401531

–resource如果设置,则在不提示用户的情况下生成资源resource(默认为true)。

resource:会在memcached-operator/api/v1alpha1下生成memcached_types.go

image-20220112133424396

会在文件夹config下的simple文件夹下生成yaml:

apiVersion: cache.example.com/v1alpha1
kind: Memcached
metadata:
  name: memcached-sample
spec:
  # Add fields here

Manager

operator的主程序为main.go,初始化并且运行Manager。

Manager可以限制所有控制器将要监视的资源:

mgr, err := ctrl.NewManager(cfg, manager.Options{Namespace: namespace})

默认情况下,这将是operator运行时的命名空间,要监视所有命名空间,需要将命名空间的选项保留为空:

mgr, err := ctrl.NewManager(cfg, manager.Options{Namespace: ""})

这跟1.1 Manager Watching Option的说明保持一致。

创建 API 和 Controller

创建一个CRD API,其中组为cache,版本为v1alpha1,类型为Memcached。出现提示时,输入y来创建resource和controller。

$ operator-sdk create api --group cache --version v1alpha1 --kind Memcached --resource --controller
Writing scaffold for you to edit...
api/v1alpha1/memcached_types.go
controllers/memcached_controller.go
...

这将会构建一个 Memcached 资源的 API 在目录 api/v1alpha1/memcached_types.go ,控制器 controller 在目录 controllers/memcached_controller.go

通常,建议由一个控制器controller负责管理为项目创建的每个API,以正确遵循controller-runtime 设置的设计目标。

定义API

首先,我们将通过定义 Memcached 类型来表示API,该类型具有 MemcachedSpec.Size 字段,用于设置要部署的 memcached 实例 (CRs)的数量,以及一个 MemcachedStatus.Nodes 字段来存储 CR 的Pod的名称name。

通过将 api/v1alpha1/memcached_types.go 中Go类型的定义修改为以下的spec和status,来定义Memcached Custom Resource(CR)的API:

// MemcachedSpec defines the desired state of Memcached
type MemcachedSpec struct {
	//+kubebuilder:validation:Minimum=0
	// Size is the size of the memcached deployment
	Size int32 `json:"size"`
}

// MemcachedStatus defines the observed state of Memcached
type MemcachedStatus struct {
	// Nodes are the names of the memcached pods
	Nodes []string `json:"nodes"`
}

Size是memcached类型部署的数量,并且设置了其最小值为0。

通过添加 +kubebuilder:subresource:status markerstatus subresource 添加到 CRD manifest,以便控制器可以在不更改CR对象其余部分的情况下更新CR状态:

// Memcached is the Schema for the memcacheds API
//+kubebuilder:subresource:status
type Memcached struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   MemcachedSpec   `json:"spec,omitempty"`
	Status MemcachedStatus `json:"status,omitempty"`
}

修改 *_types.go 文件后,必须要运行以下命令以更新该资源类型的生成代码:

make generate

上面的makefile目标将调用 controller-gen 实用程序来更新生成的 api/v1alpha1/zz_generated.deepcopy.go 文件,以确保API的Go类型定义可以实现应用在Kind类型上的 runtime.Object 接口。

生成 CRD manifests

使用 spec/status 字段和 CRD 验证 markers定义API以后,可以使用以下命令生成和更新 CRD manifests:

make manifests

这个 makefile 目标将会调用 controller-genconfig/crd/bases/cache.example.com_memcacheds.yaml 处生成 CRD manifests:

shark@root:~/go-project/memcached-operator$ make generate 
/home/shark/go-project/memcached-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."

shark@root:~/go-project/memcached-operator$ make manifests 
/home/shark/go-project/memcached-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

可以看到yaml文件发生了改变,crd的yaml增加了内容:

image-20220113194657048

OpenAPI validation

生成清单时,OpenAPIv3模式将添加到spec.validation块中的CRD清单中。此验证块允许Kubernetes在创建或更新Memcached自定义资源时验证其属性。

Markers (注释) 可用于为 API 配置验证,这些标记将始终具有 +kubebuilder:validation 的前缀。

kubebuilder CRD generationmarker 文档中讨论了API代码中标记的使用。在这里可以找到OpenAPIv3验证标记的完整列表 CRD Validation - The Kubebuilder Book

实现 Controller

对于本例,请使用 memcached_controller.go

替换生成的控制器文件 controllers/memcached_controller.go

接下来的两个小节将解释controller是如何监控资源resources,以及如何触发reconcile的loop循环。

Controller 监控 Resources

controllers/memcached_controller.go 中的 SetupWithManager() 函数指定了controller如何构建监控CR和该控制器拥有和管理的其他资源。

import (
	...
	appsv1 "k8s.io/api/apps/v1"
	...
)

func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&cachev1alpha1.Memcached{}).
		Owns(&appsv1.Deployment{}).
		Complete(r)
}

函数 NewControllerManagedBy() 提供了一个允许各种控制器配置的controller生成器,比如在5.3中提到的WithEventFilter过滤选项。

For(&cachev1alpha1.Memcached{}) 将 Memcached 类型指定为要监视的主要资源。对每个 Memcachl类型的 Add/Update/Delete 事件(event),协调循环reconcile loop 将为该Memcached对象发送一个 reconcile 请求Request ( namespace/name key) 。

Owns(&appsv1.Deployment{}) 将 Deployments 类型指定为要监视的辅助资源(secondary resource)。对每个 Deployment 类型的 Add/Update/Delete 事件(event),事件处理器 event handler 将每个事件映射到部署所有者(Owner)的 协调请求中reconcile Request 。在本例中,它是由Deployment创建的 Memcached 对象。

Controller 配置

初始化控制器时,可以进行许多其他有用的配置,有关这些配置的详细信息,可以查阅 builder and controller

  • 通过使用predicates筛选器设置控制器的最大并发 Reconciles 次数 MaxConcurrentReconciles选项。默认值为1。此处设置为2:

    func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
      return ctrl.NewControllerManagedBy(mgr).
        For(&cachev1alpha1.Memcached{}).
        Owns(&appsv1.Deployment{}).
        WithOptions(controller.Options{MaxConcurrentReconciles: 2}).
        Complete(r)
    }
    
  • 使用 predicates 设置过滤选项

  • 选择 EventHandler 的类型来更改监控事件转换为 reconcile loop 的 reconcile requests 的方式。对于比主资源和辅助资源更复杂的operator关系,可以使用 EnqueueRequestsFromMapFunc 处理器来将监控事件转换为任意一组协调请求。

Reconcile loop

调谐功能能负责在系统的实际状态上强制执行所需的CR状态。每次监控的CR或资源发生变化的事件时,它都会运行,并根据这些状态是否匹配的结果返回对应的值。

通过这种方法,每个控制器都有一个 Reconcile() 方法,该方法具有实现调谐循环的功能。Reconcile loop 将传递请求参数 Request ,该参数适用于从Cache中查找主资源对象 Memcached 的 Namespace/Name key:

import (
	ctrl "sigs.k8s.io/controller-runtime"
	cachev1alpha1 "github.com/example/memcached-operator/api/v1alpha1"
	...
)

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  // 查找这个 reconcile request 中的 Memcached 实例 
  memcached := &cachev1alpha1.Memcached{}
  err := r.Get(ctx, req.NamespacedName, memcached)
  ...
}

以下是 Reconciler 可能返回的几个选项:

  • 发生 error :

    return ctrl.Result{}, err
    
  • 没有发生 error :

    return ctrl.Result{Requeue: true}, nil
    
  • 停止 Reconcile :

    return ctrl.Result{}, nil
    
  • 在 X 时间后重新 Reconcile :

     return ctrl.Result{RequeueAfter: nextRun.Sub(r.Now())}, nil
    

指定权限并生成 RBAC 清单

控制器需要特定的 RBAC 权限才能与其管理的资源进行交互,这些是通过 RBAC markers 指定的,如下所示:

//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;

func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  ...
}

ClusterRole 清单位于 config/rbac/role.yaml ,这是通过以下命令从上述markers中生成:

make manifests
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;

这两条命令在原本生成的controller中是没有的,把他们加入congtroller中并运行命令,再次查看role.yaml文件,可以看到多出这些内容:

image-20220113204545800

配置 operator 的 image 注册表

剩下的事情就是构建operator镜像并将其推送到所需的镜像注册表。

在构建operator镜像之前,请确保生成的 Dockerfile 引用所需的基本镜像。我们可以通过将其标记替换为另外一个标记来更改默认的 “runner” 镜像 gcr.io/distroless/static:nonroot ,比如 alpine:latest ,并删除 USER 65532:65532 指令。

以下是Dockerfile中的部分内容:

# Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]

Makefile 根据项目初始化时写入的值或CLI中写入的值组合镜像的标记(tags)。特别地, IMAGE_TAG_BASE 允许我们为所有镜像标记定义一个公共的镜像注册表、命名空间和部分名称。如果当前值不正确,请将其更新到其他的注册表或命名空间,之后,可以更新 IMG 变量定义,如下所示:

-IMG ?= controller:latest
+IMG ?= $(IMAGE_TAG_BASE):$(VERSION)

完成后,不必在CLI中设置 IMG 或任何镜像变量,以下命令将生成并推送标记为 example.com/memcached-operator:v0.0.1 的operator镜像到Docker Hub:

make docker-build docker-push

docker的问题

Docker daemon权限问题

如果碰到docker的权限问题:

Got permission denied while trying to connect to the Docker daemon socket at ...

运行命令:

sudo chmod a+rw /var/run/docker.sock

如果碰到无法连接docker问题:

docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?.

则进入 /var/run/ 目录,启动docker:

cd /var/run/
sudo service docker start
timeout问题

如果碰到问题:

go: github.com/onsi/ginkgo@v1.16.4: Get "https://proxy.golang.org/github.com/onsi/ginkgo/@v/v1.16.4.mod": dial tcp 142.251.43.17:443: i/o timeout

修改/etc/docker/daemon.json配置文件:

sudo vim /etc/docker/daemon.json

添加:

{
    "registry-mirrors":[
        "https://hub-mirror.c.163.com",
        "https://registry.aliyuncs.com",
        "https://registry.docker-cn.com",
        "https://docker.mirrors.ustc.edu.cn"
    ]
}

重启docker:

sudo service docker restart

如果还是有问题,尝试修改GOSUMDB。原本的GOSUMDB:

GOSUMDB="sum.golang.org"

可以设置 GOSUMDB=“sum.golang.google.cn”, 这个是专门为国内提供的sum 验证服务。

go env -w GOSUMDB="sum.golang.google.cn"

如果还是有问题,应该是docker不与宿主机共享代理造成的,需要为docker容器内部配置proxy环境变量:

将 Docker 配置为使用代理服务器|Docker 文档

用户权限问题
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: ...

添加当前用户权限:

sudo groupadd docker     #添加docker用户组
sudo gpasswd -a $USER docker     #将登陆用户加入到docker用户组中
newgrp docker     #更新用户组
docker ps    #测试docker命令是否可以使用sudo正常使用

运行 Operator

由三种方法可以运行operator:

1. 在集群外部本地运行

以下步骤将说明如何在集群上部署operator。 但是,为了开发目的而在本地和集群之外运行,请使用目标 make install run.

2. 在集群中作为Deloyment运行

默认情况下,将创建一个名为 <project-name>-system 的命名空间,例如 memcached-operator-system ,并将用于部署。

运行以下命令以部署 operator ,这还将从 config/rbac 安装RBAC清单:

make deploy

3.使用 OLM 部署

安装olm:

operator-sdk olm install

绑定 operator ,然后构建并推送绑定镜像。bundle 目标生成一个 bundlebundle 目录中,其中包含operator清单和元数据.。bundle-buildbundle-push 定义并且推送bundle镜像 bundle.Dockerfile

最后,运行 bundle 。如果捆绑包镜像托管在私有或自定义的CA注册表中,则必须完成这些安装步骤 configuration steps

operator-sdk run bundle <some-registry>/memcached-operator-bundle:v0.0.1

创建 Memcached CR

更新 Memcached CR 清单 config/samples/cache_v1alpha1_memcached.yaml 并且定义 spec

apiVersion: cache.example.com/v1alpha1
kind: Memcached
metadata:
  name: memcached-sample
spec:
  size: 3

Create the CR:

kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml
$ kubectl get pods
NAME                                  READY     STATUS    RESTARTS   AGE
memcached-sample-6fd7c98d8-7dqdr      1/1       Running   0          1m
memcached-sample-6fd7c98d8-g5k7v      1/1       Running   0          1m
memcached-sample-6fd7c98d8-m7vn7      1/1       Running   0          1m

清除

运行以下命令删除所有已部署的资源:

kubectl delete -f config/samples/cache_v1alpha1_memcached.yaml
make undeploy
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值