controller-runtime
1 - controller-runtime介绍
Kubernetes controller-runtime 项目是一套用于构建 controller 控制器的 Go 库。它被 Kubebuilder 和 operator SDK 所使用。对于新项目来说,这两个项目都是一个很好的起点。
2 - 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 实例:
-
- Watch ReplicaSet 和 Pods Source
-
1.1
ReplicaSet -> handler.EnqueueRequestForObject
– 用 ReplicaSet 的命名空间和名称来入队请求。 -
1.2
Pod (由 ReplicaSet 创建) -> handler.EnqueueRequestForOwnerHandler
–以拥有的 ReplicaSet 命名空间和名称来入队Request。
-
- 响应事件,对 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 可能使用 EnqueueRequestForObject
和 EnqueueRequestForOwner
来:
-
观察 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
}