1 - controller-runtime介绍

controller-runtime

Kubernetes controller-runtime 项目是一套用于构建 controller 控制器的 Go 库。它被 Kubebuilder 和 operator SDK 所使用。对于新项目来说,这两个项目都是一个很好的起点。

2 - controller-runtime package概况

controller-runtime package概况

https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg

pkg 包提供了用于构建 controller 控制器的库。控制器实现了 Kubernetes API,是构建 operator、workload API、配置API、自动缩放器等的基础。

client

客户端为读写Kubernetes对象提供了一个 读+写 的客户端。

cache

缓存提供了一个读客户端,用于从本地缓存中读取对象。缓存可以注册 handler 来响应更新缓存的事件。

manager

Manager是创建 controller 控制器的必要条件,并提供 controller 的共享依赖关系,如客户端、缓存、scheme 等。 controller 应该通过 manager 调用 Manager.Start 来启动。

Controller

Controller 通过响应事件(对象创建、更新、删除)来实现 Kubernetes API,并确保对象的 Spec 中指定的状态与系统的状态相匹配。这被称为 reconcile(调和)。如果它们不匹配,Controller 将根据需要创建/更新/删除对象,使它们匹配。

Controller 被实现为处理 reconcile.Requests(为特定对象 reconcile 状态的请求)的工作队列。

与 http handler 不同的是,Controller 不直接处理事件,而是将 Requests 入列来最终 reconcile 对象。这意味着多个事件的处理可能会被批量进行,而且每次 reconcile 都必须读取系统的全部状态。

  • Controller 需要提供一个 Reconciler 来执行从工作队列中拉出的工作。

  • Controller 需要配置 Watches,以便将 reconcile.Requests 入列并对事件进行响应。

Webhook

Admission Webhooks 是一种扩展 kubernetes APIs 的机制。Webhooks 可以配置目标事件类型(对象创建、更新、删除),API 服务器将在某些事件发生时向它们发送 AdmissionRequests。Webhooks 可以修改和(或)验证嵌入在 AdmissionReview 请求中的对象,并将响应发回给 API 服务器。

有2种类型的 Admission Webhooks :修改(mutating)和验证(validating)的 Admission Webhooks 。修改的 webhook 是用来在 API 服务器接纳之前修改 core API 对象或 CRD 实例。验证 webhook 用于验证对象是否符合某些要求。

  • Admission Webhooks 需要提供 Handler(s)来处理收到的 AdmissionReview 请求。

Reconciler

Reconciler 是提供给 Controller 的一个函数,可以在任何时候用对象的名称和命名空间来调用。当被调用时,Reconciler 将确保系统的状态与 Reconciler 被调用时在对象中指定的内容相匹配。

例子: 为一个 ReplicaSet 对象调用 Reconciler。ReplicaSet 指定了5个副本,但系统中只存在 3 个 Pod。Reconciler 又创建了2个Pod,并将其 OwnerReference 设置为指向 ReplicaSet,并设置 controller=true。

  • Reconciler 包含 Controller 的所有业务逻辑。

  • Reconciler 通常只对单一对象类型工作。- 例如,它将只对 ReplicaSets 进行调节。对于单独的类型,使用单独的 Controller。如果你想从其他对象触发 reconcile,你可以提供一个映射(例如所有者引用/owner references),将触发 reconcile 的对象映射到被 reconcile 的对象。

  • Reconciler 被提供给要 reconcile 的对象的名称/命名空间。

  • Reconciler 不关心负责触发 Reconcile 的事件内容或事件类型。例如,ReplicaSet 是否被创建或更新并不重要,Reconciler 总是将系统中的 Pod 数量与它被调用时在对象中指定的数量进行比较。

Source

resource.Source 是 Controller.Watch 的参数,提供了事件流。事件通常来自于 watch Kubernetes APIs(例如:Pod Create, Update, Delete)。

例如:source.Kind 使用 Kubernetes API Watch 端点,为 GroupVersionKind 提供创建、更新、删除事件。

  • Source 通常通过 Watch API 为 Kubernetes 对象提供事件流(如对象创建、更新、删除)。

  • 在几乎所有情况下,用户都应该只使用所提供的 Source 实现,而不是实现自己的。

EventHandler

handler.EventHandler 是 Controller.Watch 的参数,它在响应事件时入队 reconcile.Requests。

例如:一个来自 Source 的 Pod 创建事件被提供给 eventhandler.EnqueueHandler,它入队包含 Pod 的名称/命名空间的reconcile.Request。

  • EventHandlers 通过入队 reconcile.Requests 来为一个或多个对象处理事件。

  • EventHandlers 可以将一个对象的事件映射到同一类型的另一个对象的 reconcile.Request 上。

  • EventHandlers 可以将一个对象的事件映射到不同类型的另一个对象的 reconcile.Request 。例如,将 Pod 事件映射到拥有它的 ReplicaSet 的 reconcile.Request 上。

  • EventHandlers 可以将一个对象的事件映射到相同或不同类型的对象的多个 reconcile.Request 。例如,将一个 Node 事件映射到响应集群大小事件的对象上。

  • 在几乎所有情况下,用户都应该只使用所提供的 EventHandler 实现,而不是实现自己的。

Predicate

predicate.Predicate 是 Controller.Watch 的一个可选的参数,用于过滤事件。这使得常见的过滤器可以被重复使用和组合。

  • Predicate 接收一个事件,并返回bool( true为enqueue )。

  • Predicate 是可选的参数

  • 用户应该使用所提供的 Predicate 实现,但可以实现额外的 Predicate,例如:generation 改变,标签选择器改变等。

PodController 图例

Source 提供事件:

  • &source.KindSource{&v1.Pod{}} -> (Pod foo/bar Create Event)

EventHandlers 入队请求:

  • &handler.EnqueueRequestForObject{} -> (reconcile.Request{types.NamespaceName{Name: "foo", Namespace: "bar"}})

Reconciler 被调用,携带参数Request:

  • Reconciler(reconcile.Request{types.NamespaceName{Name: "foo", Namespace: "bar"}})

用法

下面的例子显示了创建一个新的 Controller 程序,该程序响应 Pod 或 ReplicaSet 事件,对 ReplicaSet 对象进行 Reconcile。Reconciler 函数只是给 ReplicaSet 添加一个标签。

参见 examples/builtins/main.go 中的使用实例。

Controller 实例:

    1. Watch ReplicaSet 和 Pods Source
    • 1.1 ReplicaSet -> handler.EnqueueRequestForObject – 用 ReplicaSet 的命名空间和名称来入队请求。

    • 1.2 Pod (由 ReplicaSet 创建) -> handler.EnqueueRequestForOwnerHandler –以拥有的 ReplicaSet 命名空间和名称来入队Request。

    1. 响应事件,对 ReplicaSet 进行 Reconcile
    • 2.1 创建 ReplicaSet 对象 -> 读取 ReplicaSet,尝试读取 Pods -> 如果空缺就创建 Pods。

    • 2.2 创建 Pods 锁触发的 Reconciler -> 读取 ReplicaSet 和 Pods,什么都不做。

    • 2.3 从其他角色中刪除Pods而触发Reconciler -> 读取 ReplicaSet 和 Pods,创建替代Pods。

监视和事件处理

Controller 可以观察多种类型的对象(如Pods、ReplicaSets和 deployment),但他们只 reconcile 一个类型。当一种类型的对象必须更新以响应另一种类型的对象的变化时,EnqueueRequestsFromMapFunc 可用于将事件从一种类型映射到另一种类型。例如,通过重新 reconcile 某些API的所有实例来响应集群大小调整事件(添加/删除节点)。

Deployment Controller 可能使用 EnqueueRequestForObjectEnqueueRequestForOwner 来:

  • 观察 Deployment 事件–入队 Deployment 的命名空间和名称。

  • 观察 ReplicaSet 事件– 入队创建 ReplicaSet 的 Deployment 的命名空间和名称(例如,所有者/Owner)。

注意:reconcile.Requests 在入队的时候会被去重。同一个 ReplicaSet 的许多 Pod 事件可能只触发1个 reconcile 调用,因为每个事件都会导致 handler 试图为 ReplicaSet 入队相同的 reconcile.Request。

Controller的编写技巧

Reconciler 运行时的复杂性:

  • 最好是编写 Controllers 来执行N次O(1) reconcile(例如对N个不同的对象),而不是执行1次O(N) reconcile(例如对一个管理着N个其他对象的单一对象)。

  • 例子: 如果你需要更新所有服务以响应一个节点的添加– reconcile 服务但观察节点(转变为服务对象的名称/命名空间),而不是 reconcile 节点并更新服务。

事件复用:

  • 对同一名称/命名空间的 reconcile 请求在入队时被批量和去重处理。这使 Controller 能够优雅地处理单个对象的大量事件。将多个事件源复用到一个对象类型上,将对不同对象类型的事件进行批量请求。

  • 例如: ReplicaSet 的 Pod 事件被转换为 ReplicaSet 名称/命名空间,因此 ReplicaSet 将只对来自多个 Pod 的多个事件进行1次Reconciled。

目录

builder Package builder 封装了其他 controller-runtime 库,并公开了构建普通 controller 的简单模式。
cache Package cache provides object caches that act as caching client.Reader instances and help drive Kubernetes-object-based event handlers. 包缓存提供对象缓存,作为缓存客户端.阅读器实例,帮助驱动基于Kubernetes对象的事件处理程序。
certwatcher Package certwatcher is a helper for reloading Certificates from disk to be used with tls servers.
client Package client contains functionality for interacting with Kubernetes API servers.
cluster
config Package config contains functionality for interacting with configuration for controller-runtime components.
controller Package controller provides types and functions for building Controllers.
conversion Package conversion provides interface definitions that an API Type needs to implement for it to be supported by the generic conversion webhook handler defined under pkg/webhook/conversion.
envtest Package envtest provides libraries for integration testing by starting a local control plane
event Package event contains the definitions for the Event types produced by source.Sources and transformed into reconcile.Requests by handler.EventHandler.
finalizer
handler Package handler defines EventHandlers that enqueue reconcile.Requests in response to Create, Update, Deletion Events observed from Watching Kubernetes APIs.
healthz Package healthz contains helpers from supporting liveness and readiness endpoints.
leaderelection Package leaderelection contains a constructor for a leader election resource lock.
log Package log contains utilities for fetching a new logger when one is not already available.
manager Package manager is required to create Controllers and provides shared dependencies such as clients, caches, schemes, etc.
metrics Package metrics contains controller related metrics utilities
predicate Package predicate defines Predicates used by Controllers to filter Events before they are provided to EventHandlers.
ratelimiter Package ratelimiter defines rate limiters used by Controllers to limit how frequently requests may be queued.
reconcile Package reconcile defines the Reconciler interface to implement Kubernetes APIs.
recorder Package recorder defines interfaces for working with Kubernetes event recorders.
scheme Package scheme contains utilities for gradually building Schemes, which contain information associating Go types with Kubernetes groups, versions, and kinds.
source Package source provides event streams to hook up to Controllers with Controller.Watch.
webhook Package webhook provides methods to build and bootstrap a webhook server.

3 - 使用builder的基础controller

https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/builder#example-Builder

概述

Package builder包装了其他的控制器运行时库,并展示了构建普通控制器的简单模式。

如果项目在未来需要更多的自定义行为,使用builder包构建的项目可以在底层包的基础上进行简单的重构。

例子

这个例子创建了一个简单的应用程序 ControllerManagedBy,为ReplicaSets 和 Pod 进行了配置。

  • 为ReplicaSets创建一个新的应用程序,管理 ReplicaSet 所拥有的 Pod,并调用到 ReplicaSetReconciler。

  • 启动应用程序。

package main

import (
	"context"
	"fmt"
	"os"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"

	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/config"

	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	"sigs.k8s.io/controller-runtime/pkg/manager/signals"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

func main() {
	logf.SetLogger(zap.New())

	var log = logf.Log.WithName("builder-examples")

	mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{})
	if err != nil {
		log.Error(err, "could not create manager")
		os.Exit(1)
	}

	err = builder.
		ControllerManagedBy(mgr).  // Create the ControllerManagedBy
		For(&appsv1.ReplicaSet{}). // ReplicaSet is the Application API
		Owns(&corev1.Pod{}).       // ReplicaSet owns Pods created by it
		Complete(&ReplicaSetReconciler{
			Client: mgr.GetClient(),
		})
	if err != nil {
		log.Error(err, "could not create controller")
		os.Exit(1)
	}

	if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
		log.Error(err, "could not start manager")
		os.Exit(1)
	}
}

// ReplicaSetReconciler is a simple ControllerManagedBy example implementation.
type ReplicaSetReconciler struct {
	client.Client
}

// Implement the business logic:
// This function will be called when there is a change to a ReplicaSet or a Pod with an OwnerReference
// to a ReplicaSet.
//
// * Read the ReplicaSet
// * Read the Pods
// * Set a Label on the ReplicaSet with the Pod count.
func (a *ReplicaSetReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {

	rs := &appsv1.ReplicaSet{}
	err := a.Get(ctx, req.NamespacedName, rs)
	if err != nil {
		return reconcile.Result{}, err
	}

	pods := &corev1.PodList{}
	err = a.List(ctx, pods, client.InNamespace(req.Namespace), client.MatchingLabels(rs.Spec.Template.Labels))
	if err != nil {
		return reconcile.Result{}, err
	}

	rs.Labels["pod-count"] = fmt.Sprintf("%v", len(pods.Items))
	err = a.Update(ctx, rs)
	if err != nil {
		return reconcile.Result{}, err
	}

	return reconcile.Result{}, nil
}

仅有

package main

import (
	"context"
	"fmt"
	"os"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"

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

	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/config"

	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	"sigs.k8s.io/controller-runtime/pkg/manager/signals"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

func main() {
	logf.SetLogger(zap.New())

	var log = logf.Log.WithName("builder-examples")

	mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{})
	if err != nil {
		log.Error(err, "could not create manager")
		os.Exit(1)
	}

	cl := mgr.GetClient()
	err = builder.
		ControllerManagedBy(mgr).                  // Create the ControllerManagedBy
		For(&appsv1.ReplicaSet{}).                 // ReplicaSet is the Application API
		Owns(&corev1.Pod{}, builder.OnlyMetadata). // ReplicaSet owns Pods created by it, and caches them as metadata only
		Complete(reconcile.Func(func(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
			// Read the ReplicaSet
			rs := &appsv1.ReplicaSet{}
			err := cl.Get(ctx, req.NamespacedName, rs)
			if err != nil {
				return reconcile.Result{}, client.IgnoreNotFound(err)
			}

			// List the Pods matching the PodTemplate Labels, but only their metadata
			var podsMeta metav1.PartialObjectMetadataList
			err = cl.List(ctx, &podsMeta, client.InNamespace(req.Namespace), client.MatchingLabels(rs.Spec.Template.Labels))
			if err != nil {
				return reconcile.Result{}, client.IgnoreNotFound(err)
			}

			// Update the ReplicaSet
			rs.Labels["pod-count"] = fmt.Sprintf("%v", len(podsMeta.Items))
			err = cl.Update(ctx, rs)
			if err != nil {
				return reconcile.Result{}, err
			}

			return reconcile.Result{}, nil
		}))
	if err != nil {
		log.Error(err, "could not create controller")
		os.Exit(1)
	}

	if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
		log.Error(err, "could not start manager")
		os.Exit(1)
	}
}

4 - controller-runtime设计

https://github.com/kubernetes-sigs/controller-runtime/tree/main/designs

这些是对 Controller Runtime 进行修改的设计文件。它们的存在是为了帮助记录编写 Controller Runtime 的设计过程,但可能不是最新的(更多内容见下文)。

并非所有对 Controller Runtime 的修改都需要设计文档–只有重大的修改才需要。使用你的最佳判断。

当提交设计文件时,我们鼓励你写概念验证,而且完全可以在提交设计文件的同时提交概念验证PR,因为概念验证过程可以帮助消除困惑,并且可以帮助模板中的示例部分。

过时的设计

Controller Runtime 文档 GoDoc 应该被认为是 Controller Runtime 的经典的、最新的参考和架构文档。

然而,如果你看到一个过时的设计文档,请随时提交一个PR,将其标记为这样,并添加一个链接到问题的附录,记录事情的变化原因。比如说:

4.1 - 缓存选项

描述对未来缓存选项的设想

https://github.com/kubernetes-sigs/controller-runtime/blob/main/designs/cache_options.md

这份文件描述了我们对未来缓存选项的设想。

目标

  • 使每个人对我们想要支持的缓存的设置及其配置保持一致
  • 确保我们既支持复杂的缓存设置,又提供一个直观的配置用户体验。

非目标

  • 描述缓存本身的设计和实现。我们的假设是,最细的级别是 “每个对象的多命名空间和不同的选择器”/“per-object multiple namespaces with distinct selectors”,这可以通过一个 “元缓存”/“meta cache” 来实现,这个 “元缓存” 委托给每个对象,并通过扩展当前的多命名空间缓存。
  • 概述这些设置何时实施的时间表。只要有人站出来做实际的工作,实施就会逐渐发生。

提案

const (
   AllNamespaces = corev1.NamespaceAll
)

type Config struct {
  // LabelSelector 指定标签选择器。nil值允许默认。
  LabelSelector labels.Selector

  // FieldSelector 指定字段选择器。nil值允许默认。
  FieldSelector fields.Selector

  // Transform 指定转换函数。nil值允许默认。
  Transform     toolscache.TransformFunc

  // UnsafeDisableDeepCopy 指定针对缓存的List和Get请求是否不需要DeepCopy。nil值允许默认。
  UnsafeDisableDeepCopy *bool
}


type ByObject struct {
  // Namespaces 将命名空间名称映射到缓存设置。如果设置,只有该映射中的命名空间将被缓存。
  // 
  // 映射值中的设置如果因为整体值为零或具体设置为零而未设置,将被默认。为防止这种情况,请为特定的设置使用一个空值。
  // 
  // 可以通过使用 AllNamespaces 常量作为映射键,为某些命名空间设置特定的Config,但缓存所有命名空间。这将包括所有没有特定设置的名字空间。
  // 
  // nil的映射允许将其默认为缓存的DefaultNamespaces设置。
  //
  // 空的映射可以防止这种情况。
  //
  // 对于集群范围内的对象必须取消设置。
  Namespaces map[string]*Config

  // Config will be used for cluster-scoped objects and to default
  // Config in the Namespaces field.
  //
  // It gets defaulted from the cache'sDefaultLabelSelector, DefaultFieldSelector,
  // DefaultUnsafeDisableDeepCopy and DefaultTransform.
  Config *Config
}

type Options struct {
  // ByObject specifies per-object cache settings. If unset for a given
  // object, this will fall through to Default* settings.
  ByObject map[client.Object]*ByObject

  // DefaultNamespaces maps namespace names to cache settings. If set, it
  // will be used for all objects that have a nil Namespaces setting.
  //
  // It is possible to have a specific Config for just some namespaces
  // but cache all namespaces by using the `AllNamespaces` const as the map
  // key. This wil then include all namespaces that do not have a more
  // specific setting.
  //
  // The options in the Config that are nil will be defaulted from
  // the respective Default* settings.
  DefaultNamespaces map[string]*Config

  // DefaultLabelSelector is the label selector that will be used as
  // the default field label selector for everything that doesn't
  // have one configured.
  DefaultLabelSelector labels.Selector

  // DefaultFieldSelector is the field selector that will be used as
  // the default field selector for everything that doesn't have
  // one configured.
  DefaultFieldSelector fields.Selector

  // DefaultUnsafeDisableDeepCopy is the default for UnsafeDisableDeepCopy
  // for everything that doesn't specify this.
  DefaultUnsafeDisableDeepCopy *bool

  // DefaultTransform will be used as transform for all object types
  // unless they have a more specific transform set in ByObject.
  DefaultTransform toolscache.TransformFunc

  // HTTPClient is the http client to use for the REST client
  HTTPClient *http.Client

  // Scheme is the scheme to use for mapping objects to GroupVersionKinds
  Scheme *runtime.Scheme

  // Mapper is the RESTMapper to use for mapping GroupVersionKinds to Resources
  Mapper meta.RESTMapper

  // SyncPeriod determines the minimum frequency at which watched resources are
  // reconciled. A lower period will correct entropy more quickly, but reduce
  // responsiveness to change if there are many watched resources. Change this
  // value only if you know what you are doing. Defaults to 10 hours if unset.
  // there will a 10 percent jitter between the SyncPeriod of all controllers
  // so that all controllers will not send list requests simultaneously.
  //
  // This applies to all controllers.
  //
  // A period sync happens for two reasons:
  // 1. To insure against a bug in the controller that causes an object to not
  // be requeued, when it otherwise should be requeued.
  // 2. To insure against an unknown bug in controller-runtime, or its dependencies,
  // that causes an object to not be requeued, when it otherwise should be
  // requeued, or to be removed from the queue, when it otherwise should not
  // be removed.
  //
  // If you want
  // 1. to insure against missed watch events, or
  // 2. to poll services that cannot be watched,
  // then we recommend that, instead of changing the default period, the
  // controller requeue, with a constant duration `t`, whenever the controller
  // is "done" with an object, and would otherwise not requeue it, i.e., we
  // recommend the `Reconcile` function return `reconcile.Result{RequeueAfter: t}`,
  // instead of `reconcile.Result{}`.
  SyncPeriod *time.Duration

}