学习 Kubernetes 客户端的使用
客户端
- 1: client-go
- 1.1: client-go介绍
- 1.1.1: client-go介绍
- 2: controller-runtime
- 2.1: controller-runtime介绍
- 2.2: controller-runtime package概况
- 2.3: 使用builder的基础controller
- 2.4: controller-runtime设计
- 2.4.1: 缓存选项
- 3: operator
- 4: kubebuilder
- 5: Kubernetes Programming with go学习笔记
- 5.1: [第一章]Kubernetes API介绍
- 5.1.1: Kubernetes 平台一览
- 5.1.2: openapi规范
- 5.1.3: Group-Version-Resource
- 5.1.4: Verbs 和 Kinds
- 5.1.5: 子资源
- 5.1.6: 官方API参考文档
- 5.2: [第二章]Kubernetes API操作
- 5.3: [第三章]在Go中使用API资源
- 5.4: [第4章]使用通用类型
- 5.5: [第5章]API Machinery
- 5.6: [第6章]client-go类库
- 5.7: [第7章]测试使用Client-go的应用程序
- 5.8: [第8章]用自定义资源定义扩展Kubernetes API
- 5.9: [第9章]使用自定义资源
- 5.10: [第10章]用Controller-Runtime库编写Operator
- 5.10.1: controller-runtime简介
- 5.10.2: Manager
- 5.10.3: Controller
- 5.10.4: 将管理器资源注入 Reconciler 中
- 5.10.5: 使用客户端
- 5.10.6: 日志
- 5.10.7: 事件
- 5.11: [第11章]编写 Reconcile 循环
- 5.12: [第12章]测试 Reconcile 循环
- 5.13: [第13章]使用 kubebuilder 创建 Operator
- 6: Programming Kubernetes with go
1 - client-go
1.1 - client-go介绍
1.1.1 - client-go介绍
client-go是一个调用kubernetes集群资源对象API的客户端,即通过client-go实现对kubernetes集群中资源对象(包括deployment、service、ingress、replicaSet、pod、namespace、node等)的增删改查等操作。大部分对kubernetes进行前置API封装的二次开发都通过client-go这个第三方包来实现。
2 - controller-runtime
2.1 - controller-runtime介绍
Kubernetes controller-runtime 项目是一套用于构建 controller 控制器的 Go 库。它被 Kubebuilder 和 operator SDK 所使用。对于新项目来说,这两个项目都是一个很好的起点。
2.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. |
2.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)
}
}
2.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,将其标记为这样,并添加一个链接到问题的附录,记录事情的变化原因。比如说:
2.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
}
5 - Kubernetes Programming with go学习笔记
https://learning.oreilly.com/library/view/kubernetes-programming-with/9781484290262/
本书首先介绍了Kubernetes API的结构以及它为哪些运维服务。接下来的章节展示了如何使用API和API库中定义的Go结构编写本地Kubernetes资源定义。还介绍了各种实用工具,以帮助您处理不同的资源字段,并将您的资源定义转换为YAML或JSON。接下来,你将学习如何与Kubernetes API服务器互动,使用client-go库创建、删除、更新和监控集群中的资源。有一整章专门介绍了为使用 client-go库 测试你的程序而提供的工具。接下来有一个例子来总结本书的第一部分,描述了如何编写一个kubectl插件。接下来,你将学习如何使用自定义资源定义扩展Kubernetes API,以及如何以通用方式编写Kubernetes资源,以及如何使用非结构化概念创建自己的资源。接下来的章节将深入研究 controller-runtime 库,它对通过编写 operator 来扩展Kubernetes非常有用,以及利用该库的 kubebuilder 框架,帮助你在几分钟内开始编写 operator。
读完本书后,你将深入了解Kubernetes API的结构以及Kubernetes资源在其中的组织方式,并拥有一个完整的工具箱来帮助你编写Kubernetes客户端和 operator 程序。
你将学到什么
- 了解Kubernetes API及其资源是如何组织的
- 用Go编写Kubernetes资源
- 在集群中创建资源
- 利用你新获得的知识来编写Kubernetes客户端和 operator 程序
5.1 - [第一章]Kubernetes API介绍
Kubernetes平台的入口是API。本章通过强调Kubernetes API的核心作用来探索Kubernetes的架构。然后,它侧重于Kubernetes API的HTTP REST性质,以及为组织它所管理的许多资源而添加的扩展。
5.1.1 - Kubernetes 平台一览
在链条的一侧,用户声明高级资源来构建要部署的应用程序: Deployments,Ingresses,等等。
在中间,controller 被激活,将这些资源转化为低级别的资源(Pod),调度器将这些资源分配到节点。在链条的另一端,节点代理将低级别的资源部署到节点上。
Kubernetes 平台的主要元素(通常称为控制平面)在图1-1中突出显示,并在下文中描述:
-
API server: 这是控制平面上的中心点;用户和控制平面的各个部分联系这个API来创建、获取、删除、更新和观察资源。
-
etcd数据库: 只能由API服务器访问,用于保存与资源有关的数据。
-
Controller manager: 运行 Controller,将用户声明的高级资源转化为部署在节点上的低级别资源。控制器连接到API服务器,观察高层资源,创建、删除和更新低层资源,以满足高层资源中声明的规格。
-
Scheduler: 将低级资源分配到各个节点上。调度器连接到API服务器,观察未受影响的资源并将其连接到节点上。
-
Kubelet: 这是一个运行在集群所有节点上的代理,每个代理都管理着影响到其节点的工作负载。kubelet 连接到 API 服务器,观察影响到其节点的Pods资源,并使用本地容器运行时部署相关容器。
-
Kube proxy: 这是一个在集群的所有节点上运行的代理,每个代理都管理着影响其节点的网络配置。Kube proxy 连接到API服务器,观察服务资源并在其节点上配置相关的网络规则。
5.1.2 - openapi规范
Kubernetes 的API 是HTTP REST API。Kubernetes 团队提供了 OpenAPI 格式的API规范,V2格式的规范在 https://github.com/kubernetes/kubernetes/tree/master/api/openapi-spec,Kubernetes v1.24 v3 格式的规范在 https://github.com/kubernetes/kubernetes/tree/master/api/openapi-spec/v3。
这些规范也可以从 API 服务器的这些路径访问: /openapi/v2
和 /openapi/v3
。
OpenAPI规范是由不同部分组成的,其中有路径列表和定义列表。路径是你用来请求这个API的URL,对于每个路径,规范给出了不同的操作,如 get、delte 或 post 。然后,对于每个操作,规范指出了什么是请求的参数和主体格式,以及什么是可能的响应代码和相关的响应主体格式。
请求和响应的参数和主体可以是简单的类型,也可以是更普遍的包含数据的结构。定义列表包括帮助建立操作的请求和响应的参数和主体的数据结构。
图1-2是一个 User API 规范的简化视图。这个API可以接受两个不同的路径: /user/{userId}
和 /user
。第一个路径,/user/{userId}
,可以接受两个操作,分别是 get 和 delete,用来接收特定用户的信息,给出其用户ID;以及删除特定用户的信息,给出其用户ID。第二个路径,/user
,可以接受一个单一的操作,post,以添加一个新的用户,给定其信息。
在这个API中,给出了一个 User 结构体的定义,描述了一个用户的信息:其ID、first name 和 last name。这个数据结构被用于第一条路径上的 get 操作的响应体中,以及第二条路径上的 post 操作的请求体中。
5.1.3 - Group-Version-Resource
Kubernetes API 是 REST API,因此它管理资源和资源的路径,这些资源遵循REST的命名惯例–即使用复数名称来识别资源,并将这些资源分组。
因为 Kubernetes API 管理着数以百计的资源,所以它们被分组,而且因为 API 的发展,这些资源是有版本的。由于这些原因,每个资源都属于一个给定的 group和 version,每个资源都由 Group-Version-Resource 唯一标识,通常称为 GVR。
要找到 Kubernetes API 中的各种资源,你可以浏览 OpenAPI 规范,提取不同的路径。传统的资源(如pod或节点)将在 Kubernetes API 的早期引入,都属于组 core 和版本v1。
管理整个集群的遗留资源的路径遵循 /api/v1/<plural_resource_name>
的格式–例如,/api/v1/nodes
来管理节点。请注意,core group 在路径中不被代表。要管理特定命名空间中的资源,路径格式是 /api/v1/namespaces/<namespace_name>/<plural_resource_name>
–例如,/api/v1/namespaces/default/pods
用于管理默认命名空间中的pod。
较新的资源可以通过格式为 /apis/<group>/<version>/<plural_resource_name>
或 /apis/<group>/<version>/namespaces/<namespace_name>/<plural_resource_name>
的路径访问。
概括地说,获取资源的各种路径的格式是:
-
/api/v1/<plural_name>
- 访问传统的非命名空间的资源。例如:/api/v1/nodes
,访问无命名空间的节点资源。或 -
访问整个集群的传统命名方式的资源,例如:
/api/v1/pods
,访问所有命名空间的pod。 -
/api/v1/namespaces/<ns>/<plural_name>
- 访问特定命名空间中的遗留带命名空间的资源。例如:/api/v1/namespaces/default/pods
,访问默认命名空间中的pod。 -
/apis/<group>/<version>/<plural_name>
- 访问特定组和版本中的非命名空间资源。例如:/apis/storage.k8s.io/v1/storageclasses
来访问非命名空间的storageclasses(组storage.k8s.io,版本v1)。或 -
访问整个集群的命名空间资源。例如:
/apis/apps/v1/deployments
,访问所有命名空间的 deployment。 -
/apis/<group>/<version>/namespaces/<ns>/<plural_name>
- 访问特定命名空间的命名资源。例如:/apis/apps/v1/namespaces/default/deployments
访问默认命名空间中的deployment(分组为 apps,版本为 v1)。
5.1.4 - Verbs 和 Kinds
Kubernetes API为该规范添加了两个概念:Kubernetes API Verbs (动词)和Kubernetes Kinds。
Kubernetes API Verbs 被直接映射到 OpenAPI 规范中的操作。定义的 Verbs 动词是 get、create、update、patch、delete、list、watch 和d eletecollection。与HTTP动词的对应关系可以在表1-1中找到。
表1-1 Kubernetes API动词和HTTP动词的对应关系
Kubernetes API Verb | HTTP Verb |
---|---|
get | GET |
create | POST |
update | PUT |
patch | PATCH |
delete | DELETE |
list | GET |
watch | GET |
deletecollection | DELETE |
Kubernetes Kinds 是 OpenAPI 规范中定义的一个子集。当向 Kubernetes API 发出请求时,数据结构会通过请求和响应的主体进行交换。这些结构共享共同的字段,apiVersion
和 kind
,以帮助请求的参与者识别这些结构。
如果你想让你的 User API 管理这个 Kind 概念,User 结构体将包含两个额外的字段,apiVersion
和 kind
–例如,其值为 v1
和 User
。要确定 Kubernetes OpenAPI 规范中的定义是否是 Kubernetes Kind,你可以查看定义的 x-kubernetes-group-version-kind
字段。如果这个字段被定义了,那么该定义就是一个 kind,它给你提供了apiVersion
和 kind
字段的值。
5.1.5 - 子资源
按照REST API的惯例,资源可以有子资源。子资源是属于另一个资源的,可以通过在资源名称后面指定其名称来访问,如下所示:
-
/api/v1/<plural>/<res-name>/<sub-resource>
例如: /api/v1/nodes/node1/status
-
/api/v1/namespaces/<ns>/<plural>/<res-name>/<sub-resource>
例如: /api/v1/namespaces/ns1/pods/pod1/status
-
/apis/<group>/<version>/<plural>/<res-name>/<sub-resource>
例如: /apis/storage.k8s.io/v1/volumeattachments/volatt1/status
-
/apis/<grp>/<v>/namespaces/<ns>/<plural>/<name>/<sub-res>
例如: /apis/apps/v1/namespaces/ns1/deployments/dep1/status
大多数 Kubernetes 资源都有一个 status 子资源。你可以看到,在编写 operator 时,operator 需要更新 status 子资源,以便能够表明 operator 观察到的这个资源的状态。在status 子资源中可以执行的操作有:get、patch 和 update。Pod有更多的子资源,包括 attach, binding, eviction, exec, log, portforward, 和 proxy。这些子资源对于获取特定的正在运行的 pod 的信息,或者在正在运行的 pod 上执行一些特定的操作,等等都很有用。
可以缩放的资源(即 deployment 、replicaset 等)有一个 scale 子资源。可以在 scale 子资源中执行的操作是 get、patch和update。
5.1.6 - 官方API参考文档
该API的官方参考文档在 https://kubernetes.io/docs/reference/kubernetes-api/。API管理的资源首先按类别(即工作负载、存储等)分组,对于每个类别,你可以获得一个带有简短描述的资源名称列表(图1-3)。
注意,这些类别并不是 Kubernetes API 定义的一部分,而是在本网站中用来帮助没有经验的用户在众多的可用资源中找到自己的方向。
准确地说,显示的名称不是REST意义上的资源名称,而是相关的主体种类,如图1-4所示。例如,在管理Pod时,REST路径中使用的资源名称是pods(即小写和复数),而在HTTP请求中用于交换Pod信息的定义被命名为Pod(即大写和单数)。请注意,其他种类可以与同一资源相关联。在本章的例子中,PodList种类(用于交换关于Pod列表的信息)也存在。
Deployment 文档
让我们来探讨一下这个地址提供的 Deployment 的参考文档:https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/deployment-v1/。该页的标题 “Deployment” 是与图1-5所示的 Deployment 资源有关的主要种类。
头中指出的 apiVersion 可以帮助您为部署资源编写 YAML 清单,因为您需要为 Kubernetes 清单中的每个资源指定 apiVersion 和 kind。
在这种情况下,您知道部署的清单将以下列内容开始:
apiVersion: apps/v1
kind: Deployment
下一个 header 指出了编写Go代码时要使用的 import 。在第三章中,你将看到在Go中描述资源时如何使用这个导入。
在 header 之后,描述了一个结构体定义的列表,也可以从图1-6中的 Deployment 文档页的目录中获取。第一个是资源的主要种类(kind),后面可以选择用于第一种领域的结构体定义。
例如,Deployment kind 包含一个 spec
字段,类型为 DeploymentSpec
,这将在后面描述。请注意,DeploymentSpec
不是在HTTP请求期间直接交换的结构体,因此,它不是一个 kind ,也不包含 kind 或 apiVersion 字段。
在主要 kind 及其相关定义之后,将显示与该资源相关的其他 kind。在本例中,是 DeploymentList kind。
操作文档
资源的API文档的下一个主题是对该资源或其子资源可能进行的操作列表,也可以从目录页中访问(见图1-6)。如图1-7所示,通过检查创建部署的操作细节,你可以看到要使用的HTTP请求动词和路径、请求过程中要传递的参数,以及可能的响应。请求使用的HTTP动词是POST,路径是 /apis/apps/v1/namespaces/{namespace}/deployments
。
路径的 {namespace}
部分表示一个路径参数,它将被你想在其上创建部署的命名空间的名称所取代。你可以指定查询参数:dryRun、fieldManager、fieldValidation 和 pretty。这些参数将遵循格式为 path?dryRun=All
的路径。
请求的主体必须是一个Deployment 的种类。当使用 kubectl 时,你正在编写包含这个主体的 Kubernetes Manifest。在第三章中,你将看到如何在Go中构建主体。响应的HTTP代码可能是: 200、201、202和401;对于2xx代码,响应体将包含一个 Deployment 类型。
pod文档
有些结构体包含许多字段。对于它们,Kubernetes API 文档对字段进行了分类。一个例子是Pod资源的文档。
Pod 资源的文档页面首先包含主要种类 Pod 的描述,然后是 PodSpec 结构体的描述。PodSpec 结构体包含约40个字段。为了帮助你理解这些字段之间的关系,并简化它们的探索,它们被安排成不同的分类(categories)。PodSpec 字段的分类如下: Containers, Volumes, Scheduling, Lifecycle,等等。
此外,对于包含嵌套字段的字段,它们的描述一般是 inline 显示,以避免在结构体描述之间来回穿梭。然而,对于复杂的结构体,描述会随后在页面上报告,并且在字段名旁边有一个链接,以便能够方便地访问它。
对于 Spec 和 Status 结构来说,这种情况一直存在,因为它们在几乎所有的资源中都非常常见。此外,Pod中使用的一些结构体也是如此–例如,Container、EphemeralContainer、LifecycleHandler、NodeAffinity等等。
一些在多个资源中使用的结构被放置在通用定义部分,在字段名旁边有一个链接,可以很容易地访问它。在图1-8中,你可以看到 PodSpec 结构描述里面的容器类别。
你还可以看到,containers 和 initContainers 这些字段的类型与Container相同,Container在本页后面有描述,可以通过链接访问。imagePullSecrets字段的类型是 LocalObjectReference,这在通用定义部分有描述,也可以通过链接访问。
单页版的文档
另一个版本的API参考文档存在,并在一个页面上呈现。这个版本涵盖了一个Kubernretes版本所服务的所有资源版本(不仅仅是最新的版本)。这个版本(如果你想,改变路径的最后部分,以导航到另一个Kubernetes版本)可以在以下网址找到:
https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/
总结
在本章中,你已经能够发现Kubernetes平台的架构,以及API server 发挥的核心作用。Kubernetes API 是一个HTTP REST API,资源被分类为各种有版本的分组。
类型 kind 是用于在API服务器和客户端之间交换数据的特定结构。你可以使用 Kubernetes 官方网站,以人类可读的形式浏览API规范,发现各种资源和种类的结构,每个资源和子资源的不同操作,以及它们相关的动词。
5.2 - [第二章]Kubernetes API操作
上一章介绍了 Kubernetes API 遵循 REST 原则,使用户能够操作资源。
在本章中,你将学习如何通过直接发出 HTTP 请求来执行各种操作。在你的日常工作中,你可能不需要直接与HTTP层进行交互,但了解API在这个层面上的工作原理是很重要的,这样你就能理解如何用更高层次的库更容易地使用它。
检查请求
在开始编写你自己的HTTP请求之前,你可以用 kubectl 检查哪些请求是在执行kubectl命令时使用的。这可以通过使用大于或等于6的 verbose 标志-v来实现。表2-1显示了在每个级别上显示的信息。
例如,如果你想知道在获取所有命名空间的pod时被调用的URL,你可以使用以下命令:
$ kubectl get pods --all-namespaces -v6
loader.go:372] Config loaded from file: /home/user/.kube/config
round_trippers.go:553] GET https://192.168.1.194:6443/api/v1/pods?limit=500 200 OK in 745 milliseconds
在该命令的输出中,你可以看到使用的路径是 /api/v1/pods
。或者,在获取特定命名空间的pod时,你可以看到使用的路径是 /api/v1/namespaces/default/pods
$ kubectl get pods --namespace default -v6
loader.go:372] Config loaded from file: /home/user/.kube/config
round_trippers.go:553] GET https://192.168.1.194:6443/api/v1/namespaces/default/pods?limit=500 200 OK in 138 milliseconds
- 动词级别
Level | Method and URL | Request timing | Events timing | Request headers | Response status | Response headers | Curl cmd | Body length |
---|---|---|---|---|---|---|---|---|
-v 6 | yes | yes | – | – | – | – | – | 0 |
-v 7 | yes | – | – | yes | yes | – | – | 0 |
-v 8 | yes | – | – | yes | yes | yes | - | ≤ 1024 |
-v 9 | yes | yes | yes | – | – | yes | yes | ≤ 10240 |
-v 10 | yes | yes | yes | – | – | yes | yes | ∞ |
提出请求
本节研究了你可以对Kubernetes资源进行的所有操作。
使用 kubectl 作为代理
你必须经过认证才能向集群的 Kubernetes API 发出请求,除非你的集群接受未经认证的请求,但这是不可能的。
运行认证的HTTP请求的方法是使用kubectl作为代理,使其处理认证。为此,可以使用 kubectl proxy
命令:
kubectl proxy
开始在 127.0.0.1:8001 上提供服务
在一个新的终端上,你现在可以运行你的 HTTP 请求,不需要任何认证。接下来,定义一个HOST变量来访问代理:
HOST=http://127.0.0.1:8001
创建资源
你可以通过首先创建描述该资源的Kubernetes清单来创建一个新的资源–例如,要创建一个Pod,你可以写:
cat > pod.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- image: nginx
name: nginx
EOF
然后,你需要将资源描述传递到POST请求的正文中(注意,-X POST标志可以省略,因为使用的是 -data-binary
标志)。例如,要创建一个pod资源,请使用:
curl $HOST/api/v1/namespaces/project1/pods -H "Content-Type: application/yaml" --data-binary @pod.yaml
这等同于运行kubectl命令:
kubectl create --namespace project1 -f pod.yaml -o json
请注意,命名空间在 pod.yaml 文件中没有被指出。如果你添加它,你必须在 YAML 文件和路径中指定相同的命名空间,否则你会得到一个错误–即提供的对象的命名空间与请求中发送的命名空间不匹配。
获取资源的信息
你可以使用GET请求来获取特定资源的信息,并在路径中传递其名称作为参数(如果是命名空间的资源,还要传递其命名空间)。在这个例子中,你将请求project1命名空间中的nginx的信息:
curl -X GET $HOST/api/v1/namespaces/project1/pods/nginx
这将返回JSON格式的资源信息,使用与该资源相关的种类作为结构;在这个例子中,它是一个Pod种类。这等同于运行kubectl命令:
kubectl get pods --namespace project1 nginx -o json
获取资源列表
对于命名空间的资源,你可以获得整个集群或特定命名空间的资源列表。对于非命名空间的资源,你可以获得资源的列表。在任何情况下,你将使用GET请求。
集群范围
要获得集群范围内的资源列表,对于带命名空间的或不带命名空间的资源;例如,对于pod资源,使用以下方法:
curl $HOST/api/v1/pods
这将返回所有命名空间的pod列表的信息,使用PodList种类。这等同于运行kubectl命令:
kubectl get pods --all-namespaces -o json
在特定的命名空间中
要获得特定命名空间中的资源列表,你需要在路径中指明命名空间;例如,对于pod资源,可以这样使用:
curl $HOST/api/v1/namespaces/project1/pods
这将返回project1命名空间中的pod列表信息,使用PodList种类。这等同于运行kubectl命令:
kubectl get pods --namespace project1 -o json
筛选列表的结果
当运行一个 list 请求时,你会得到这类资源的完整列表,在指定的命名空间或集群范围内,取决于你的请求。
你可能想过滤这个结果。在Kubernetes中过滤资源最常见的方法是使用标签 label。为此,资源需要有定义的标签;然后,在列表请求中,你可以定义一些标签选择器。也可以通过使用字段选择器,根据一组有限的字段来过滤资源。
使用标签选择器
所有 Kubernetes 资源都可以定义标签。例如,在创建pod时,你可以用kubectl定义一些标签:
kubectl run nginx1 --image nginx --labels mylabel=foo
kubectl run nginx2 --image nginx --labels mylabel=bar
这导致了带有在资源的元数据部分定义的标签的 pod:
$ kubectl get pods nginx1 -o yaml
apiVersion: v1
kind: Pod
metadata:
labels:
mylabel: foo
name: nginx1
[...]
$ kubectl get pods nginx2 -o yaml
apiVersion: v1
kind: Pod
metadata:
labels:
mylabel: bar
name: nginx2
[...]
现在,当运行 list 请求时,你可以通过使用 labelSelector 查询参数定义一些标签选择器来过滤这些资源,这个参数可以包含一个逗号分隔的选择器列表。
-
选择所有定义特定标签的资源,无论其值如何;例如,mylabel标签:
curl "$HOST/api/v1/namespaces/default/pods?labelSelector=mylabel"
-
选择所有没有定义特定标签的资源;例如,mylabel标签:
curl "$HOST/api/v1/namespaces/default/pods?labelSelector=\!mylabel"
注意:
标签名称前的感叹号(!)–使用反斜线字符(\)是因为感叹号是shell的一个特殊字符。
-
选择所有定义有特定值的标签的资源;例如,mylabel的值是foo:
curl "$HOST/api/v1/namespaces/default/pods?labelSelector=mylabel==foo"
或者
curl "$HOST/api/v1/namespaces/default/pods?labelSelector=mylabel=foo"
-
选择所有定义标签的资源,其值与特定的标签不同;例如,标签mylabel的值与foo不同:
curl "$HOST/api/v1/namespaces/default/pods?labelSelector=mylabel\!=foo"
注意:
等号(=)前的感叹号(!)–使用反斜杠字符(\)是因为感叹号是shell的一个特殊字符。
-
选择所有定义有一组值的标签的资源;例如,具有foo或baz其中一个值的标签mylabel:
curl "$HOST/api/v1/namespaces/default/pods?labelSelector=mylabel+in+(foo,baz)"
注意:
加号字符(+),它对URL中的空格进行编码。原来的选择器是:mylabel in (foo,baz)。
-
选择所有定义标签的资源,其值不在一组值中;例如,标签mylabel的值与foo或baz不同:
curl "$HOST/api/v1/namespaces/default/pods?labelSelector=mylabel+notin+(foo,baz)"
注意:
加号字符(+),它对URL中的空格进行编码。原来的选择器是:mylabel notin (foo,baz)。
你可以用逗号把几个选择器分开来组合。这将充当一个 AND 操作符。例如,要选择所有定义了标签 mylabel 并且标签otherlabel等于bar的资源,你可以使用以下标签选择器:
curl "$HOST/api/v1/namespaces/default/pods?labelSelector=mylabel,otherlabel==bar"
使用字段选择器
你可以使用一组有限的字段来过滤资源。对于所有资源,你可以在 metadata.name
字段上进行过滤;对于所有 namespaced
资源,你可以在metadata.namespace
字段上过滤。
下面是 Kubernetes 1.23的其他可用于过滤的字段列表,具体取决于资源:
core.event:
- involvedObject.apiVersion
- involvedObject.fieldPath
- involvedObject.kind
- involvedObject.name
- involvedObject.namespace
- involvedObject.resourceVersion
- involvedObject.uid
- reason
- reportingComponent
- source
- type
core.namespace:
- status.phase
core.node:
- spec.unschedulable
core.pod:
- spec.nodeName
- spec.restartPolicy
- spec.schedulerName
- spec.serviceAccountName
- status.nominatedNodeName
- status.phase
- status.podIP
core.replicationcontroller:
- status.replicas
core.secret:
- type
apps.replicaset:
- status.replicas
batch.job:
- status.successful
certificates.certificatesigningrequest:
- spec.signerName
现在,当运行 list 请求时,你可以通过使用 fieldSelector 参数来指示一些字段选择器来过滤这些资源,该参数可以包含一个逗号分隔的选择器列表。
-
选择某个字段有特定值的所有资源;例如,字段 status.phase 的值为Running:
curl "$HOST/api/v1/namespaces/default/pods?fieldSelector=status.phase==Running"
或者
curl "$HOST/api/v1/namespaces/default/pods?fieldSelector=status.phase=Running"
-
选择某个字段的值与特定字段不同的所有资源;例如,字段 status.phase 的值与 Running 不同:
curl "$HOST/api/v1/namespaces/default/pods?fieldSelector=status.phase\!=Running"
你可以用逗号把几个选择器分开来组合。这将充当一个AND运算符。例如,要选择所有 phase 等于 “运行 “且重启策略不是 “总是 “的 pod,你可以使用这个字段选择器:
curl "$HOST/api/v1/namespaces/default/pods?fieldSelector=status.phase==Running,spec.restartPolicy\!=Always"
删除资源
要删除资源,你需要在路径中指定它的名字(和带有命名空间的资源的命名空间)并使用DELETE请求。例如,要删除pod,使用下面的方法:
curl -X DELETE "$HOST/api/v1/namespaces/project1/pods/nginx"
这将以 JSON 格式返回被删除资源的信息,使用与该资源相关的种类–本例是Pod kind。
这相当于运行kubectl命令(除了你不能得到关于被删除资源的信息,只能通过-o name标志得到它的名字):
kubectl delete pods --namespace project1 nginx
删除资源的集合
也可以使用DELETE请求删除特定命名空间中的特定资源集合;对于命名空间的资源,在路径中指明命名空间:
curl -X DELETE "$HOST/api/v1/namespaces/project1/pods"
这将以JSON格式返回被删除资源的信息,使用与资源相关的List种类;在这个例子中,PodList种类。
这相当于运行kubectl命令(除了不能获得被删除资源的信息,只能通过-o name标志获得其名称):
kubectl delete pods --namespace project1 --all
注意
不可能像使用kubectl命令那样,在一次请求中从所有命名空间中删除所有特定种类的资源:kubectl delete pods –all-namespaces –all。
更新资源
通过使用PUT请求并在路径中指定名称(和带有命名空间的资源的命名空间)以及在请求正文中指定新的资源信息,可以替换关于特定资源的完整信息。
为了说明这一点,你可以首先定义一个新的 deployment,使用以下命令:
cat > deploy.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
EOF
然后,你可以用以下方法在集群中创建这个部署:
curl "$HOST/apis/apps/v1/namespaces/project1/deployments" -H "Content-Type: application/yaml" --data-binary @deploy.yaml
接下来,你可以为部署创建一个更新的清单;例如,通过以下命令更新容器的镜像(这将把镜像名称nginx替换为nginx:latest):
cat deploy.yaml | sed 's/image: nginx/image: nginx:latest/' > deploy2.yaml
最后,你可以使用以下请求来更新部署到集群中:
curl -X PUT "$HOST/apis/apps/v1/namespaces/project1/deployments/nginx" -H "Content-Type: application/yaml" --data-binary @deploy2.yaml
这等同于运行kubectl命令:
kubectl replace --namespace project1 -f deploy2.yaml -o json
更新资源时的冲突管理
当用以前的技术更新一个资源时,如果另一个参与者在你创建它和你更新它之间对资源进行了修改,那么在你更新资源时,其他参与者所做的修改将会丢失。
为了避免这种冲突的风险,你可以先读取资源信息(使用GET请求),在资源的元数据中找到 resourceVersion 字段的值,然后在你要更新的资源的规格中指出这个resourceVersion。
通过发送带有该 resourceVersion 的PUT请求,API 服务器将比较所收到的资源和当前资源的 resourceVersion 值。如果数值不同(因为另一个参与者在此期间修改了该资源),API服务器将回复错误: 操作无法在[…]上完成:该对象已被修改;请将您的修改应用到最新的版本上并重试。
为了说明问题,让我们来创建部署(如果你已经从上一节创建了它,请确保在这样做之前删除它):
curl "$HOST/apis/apps/v1/namespaces/project1/deployments" -H "Content-Type: application/yaml" --data-binary @deploy.yaml
你将收到一个响应,表明你所创建的资源的 resourceVersion;在这个例子中,它是668867:
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "nginx",
"namespace": "project1",
"uid": "99d3a1eb-176c-40de-89ec-74313169fe60",
"resourceVersion": "668867",
"generation": 1,
[...]
}
在等待几秒钟后,你可以执行一个GET请求来查找最新的版本,你会收到以下响应:
curl "$HOST/apis/apps/v1/namespaces/project1/deployments/nginx"
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "nginx",
"namespace": "project1",
"uid": "99d3a1eb-176c-40de-89ec-74313169fe60",
"resourceVersion": "668908",
"generation": 1,
[...]
}
你可以看到 resourceVersion 已经被递增,现在是668908。发生这种情况是因为部署控制器已经自行更新了资源。
现在,如果你把第一次收到的版本添加到YAML清单中,并试图更新部署,你会得到一个错误,表明已经检测到冲突:
cat > deploy2.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
resourceVersion: "668867"
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
EOF
curl -X PUT "$HOST/apis/apps/v1/namespaces/project1/deployments/nginx" -H "Content-Type: application/yaml" --data-binary @deploy2.yaml
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "Operation cannot be fulfilled on deployments.apps \"nginx\": the object has been modified; please apply your changes to the latest version and try again",
"reason": "Conflict",
"details": {
"name": "nginx",
"group": "apps",
"kind": "deployments"
},
"code": 409
}
现在,如果你使用以下命令用最新的资源版本更新YAML清单,并再次运行PUT命令,操作将成功:
$ sed -i 's/668867/668908/' deploy2.yaml
$ curl -X PUT
$HOST/apis/apps/v1/namespaces/project1/deployments/nginx
-H "Content-Type: application/yaml"
--data-binary @deploy2.yaml
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "nginx",
"namespace": "project1",
"uid": "99d3a1eb-176c-40de-89ec-74313169fe60",
"resourceVersion": "671623",
"generation": 2,
[...]
}
使用 Strategic Merge Patch 来更新资源
当修改资源时,与其发送完整的描述,不如通过使用补丁只发送你想修改的部分。
这可以通过使用带有 application/strategic-merge-patch+json content-type 的 PATCH 请求来实现,并在路径中指定名称(和带有命名空间的资源的命名空间),在请求的正文中指定补丁信息。
补丁信息是 YAML 清单的提取物,其中只包含你想更新的字段。这样做,你在路径中指定的字段将被更新,而补丁中未指定的字段将保持不动。
为了说明这一点,您可以先用以下方式创建一个包含补丁信息的文件:
$ cat > deploy-patch.json <<EOF
{
"spec":{
"template":{
"spec":{
"containers":[{
"name":"nginx",
"image":"nginx:alpine"
}]
}}}}
EOF
然后,你可以通过以下请求将这个补丁应用于资源:
请注意具体使用的 Content-Type 头为
pplication/strategic-merge-patch+json
。
$ curl -X PATCH
$HOST/apis/apps/v1/namespaces/project1/deployments/nginx
-H
"Content-Type: application/strategic-merge-patch+json"
--data-binary @deploy-patch.json
这等同于运行 kubectl 命令:
$ kubectl patch deployment nginx --namespace project1
--patch-file deploy-patch.json
--type=strategic
-o json
当字段是单一的值(要么是像字符串这样的简单值,要么是一个有几个字段的对象),补丁的值会取代现有的值。
注意:
如果字段在补丁中不存在,原始值将不会被删除,而是保持不动。你可以为字段指定 null 值,以便从结果中删除它。
修补数组字段
当字段包含数组的值时,其行为与单个值的行为不同。
默认行为取决于 Kubernetes API 规范中为该字段定义的补丁策略。例如,你可以在图2-1中看到,container 结构体 的 env 字段在键名上的补丁策略是 Merge 。补丁策略的另一个可能值是 Replace。
当字段的补丁策略是 Replace(替换)时,结果数组是包含在补丁中的数组,而原始数组中的值不被考虑。
当字段的补丁策略是在特定的键上合并时,原始数组和补丁数组将被合并。补丁数组中包含的元素,但不存在于原始数组中,将被添加到结果中,原始数组和补丁数组中存在的元素将获得补丁元素的值(如果元素的键值相同,则原始数组和补丁数组中的元素是相同的)。
注意
原数组中存在的元素,但在补丁中不存在,将保持不动。
为了说明问题,考虑现有的部署,其中有一个定义了这些环境变量的容器,以及一个定义了这些值的补丁:
**Original Patch**
env: env:
\- name: key1 - name: key1
value: value1 value: value1bis
\- name: key2 - name: key3
value: value2 value: value3
通过将该补丁应用于现有的部署,产生的环境变量列表将是以下内容:
Result for Merge strategy
env:
- name: key1
value: value1bis
- name: key2
value: value2
- name: key3
value: value3
替换指令
你可以对对象或数组使用替换指令。当对对象使用时,原始对象将被替换成补丁对象。这意味着不在补丁中的字段这次不会出现在结果中;而且这个对象的数组将与补丁中的数组完全相同,不会发生合并操作。
要为对象声明这个指令,你需要为该对象添加一个字段 $patch,值为 replace。例如,下面的补丁将用一个只包含 runAsNonRoot
字段的 securityContext
来替换 nginx 容器的 securityContext:
{
"spec":{
"template":{
"spec":{
"containers":[{
"name":"nginx",
"securityContext": {
"$patch": "replace",
"runAsNonRoot": false
}}]}}}}
当与数组一起使用时,原始数组将被补丁数组所取代。
要为数组声明这个指令,你需要为这个数组添加一个对象,其中有一个值为替换的单字段 $patch。例如,下面的补丁将把容器 nginx 的环境变量设置为单一变量key1,而不考虑之前定义的变量数量。
{
"spec":{
"template":{
"spec":{
"containers":[{
"name":"nginx",
"env": [
{ "$patch": "replace"},
{ "name": "key1", "value": "value1" }
]
}]}}}}
删除指令
你可以对对象或数组中的对象元素使用删除指令。对对象使用这个指令,就像把这个对象的值声明为空。例如,这个补丁将从容器 nginx 中删除字段securityContext:
{
"spec":{
"template":{
"spec":{
"containers":[{
"name":"nginx",
"securityContext": {
"$patch": "delete"
}
}]}}}}
要从列表中删除一个元素,你需要将指令添加到你要删除的元素中。你需要指出键字段(为 Merge 补丁策略指出的键)。例如,你可以用这个补丁来删除名为key1的环境变量:
{
"spec":{
"template":{
"spec":{
"containers":[
{
"name":"nginx",
"env": [{
"name": "key1",
"$patch": "delete"
}]
}]}}}}
deleteFromPrimitiveList指令
delete 指令只适用于从数组中删除对象。你可以使用 deleteFromPrimitiveList 指令,通过在包含数组的字段名前加上 $deleteFromPrimitiveList/
来删除数组中的原始元素。例如,要从 nginx 容器的 args 列表中删除 -debug
参数,可以使用以下补丁:
{
"spec":{
"template":{
"spec":{
"containers":[
{
"name":"nginx",
"$deleteFromPrimitiveList/args": [
"--debug"
]
}]}}}}
注意
这个指令对于 Kubernetes 1.24 和更早的版本不能正常工作,因为它只保留指定的值,而不是只保留其他值。
setElementOrder指令
setElementOrder 指令可以用来将数组中的元素排序成不同的顺序,方法是在包含要排序的数组的字段名前加上 $setElementOrder/
。例如,要重新排序部署的 initContainers,可以使用这个补丁:
{
"spec":{
"template":{
"spec":{
"$setElementOrder/initContainers":[
{ "name": "init2"},
{ "name": "init1"}
]}}}}
应用服务器端的资源
正如你在前面的章节中所看到的,你可以更新资源,但在发生冲突的情况下,你需要编写特定的指令来表明如何解决冲突。Kubernetes API 在 Kubernetes 1.16中引入了服务器端应用的Beta功能,从 Kubernetes 1.22开始,它已经成为一个稳定的功能。
服务器端 Apply 操作和 Update 命令一样,不同的是,即使资源在集群中不存在,也可以使用这个命令,而且执行命令时必须提供一个字段管理器。
服务器端应用操作可以使用 PATCH 请求来执行,其内容类型为 application/apply-patch+yaml
,并在路径中指定名称(和命名空间的资源),在请求的正文中指定补丁信息。
Kubernetes API 将在资源的一个专用字段(.metadata.managedFields
)中保存该资源中完成的应用操作列表。
对于每个保存的 Apply 操作,它所设置的每个字段都被操作期间提供的字段管理器标记为 “owned”。如果 Apply 操作更新了一个由另一个字段管理器拥有的字段,因为之前的 Apply 操作覆盖了这个字段,就会产生冲突。
可以强制执行 Apply 操作,以便用新值建立冲突的字段,并将所有权转移给新的字段管理器。所有权被设置在对象或原始元素中,以及数组的元素中。
例如,字段管理器可以为容器定义一组环境变量,而另一个字段管理器可以为Pod的同一个容器定义一组不同的环境变量。每个字段管理器拥有它的环境变量。如果第一个字段管理器通过删除它的一些环境变量来运行一个新的Apply操作,这些环境变量将从总列表中删除,但另一个字段管理器的环境变量不会受到影响。
为了说明这个例子,你可以为一个部署创建以下YAML清单,该清单为Pod的容器定义了三个环境变量:
# deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
env:
- name: key1
value: value1
- name: key2
value: value2
- name: key3
value: value3
然后,您可以通过使用服务器端应用操作来应用这个清单(注意内容类型头被设置为 application/apply-patch+yaml
,查询参数 fieldManager 被设置为manager1):
$ curl -X PATCH
$HOST/apis/apps/v1/namespaces/project1/deployments/nginx?fieldManager=manager1
-H
"Content-Type: application/apply-patch+yaml"
--data-binary @deploy.yaml
这条命令将创建部署。你可以检查用这个命令创建的部署资源的 .metadata.managedFields
(jq被用来获取缩进的JSON形式):
$ kubectl get deploy nginx -o jsonpath={.metadata.managedFields} | jq
[{
"apiVersion": "apps/v1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:spec": {
"f:template": {
"f:spec": {
"f:containers": {
"k:{\"name\":\"nginx\"}": {
".": {}, ➊
"f:image": {}, ➋
"f:name": {} ➌
"f:env": {
"k:{\"name\":\"key1\"}": {
".": {}, "f:name": {}, "f:value": {} ➍
},
"k:{\"name\":\"key2\"}": {
".": {}, "f:name": {}, "f:value": {} ➎
},
"k:{\"name\":\"key3\"}": {
".": {}, "f:name": {}, "f:value": {} ➏
}}}}}}}},
"manager": "manager1", ➐
"operation": "Apply", ➑
"time": "2022-07-14T16:46:48Z"
},
{
"apiVersion": "apps/v1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:status": {
"f:availableReplicas": {},
"f:observedGeneration": {},
"f:readyReplicas": {},
"f:replicas": {},
"f:updatedReplicas": {}
}
},
"manager": "kube-controller-manager", ➒
"operation": "Update", ➓
"subresource": "status",
"time": "2022-07-14T16:46:52Z"
}]
你可以在managedFields中看到,你的操作被保存为Apply操作,➑,由经理manager1➐; 这个管理器拥有名称为nginx ➊的容器元素、字段image ➋、name ➌,以及名称为key1 ➍、key2 ➎、key3 ➏的env元素。
管理者是用于编辑资源的任何程序;例如,kubectl在使用编辑或应用命令时,或管理这些资源的控制器或操作员。你还可以看到,第二个更新➓类型的操作已被保存,并由kube-controller-manager ➒拥有,因为在你创建部署资源时,部署控制器在其状态中设置了一些值。
现在,第二个管理器,manager2,想更新环境变量key2。它可以通过创建以下内容并运行该命令(注意强制查询参数被设置为真)来实现:
TBD.。。。。。
观察资源
Kubernetes API允许你观察资源。这意味着你的请求不会立即终止,而是一个长期运行的请求,它将发送一个JSON流作为响应,当被监视的资源发生变化时将JSON对象添加到流中。JSON流是一系列由新行分隔的JSON对象–例如:
{ "type": "ADDED", "object": ... }
{ "type": "DELETED", "object": ... }
观察资源的请求与用于列出资源的请求一样,只是将 watch 参数作为查询参数添加。例如,要观察 project1 命名空间的pod,你可以发送这个请求:
curl "$HOST/api/v1/namespaces/project1/pods?watch=true"
在 Kubernetes 术语中,流的每个JSON对象被称为 Watch Event ,它将包含两个字段: type 和 object。类型的值可以是ADDED、MODIFIED、DELETED、BOOKMARK 或 ERROR。对于每一种类型,其对象如表2-2所描述。
表2-2每种类型的观察事件的对象描述
Type value | Object description |
---|---|
ADDEDMODIFIED | 资源的新状态,使用其 kind(例如,Pod)。 |
DELETED | 该资源在被删除前的状态,使用其 kind(例如,Pod)。 |
BOOKMARK | resource version,使用它的 kind(例如,Pod),只设置 resourceVersion 字段。在本章的后面,你会看到在哪些情况下会使用这种类型。 |
ERROR | 描述错误的对象。 |
当这个请求被执行时,你会立即得到一系列的 ADDED JSON 对象,描述在请求时集群中存在的所有资源;最终,当集群中的资源被创建、修改或删除时,会有其他事件发生。这相当于运行 kubectl 命令,不同的是不给类型字段,直接给对象的内容:
kubectl get pods --namespace project1 --watch -o json
在列出资源后进行观察
你可以先运行 list 请求来找到现有资源的列表,然后运行 watch 请求来获得这些资源的修改情况,而不是把请求时存在的资源作为 watch 响应的一部分。这样做有一个风险,即在 list 请求和 watch 请求开始之间可能会发生一些修改,而你并没有得到关于这些修改的通知。
对于这种情况,你可以使用列表请求返回的 resourceVersion 值来指示你想在哪个时间点开始你的观察请求。(注意:你需要将 resourceVersion 获取到 List 结构体中,而不是从一个子项中获取。)
作为例子,你可以首先通过使用命令获得 pod 的列表:
curl "$HOST/api/v1/pods"
{
"kind": "PodList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "2433789"
},
"items": [ ... ]
}
作为对这第一个请求的响应,你会得到资源版本和在请求时存在于项目领域的资源列表。然后,你可以通过指定这个资源版本来执行 watch 请求:
curl "$HOST/api/v1/namespaces/default/pods?watch=true&resourceVersion=2433789"
因此,你不会立即在响应体中收到描述集群中存在的资源的数据;只有当一些资源被修改、添加或删除时,你才会收到数据。
重启观察请求
观察请求可能会被中断,你可能想在这个过程中从最后收到的修改(或之前的修改)重新启动它。
为此,观察响应中的 DELETE、ADD 或 MODIFIED JSON 对象的每个资源部分都包含 resourceVersion,你可以用它来执行一个新的观察请求,就在指定的修改之后开始。例如,你可以启动一个 watch 请求,在几次修改后就中断了:
$ curl "$HOST/api/v1/namespaces/default/pods?watch=true"
{"type":"ADDED","object":{
"kind":"Pod","apiVersion":"v1","metadata":{
"resourceVersion":"2435623", ...}, ...}}
{"type":"ADDED","object":{
"kind":"Pod","apiVersion":"v1","metadata":{
"resourceVersion":"2354893", ...}, ...}}
{"type":"MODIFIED","object":{
"kind":"Pod","apiVersion":"v1","metadata":{
"resourceVersion":"2436655", ...}, ...}}
{"type":"DELETED","object":{
"kind":"Pod","apiVersion":"v1","metadata":{
"resourceVersion":"2436677", ...}, ...}}
然后,你可以从上一次请求的任何时间开始,或从最近一次修改后开始,重新启动 watch 请求:
$ curl "$HOST/api/v1/namespaces/default/pods?watch=true&resourceVersion=2436677"
或者在之前的修改之后开始。这样,你将再次收到最新的修改:
$ curl "$HOST/api/v1/namespaces/default/pods?watch=true&resourceVersion=2436655"
{"type":"DELETED","object":{
"kind":"Pod","apiVersion":"v1","metadata":{
"resourceVersion":"2436677", ...}, ...}}
允许书签有效地重新启动 watch 请求
正如前一节所示,通过使用标签或字段选择器,可以在资源的子集上执行 watch 会话。例如,这个请求将观察具有特定标签 mylabel 等于 foo 的 pod:
$ curl "$HOST/api/v1/namespaces/project1/pods?labelSelector=mylabel==foo&watch=true"
通过这个请求,你将只获得与你的选择器相匹配的pod的事件,而不是与你的请求不匹配的同一命名空间的其他pod的事件。
当重新启动观察请求时,你将能够使用与选择器相匹配的pod的资源版本;然而,在这个请求之后,其他pod上的许多事件可能已经发生。API服务器将不得不在你在这个旧的资源版本上重启观察时,执行对在此期间创建的所有pod的事件的过滤。
同样,由于API服务器在有限的时间内缓存这些事件,与最近的资源版本相比,旧的资源版本不再可用的风险更大。
为此,你可以使用 allowWatchBookmarks 参数,要求API服务器定期发送包含最新资源版本的 BOOKMARK 事件;这些可能是与你的选择分开的资源版本。
BOOKMARK 事件可能包含一个你请求的种类的对象(例如,如果你在观察pod,则是Pod种类),但这只包括 resourceVersion 字段。
{"type":"BOOKMARK",
"object":{
"kind":"Pod",
"apiVersion":"v1",
"metadata":{
"resourceVersion":"2525115",
"creationTimestamp":null
},
"spec":{
"containers":null
},
"status":{}
}
}
为了说明问题,这里有一个小实验。首先创建两(2)个pod,用选择器观察pod,只匹配第一个pod。你会得到一个匹配的pod的ADDED事件,而且,过了一段时间,你应该得到一个BOOKMARK事件(但不保证)。如果在此期间,该命名空间的pod上没有任何活动,resourceVersion 应该是相同的。
$ kubectl run nginx1 --image nginx --labels mylabel=foo
$ kubectl run nginx2 --image nginx --labels mylabel=bar
$ curl "$HOST/api/v1/namespaces/default/pods?
labelSelector=mylabel==foo&
watch=true&
allowWatchBookmarks=true"
{"type":"ADDED","object":{
"kind":"Pod","apiVersion":"v1","metadata":{
"name":"nginx1","resourceVersion":"2520070", ...}, ...}}
{"type":"BOOKMARK","object":{
"kind":"Pod","apiVersion":"v1","metadata":{
"resourceVersion":"2520070", ...}, ...}}
在另一个终端,让我们用命令删除不匹配的pod,nginx2:
$ kubectl delete pods nginx2
你不应该收到这个变化的任何事件,因为 pod 不匹配请求选择器,但过了一会儿,你应该收到一个新的 BOOKMARK事件–这次是一个新的资源版本:
{"type":"BOOKMARK","object":{
"kind":"Pod","apiVersion":"v1","metadata":{
"resourceVersion":"2532566", ...}, ...}}
在这一点上,你可以从你的第一个终端停止观察请求。
接下来,你可以对命名空间的pod做一些修改–例如,删除 nginx1 pod,重新创建 nginx2 pod:
$ kubectl delete pods nginx1
$ kubectl run nginx2 --image nginx --labels mylabel=bar
现在,你可以使用资源版本 2532566 来重新启动观察请求,当请求停止时,你可以重新启动它:
curl "$HOST/api/v1/namespaces/default/pods?
labelSelector=mylabel==foo&
watch=true&
allowWatchBookmarks=true&
resourceVersion=2532566"
结果,你可以看到,当你删除这个pod的时候,你得到了 nginx1 的修改和删除的事件。你没有丢失任何事件,而且你使用了一个最新的资源版本,这对API服务器来说更有效率。
将结果分页
当你执行 list 请求时,结果有可能包含很多元素。在这种情况下,最好是通过发出几个请求来分页处理结果,每个响应将发送有限数量的元素。
对于这种情况,要使用 limit 和 continue 查询参数。第一个列表请求需要指定 limit 参数,以表明要返回的元素的最大数量。响应将在List结构的元数据中包含一个 continue 字段,该字段包含一个不透明的标记,以便在下一次请求中使用,以获得下一整块元素。
$ curl "$HOST/api/v1/pods?limit=1"
{
"kind": "PodList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "2931316",
"continue": <continue_token_1>,
"remainingItemCount": 10
},
"items": [{ ... }]
}
$ curl "$HOST/api/v1/pods?limit=1&continue=<continue_token_1>"
{
"kind": "PodList",
"apiVersion": "v1",
"metadata": {
"resourceVersion": "2931316",
"continue": <continue_token_2>,
"remainingItemCount": 9
},
"items": [{ ... }]
}
注意,你不需要对每个块使用相同的限制值。你可以把第一个请求的限制值定为1,第二个请求的限制值定为4,第三个请求的限制值定为6。
完整列表的一致性
请注意,两个响应中的 List 结构中的 resourceVersion 是相同的(即例子中的 “resourceVersion”: “2931316”)。当你运行第一个请求时,完整的响应被缓存在服务器上,你可以保证在接下来的几块中得到一个一致的结果,与你获得以下请求的时间以及在此期间对资源的修改无关。在这段时间内创建、修改或删除的资源不会影响下一个块的结果。
然而,有可能在你运行所有的请求之前,缓存就已经过期了。在这种情况下,你会收到一个代码为 410 的错误响应和一个新的继续值。因此,你有两个选择:
- 启动新的 List 请求,不使用 continue 参数,从头开始重新启动整个 List 会话。
- 用返回的 continue 值发起新的请求,但是以不一致的方式–也就是说,自第一个 chunk 被返回后,添加、修改或删除的资源将影响响应。
检测最后一个 chunk
你可以从响应的元数据中看到,remainItemCount 表示完成整个响应所需的剩余元素数量。然而,请注意,这个信息只适用于没有选择器的请求(无论是标签还是字段选择器)。
当运行没有选择器的分页列表请求时,服务器可以知道完整列表中的元素数量,并且能够在每次请求后指出剩余元素的数量。当发送完整列表的最后一个元素时,服务器也能够通过在列表结构的元数据中回复一个空的 continue 字段来表明这是最后一个块。
当使用选择器运行分页的列表请求时,服务器无法预先知道完整列表中的元素数量。由于这个原因,它不会在请求后发送剩余元素的数量,而且即使碰巧下一个块是空的,它也会发送一个continue 非空值。
你需要检查返回的列表是空的还是包含的元素少于 limit 字段中请求的元素,这样你就可以检测到最后一个chunk。
获取各种格式的结果
Kubernetes API可以以各种格式返回数据。你可以通过在HTTP请求中指定 Accept 头来询问你希望收到哪种格式。
以表格形式获取结果
kubectl客户端(和其他客户端)以表格的形式显示资源的列表。当运行 list 请求时,你可以要求API服务器给你必要的信息,通过使用 Accept 头来表示这种特定的格式来建立这种表格:
$ curl $HOST/api/v1/pods
-H 'Accept: application/json;as=Table;g=meta.k8s.io;v=v1'
{
"kind": "Table",
"apiVersion": "meta.k8s.io/v1",
"metadata": {
"resourceVersion": "2995797"
},
"columnDefinitions": [ { ... }, { ... }, { ... } ],
"rows": [ { ... }, { ... } ]
}
这有助于客户端以表格格式显示任何资源的信息,包括自定义资源,因为自定义资源定义将包含关于在哪一列显示资源的哪个字段的信息。
对于请求的任何资源,响应的 kind 将总是 Table。第一个字段 columnDefinitions
描述了表格的每一列,第二个字段 rows
给出了结果中每个资源的列值。
列的定义
列的定义包括 name, type, format, description 和 priority 字段。name 是指该列的标题。type 是该列的 OpenAPI 类型定义(例如,整数、数字、字符串或布尔值)。
可选的 format 是列的类型的 OpenAPI 修改器,给出更多的格式化信息。整数类型的格式是 int32 和 int64,数字类型的格式是 float 和 double,而字符串类型的格式是 byte、binary、data、date-time、password 和 name。name 格式值不是 OpenAPI 规范的一部分,是 Kubernetes API 特有的。它向客户端指出包含资源名称的主列。
priority 字段是一个整数,表示一个列相对于其他列的重要性。当空间有限时,数值较高的列可能会被省略。
行数据
行包括 cells、conditions 和 object 字段。
cells 字段是一个与 columnDefinitions
数组长度相同的数组,包含当前行中显示的资源值。数组中每个元素的JSON类型和可选格式是由相应的列定义的类型和格式推断出来的。
conditions 字段给出了显示该行的具体属性。从Kubernetes 1.23开始,唯一定义的值是 “已完成”,表示该行显示的资源已经运行完成,可以给予较低的视觉优先级。
默认情况下,object 字段包含该列中显示的资源的元数据。你可以在列表请求中添加一个 includeObject 查询参数,以便不需要关于对象的信息(?includeObject=None),或者需要完整的对象(?includeObject=Object)。这个查询参数的默认值是Metadata,它只需要资源的元数据。例如,使用下面的方法来返回没有对象的信息作为行数据的一部分:
$ curl $HOST/api/v1/pods?includeObject=None
-H 'Accept: application/json;as=Table;g=meta.k8s.io;v=v1'
使用YAML格式
在前面的创建资源部分,你看到可以使用 YAML 格式来描述使用 Content-Type: application/yaml
头创建的资源。如果你没有指定这个头,你将需要用JSON格式来描述资源。
也可以使用 Accept: application/yaml
头来获得YAML格式的请求响应。这对Get和List请求有效,但也可以用于创建或更新资源的请求,返回它们的新值。例如,要获得YAML格式的所有pod的列表,请使用这个:
$ curl $HOST/api/v1/pods -H 'Accept: application/yaml'
kind: PodList
metadata:
resourceVersion: "3009983"
items:
[...]
或者,要创建一个新的Pod并获得YAML格式的创建的Pod,请使用以下内容:
$ curl $HOST/api/v1/namespaces/default/pods
-H "Content-Type: application/yaml"
-H 'Accept: application/yaml'
--data-binary @pod.yaml
注意:不可能以 YAML 格式获得 Watch 请求的结果。
使用Protobuf格式
Protobuf 格式也可用于向 API 服务器发送数据或从其接收数据。为此,你需要在 Content-Type 或 Accept 中使用 application/vnd.kubernetes.protobuf
类型。
Kubernetes团队不鼓励在 Kubernetes 控制平面之外使用 Protobuf 格式,因为他们不能保证 Protobuf 消息会像JSON消息一样稳定。
如果你决定使用Protobuf格式,你需要知道API服务器不会交换纯粹的 Protobuf 数据,但它会给它添加一个头来检查 Kubernetes 版本之间的兼容性。
apimachinery 库包含Go代码,帮助开发者将数据序列化为各种格式,包括 Protobuf。第5章描述了如何使用这个库。
总结
本章讨论了如何运行 kubectl 以帮助理解其下执行的HTTP请求。然后,它展示了如何使用各种HTTP操作来创建、更新、应用、删除、获取、列出和观察资源的细节。最后,本章介绍了如何以几种格式获得这些操作的结果: JSON、YAML和Protobuf,或以表格的形式。
5.3 - [第三章]在Go中使用API资源
本书的前两章已经描述了Kubernetes API是如何设计的,以及如何使用HTTP请求访问它。具体来说,你已经看到由API管理的资源被组织成Group-Version-Resources,而客户端和API服务器之间交换的对象被Kubernetes API定义为Kinds。本章还显示,在交换过程中,这些数据可以用JSON、YAML或Protobuf编码,这取决于客户端设置的HTTP头。
在接下来的章节中,你将看到如何使用Go语言访问这个API。与Kubernetes API合作需要的两个重要Go库是apimachinery和api。
apimachinery 是一个通用库,它负责Go结构体和以 JSON(或YAML或Protobuf)格式编写的对象之间的数据序列化。这使得客户和API服务器的开发者有可能使用Go结构体编写数据,并在HTTP交换过程中透明地使用这些资源的JSON(或YAML或Protobuf)。
apimachinery 是通用的,因为它不包括任何Kubernetes API资源定义。它使Kubernetes API可以扩展,并使 apimachinery 可用于任何其他使用相同机制的API,即Kinds 和 Group-Version-Resources。
API 库则是 go 结构体的集合,需要在 go 中与Kubernetes API定义的资源一起工作。
API库的源码和导入
API库的源代码可以从 https://github.com/kubernetes/api 。如果你想为这个库做贡献,请注意,源代码不是从这个库管理的,而是从中心库,https://github.com/kubernetes/kubernetes,在 staging/src/k8s.io/api
目录下,源代码从 kubernetes 库同步到 api 库中。
要把 API 库的包导入Go源代码,你需要使用 k8s.io/api
的前缀–例如:
import "k8s.io/api/core/v1"
进入API库的包遵循API的 Group-Version-Resource
结构。当你想使用某个资源的结构体时,你需要通过这种模式导入与该资源的组和版本相关的包:
import "k8s.io/api/<group>/<version>"
包的内容
让我们来检查包中包含的文件–例如,k8s.io/api/apps/v1
包。
types.go
这个文件可以被认为是包的主文件,因为它定义了所有的 Kind 结构体和其他相关的子结构体。它还定义了这些结构体中的枚举字段的所有类型和常量。举个例子,考虑一下 Deployment Kind;Deployment 结构体首先被定义如下:
type Deployment struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec DeploymentSpec
Status DeploymentStatus
}
然后,相关的子结构体,DeploymentSpec 和 DeploymentStatus,都是用这个定义的:
type DeploymentSpec struct {
Replicas *int32
Selector *metav1.LabelSelector
Template v1.PodTemplateSpec
Strategy DeploymentStrategy
[...]
}
type DeploymentStatus struct {
ObservedGeneration int64
Replicas int32
[...]
Conditions []DeploymentCondition
}
然后,以同样的方式继续处理在前一个结构体中作为类型使用的每一个结构体。
在 DeploymentCondition 结构体中使用的 DeploymentConditionType 类型(这里没列出),以及这个枚举的可能值都被定义:
type DeploymentConditionType string
const (
DeploymentAvailable DeploymentConditionType = "Available"
DeploymentProgressing DeploymentConditionType = "Progressing"
DeploymentReplicaFailure DeploymentConditionType = "ReplicaFailure"
)
你可以看到,每个 Kind 都嵌入了两个结构体:metav1.TypeMeta 和 metav1.ObjectMeta。它们是强制性的,以便被 API Machinery 识别。TypeMeta 结构体包含关于 Kind 的 GVK 的信息,而 ObjectMeta 包含 Kind 的元数据,比如它的 name 。
register.go
这个文件定义了与这个包相关的分组(group)和版本(version),以及这个分组和版本中的 Kinds 列表。当你需要从这个分组-版本中指定一个资源的分组和版本时,可以使用公共变量 SchemeGroupVersion。
它还声明了一个函数 AddToScheme,它可以用来将分组、版本和 Kinds 添加到 Scheme 中。Scheme 是 API Machinery 中的一个抽象概念,用于在Go结构体和Group-Version-Kinds 之间建立映射。这将在第五章 “API Machinery” 中进一步讨论。
doc.go
这个文件和下面的文件包含高级信息,你不需要理解这些信息就可以开始用 Go 编写你的第一个 Kubernetes 资源,但它们会帮助你理解如何在接下来的章节中用自定义资源定义声明新资源。
doc.go 文件包含以下生成文件的说明:
// +k8s:deepcopy-gen=package
// +k8s:protobuf-gen=package
// +k8s:openapi-gen=true
第一条指令被 deepcopy-gen 生成器用来生成 zz_generated.deepcopy.go 文件。第二条指令被 go-to-protobuf 生成器用来生成这些文件:generated.pb.go 和 generated.proto。第三条指令被 genswaggertypedocs 生成器用来生成 type_swagger_doc_generated.go 文件。
generated.pb.go 和 generated.proto
这些文件是由 go-to-protobuf 生成器生成的。当把数据序列化为 Protobuf 格式时,它们被 API Machinery 使用。
types_swagger_doc_generated.go
这个文件是由 genswaggertypedocs 生成器生成的。它在生成 Kubernetes API 的完整 swagger 定义时使用。
zz_generated.deepcopy.go
这个文件是由 deepcopy-gen 生成器生成的。它包含了为包中定义的每个类型生成的 DeepCopyObject 方法的定义。这个方法对于结构体 实现 runtime.Object 接口是必要的,这个接口是在 API Machinery 中定义的,API Machinery 期望所有的 Kind 结构体将实现这个 runtime.Object 接口。
该文件中的接口定义如下:
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}
另一个必要的方法,GetObjectKind,被自动添加到嵌入 TypeMeta 结构体的结构体中 – 所有 Kind 结构体都是如此。TypeMeta 结构体的方法定义如下:
func (obj *TypeMeta) GetObjectKind() schema.ObjectKind {
return obj
}
core/v1中的特定内容
core/v1包除了定义核心资源的结构体外,还定义了特定类型的实用方法,当你把这些类型纳入你的代码中时,这些方法会很有用。
ObjectReference
ObjectReference 可以用来以一种独特的方式引用任何对象。该结构体定义如下:
type ObjectReference struct {
APIVersion string
Kind string
Namespace string
Name string
UID types.UID
ResourceVersion string
FieldPath string
}
为这种类型定义了三种方法:
-
SetGroupVersionKind(gvk schema.GroupVersionKind)
- 这个方法将根据作为参数传递的 GroupVersionKind 值来设置字段 APIVersion 和 Kind。 -
GroupVersionKind() schema.GroupVersionKind
- 该方法将根据 ObjectReference 的字段 APIVersion 和 Kind 返回一个 GroupVersionKind 值。 -
GetObjectKind() schema.ObjectKind
- 这个方法将把 ObjectReference 对象转换成 ObjectKind。前面的两个方法实现了这个 ObjectKind 接口。因为 ObjectReference 上的 DeepCopyObject 方法也被定义了,所以 ObjectReference 将实现 runtime.Object 接口。
ResourceList
ResourceList 类型被定义为一个map,其键是 ResourceName,值是 Quantity。这被用于各种 Kubernetes 资源,以定义资源的 limits 和 requests。
在YAML中,一个使用的例子是当你为一个 container 定义资源的 requests 和 limits 时,如下所示:
apiVersion: v1
kind: Pod
metadata:
name: mypod
spec:
containers:
- name: runtime
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
在Go中,你可以把 requests 部分写成:
requests := corev1.ResourceList{
corev1.ResourceMemory:
*resource.NewQuantity(64*1024*1024, resource.BinarySI),
corev1.ResourceCPU:
*resource.NewMilliQuantity(250, resource.DecimalSI),
}
下一章将更详细地描述如何使用 resource.Quantity 类型来定义数量。ResourceList 类型有以下方法:
-
Cpu() *resource.Quantity
- 返回 map 中 CPU 键的数量,以十进制格式(1250m等)。 -
Memory() *resource.Quantity
- 返回 map 中 Memory 键的数量,以二进制格式表示(512 Ki, 64 Mi, etc.) -
storage() *resource.Quantity
- 返回 map 中 Storage 键的数量,以二进制格式表示(512 Mi, 1 Gi等)。 -
Pods() *resource.Quantity
- 返回 map 的 Pods 键的数量,以十进制格式(1,10等)。 -
StorageEphemeral() *resource.Quantity
- 返回 map 中 StorageEphemeral 键的数量,以二进制格式表示(512 Mi, 1 Gi, etc.)
对于这些方法中的每一个,如果键没有在 map 中定义,将返回一个零值的 Quantity。
另一个方法,在内部被前面的方法使用,可以被用来获得非标准格式的数量:
Name(name ResourceName, defaultFormat resource.Format) *resource.Quantity
- 这返回 Name 键的数量,以 defaultFormat 格式。
ResourceName 类型的定义枚举值是 ResourceCPU、ResourceMemory、ResourceStorage、ResourceEphemeralStorage 和 ResourcePods 。
Taint / 污点
Taint 资源是为了应用于节点,以确保不容忍这些污点的 pod 不会被安排到这些节点。Taint 结构体定义如下:
type Taint struct {
Key string
Value string
Effect TaintEffect
TimeAdded *metav1.Time
}
TaintEffect 枚举可以得到以下值:
TaintEffectNoSchedule = "NoSchedule"
TaintEffectPreferNoSchedule = "PreferNoSchedule"
TaintEffectNoExecute = "NoExecute"
众所周知的 Taint 键,在特殊情况下被控制平面使用,在这个包中定义如下:
TaintNodeNotReady
= "node.kubernetes.io/not-ready"
TaintNodeUnreachable
= "node.kubernetes.io/unreachable"
TaintNodeUnschedulable
= "node.kubernetes.io/unschedulable"
TaintNodeMemoryPressure
= "node.kubernetes.io/memory-pressure"
TaintNodeDiskPressure
= "node.kubernetes.io/disk-pressure"
TaintNodeNetworkUnavailable
="node.kubernetes.io/network-unavailable"
TaintNodePIDPressure
= "node.kubernetes.io/pid-pressure"
TaintNodeOutOfService
= "node.kubernetes.io/out-of-service"
以下两个方法是在 Taint 上定义的:
-
MatchTaint(taintToMatch *Taint) bool
- 如果两个污点的 key 和 Effect 值相同,这个方法将返回 true。 -
ToString() string
- 这个方法将返回 Taint 的字符串表示,格式是这样的:<key>=<value>:<effect>, <key>=<value>:, <key>:<effect>, 或 <key>
.
Toleration / 宽容性
Toleration 资源旨在应用于 Pod,使其能够容忍特定节点的污点。Toleration 结构体定义如下:
type Toleration struct {
Key string
Operator TolerationOperator
Value string
Effect TaintEffect
TolerationSeconds *int64
}
TolerationOperator 枚举有以下值:
TolerationOpExists = "Exists"
TolerationOpEqual = "Equal"
TaintEffect 枚举可以有这些值:
TaintEffectNoSchedule = "NoSchedule"
TaintEffectPreferNoSchedule = "PreferNoSchedule"
TaintEffectNoExecute = "NoExecute"
以下两种方法是在 Toleration 结构体上定义的:
-
MatchToleration(tolerationToMatch *Toleration) bool
- 如果两个 Toleration 的 Key、Operator、Value 和 Effect值相同,该方法返回true。 -
ToleratesTaint(taint *Taint) bool
- 如果容忍度能容忍污点,该方法返回 true。容忍器容忍污点的规则如下:- Effect: 对于空的 Toleration Effect,所有的 Taint 效果将匹配;否则,Toleration 和 Taint Effect 必须匹配。
- Operator: 如果 TolerationOperator 是Exists,所有的 Taint 值将匹配;否则,TolerationOperator 是 Equal,Toleration 和 Taint 值必须匹配。
- Key: 对于空的 Toleration Key,TolerationOperator 必须是Exists,所有 Taint 键(有任何值)都将匹配;否则,Toleration 和 Taint 键必须匹配。
知名标签
控制平面在节点上添加标签;使用的知名键及其用法可以在 core/v1
包的 well_known_labels.go
文件中找到。以下是最著名的。
节点上运行的 kubelet 会填充这些标签:
LabelOSStable = "kubernetes.io/os"
LabelArchStable = "kubernetes.io/arch"
LabelHostname = "kubernetes.io/hostname"
当节点在云提供商上运行时,可以设置这些标签,代表(虚拟)机器的实例类型、它的区域和它的地区:
LabelInstanceTypeStable
= "node.kubernetes.io/instance-type"
LabelTopologyZone
= "topology.kubernetes.io/zone"
LabelTopologyRegion
= "topology.kubernetes.io/region"
用Go写Kubernetes资源
你需要在 Go 中编写 Kubernetes 资源,以创建或更新集群中的资源,可以使用HTTP请求,也可以更广泛地使用 client-go 库。client-go 库将在以后的章节中讨论;但现在,让我们专注于用 Go 编写资源。
要创建或更新资源,你需要为与该资源相关的 Kind 创建结构。例如,要创建 Deployment ,你需要创建 Deployment kind;为此,要初始化 Deployment 结构体,它定义在 API 库的 apps/v1
包中。
导入包
在使用这个结构体之前,需要导入定义这个结构体的包。正如本章开头所见,包名的模式是 k8s.io/api/<group>/<version>
。路径的最后部分是一个版本号,但你不应该把它和 Go 模块的版本号混淆。
区别在于,当你导入一个 Go 模块的特定版本时(例如 k8s.io/klog/v2
),你将使用版本前的部分作为前缀来访问包的符号,而不需要定义任何别名。原因是在库中,v2目录并不存在,而是代表一个分支名称,进入包的文件以 package klog一行开始,而不是 package v2。
相反,在使用 Kubernetes API 库时,版本号是其中一个包的真实名称,进入这个包的文件确实以 package v1 开始。
如果你不为导入定义别名,你将不得不使用版本名来使用这个包的符号。但是,在阅读代码时,单单是版本号是没有意义的,如果你从同一个文件中包含了几个包,最后会出现几个 v1 包名,这是不可能的。
惯例是用组名定义一个别名,或者,如果你想和同一个组的几个版本一起工作,或者你想让别名更清楚地指向一个API group/verson,你可以用 group/verson 创建别名:
import (
corev1 "k8s.io/api/core/v1"
appsv1 "k8s.io/api/apps/v1"
)
现在你可以实例化一个 Deployment 结构体:
myDep := appsv1.Deployment{}
为了编译程序,将需要获取该库。为此,请使用:
$ go get k8s.io/api@latest
或者,如果你想使用特定版本的 Kubernetes API(例如,Kubernetes 1.23),请使用:
$ go get k8s.io/api@v0.23
所有与 Kinds 相关的结构体首先嵌入两个通用结构体: TypeMeta 和 ObjectMeta。这两个结构体都在 API machinery 的 /pkg/apis/meta/v1
包中声明。
TypeMeta字段
TypeMeta 结构体定义如下:
type TypeMeta struct {
Kind string
APIVersion string
}
你一般不需要自己为这些字段设置值,因为 API machinery 通过维护 Scheme–即 Group-Version-Kinds
和 Go 结构体之间的映射,从结构体的类型推断出这些值。请注意,APIVersion 值是将 Group-Version 写成一个包含 <group>/<version>
的单一字段的另一种方式(或者对于传统的核心组,只有v1)。
ObjectMeta字段
ObjectMeta 结构体定义如下(已废弃的字段和内部字段已被删除):
Type ObjectMeta {
Name string
GenerateName string
Namespace string
UID types.UID
ResourceVersion string
Generation int64
Labels map[string]string
Annotations map[string]string
OwnerReferences []OwnerReference
[...]
}
API machinery 的 /pkg/apis/meta/v1
包为这个结构体的字段定义了 Getters 和 Setters。由于 ObjectMeta 被嵌入到资源结构体中,你可以在资源对象本身中使用这些方法。
名称
这个结构体中最重要的信息是资源的名称。你可以使用 Name 字段来指定资源的确切名称,或者使用 GenerateName 字段来请求 Kubernetes API 为你选择一个独特的名称;它是通过向 GenerateName 值添加一个后缀来建立的,以使其独一无二。
你可以在资源对象上使用 GetName() string 和 SetName(name string) 方法来访问其嵌入的 ObjectMeta 的 Name 字段,例如:
configmap := corev1.ConfigMap{}
configmap.SetName("config")
命名空间
带命名空间的资源需要被放置到一个特定的命名空间。你可能认为你需要在命名空间字段中指出这个命名空间,但是,当你创建或更新一个资源时,你将定义放置该资源的命名空间,作为请求路径的一部分。第2章已经表明,在 project1 命名空间中创建 pod 的请求是:
$ curl $HOST/api/v1/namespaces/project1/pods
如果你在 Pod 结构中指定了不同于 project1 的命名空间,你会得到错误: “the namespace of the provided object does not match the namespace sent on the request.”。由于这些原因,在创建资源时,没有必要设置 Namespace 字段。
UID、ResourceVersion 和 Generation
UID 是集群中过去、现在和未来资源的唯一标识符。它由控制平面设置,在资源的生命周期内从不更新。必须用它来引用一个资源,而不是它的 kind, name, 和 namespace,后者可以描述不同时期的各种资源。
ResourceVersion 是一个不透明的值,代表资源的版本。每当资源被更新时,ResourceVersion 就会改变。
这个 ResourceVersion 用于优化并发控制:如果从 Kubernetes API 获得资源的特定版本,修改它然后把它送回 API 更新;API会检查你送回的资源的ResourceVersion 是最后一个。如果在此期间,另一个进程修改了该资源,ResourceVersion 将被修改,你的请求将被拒绝;在这种情况下,你有责任读取新的版本并再次更新它。这与悲观主义的并发控制不同,在悲观主义的并发控制中,你需要在读取资源之前获得一个锁,并在更新之后释放它。
Generation 是一个序列号,可以被资源的控制器用来表示所需状态(Spec)的版本。只有当资源的 Spec 部分被更新时,它才会被更新,而不是其他部分(标签、注解、状态)。控制器通常使用 Status 部分的 ObservedGeneration 字段来指示哪一代被最后处理并反映在状态中。
标签和注解
标签(Labels)和注解(Annotations)在Go中被定义为 map,其中的键和值都是字符串。
即使标签和注解有非常不同的用途,它们也可以用同样的方式来填充。我们将在本节讨论如何填充标签字段,但这也适用于注释。
如果你知道你想添加为标签的键和值,写标签字段的最简单方法是直接写 map,例如:
mylabels := map[string]string{
"app.kubernetes.io/component": "my-component",
"app.kubernetes.io/name": "a-name",
}
也可以在现有的 map 上添加标签,比如说:
mylabels["app.kubernetes.io/part-of"] = "my-app"
如果你需要从动态值建立标签字段,API Machinery 中提供的标签包提供了一个 Set 类型,可能会有所帮助。
import "k8s.io/apimachinery/pkg/labels"
mylabels := labels.Set{}
提供函数和方法来操作这种类型:
-
函数
ConvertSelectorToLabelsMap
将选择器字符串转换为Set。 -
函数
Conflicts
检查两个 Set 是否有冲突的标签。冲突的标签是指具有相同键但不同值的标签。 -
函数
Merge
将把两个 Set 合并成一个 Set。如果两个集合之间有冲突的标签,第二个集合中的标签将被用于产生的集合中 -
函数
Equals
检查这两个 Set 是否有相同的键和值。 -
方法
Has
表明 Set 是否包含一个键。 -
方法
Get
返回 Set 中给定键的值,如果 Set 中没有定义该键,则返回空字符串。
你可以用值来实例化 Set,你可以用单个的值来填充它,就像你用 map 做的一样:
mySet := labels.Set{
"app.kubernetes.io/component": "my-component",
"app.kubernetes.io/name": "a-name",
}
mySet["app.kubernetes.io/part-of"] = "my-app"
你可以使用以下方法来访问一个资源的标签和注释:
- GetLabels() map[string]string
- SetLabels(labels map[string]string)
- GetAnnotations() map[string]string
- SetAnnotations(annotations map[string]string)
OwnerReferences
当你想表明这个资源被另一个资源所拥有,并且你希望这个资源在所有者不存在时被垃圾收集器收集时,就会在 Kubernetes 资源上设置 OwnerReference。
这在开发 controller 和 operator 时被广泛使用。controller 或operator 创建一些资源来实现另一个资源所描述的规范,它将一个 OwnerReference 放入所创建的资源中,指向给出规范的资源。
例如,Deployment controller 根据在 Deployment 资源中发现的规格创建 ReplicaSet 资源。当你删除 Deployment 时,相关的 ReplicaSet 资源会被垃圾收集器删除,而无需 controller 的任何干预。
OwnerReference 类型定义如下:
type OwnerReference struct {
APIVersion string
Kind string
Name string
UID types.UID
Controller *bool
BlockOwnerDeletion *bool
}
要知道要引用的对象的 UID,你需要从 Kubernetes API 中使用 get(或list)请求获得该对象。
设置 APIVersion 和 Kind
使用 Client-go 库(第4章展示了如何使用它),APIVersion 和 Kind 将不会被设置;你需要在复制对象之前在被引用对象上设置它们,或者直接在 ownerReference 中设置:
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// Get the object to reference
pod, err := clientset.CoreV1().Pods(myns).
Get(context.TODO(), mypodname, metav1.GetOptions{})
If err != nil {
return err
}
// Solution 1: set the APIVersion and Kind of the Pod
// then copy all information from the pod
pod.SetGroupVersionKind(
corev1.SchemeGroupVersion.WithKind("Pod"),
)
ownerRef := metav1.OwnerReference{
APIVersion: pod.APIVersion,
Kind: pod.Kind,
Name: pod.GetName(),
UID: pod.GetUID(),
}
// Solution 2: Copy name and uid from pod
// then set APIVersion and Kind on the OwnerReference
ownerRef := metav1.OwnerReference{
Name: pod.GetName(),
UID: pod.GetUID(),
}
ownerRef.APIVersion, ownerRef.Kind =
corev1.SchemeGroupVersion.WithKind("Pod").
ToAPIVersionAndKind()
APIVersion包含与 Group 和 Version 相同的信息。你可以从 schema.GroupVersion 类型的 SchemeGroupVersion 变量中获取信息,该变量定义在与资源相关的API库包中(这里 k8s.io/api/core/v1
为 Pod 资源)。然后你可以添加 Kind 来创建一个 schema.GroupVersionKind
。
对于第一个解决方案,你可以在引用的对象上使用 SetGroupVersionKind 方法,从 GroupVersionKind 中设置 APIVersion 和 Kind。对于第二个解决方案,使用 GroupVersionKind 上的 ToAPIVersionAndKind 方法来获取相应的 APIVersion 和 Kind 值,然后再把它们移到 OwnerReference 中。
第5章描述了 API Machinery 以及与 Group、Version 和 Kinds 相关的所有类型和方法。OwnerReference 结构体还包含两个可选的布尔字段: Controller 和 BlockOwnerDeletion。
设置Controller
Controller 字段表示被引用的对象是否是管理 Controller(或 operator)。Controller 或 operator 必须在拥有的资源上将此值设置为 “true”,以表明它正在管理这个拥有的资源。
Kubernetes API 将拒绝在同一资源上添加两个 Controller 设置为 true 的 OwnerReferences。这样一来,一个资源就不可能由两个不同的 Controller 管理。
请注意,这与被各种资源所拥有是不同的。一个资源可以有不同的所有者;在这种情况下,当所有的所有者都被删除时,该资源将被删除,与哪个所有者是控制器无关,如果有的话。
Controller 的这种唯一性对于那些可以 “采用” 资源的 Controller 来说是很有用的。例如,ReplicaSet 可以采用与 ReplicaSet 的选择器相匹配的现有Pod,但前提是该 Pod 还没有被另一个 ReplicaSet 或另一个 Controller 控制。
这个字段的值是一个指向布尔值的指针。你可以声明一个布尔值,并在设置控制器字段时影响其地址,或者使用 Kubernetes Utils Library 的 BoolPtr 函数:
// Solution 1: declare a value and use its address
controller := true
ownerRef.Controller = &controller
// Solution 2: use the BoolPtr function
import (
"k8s.io/utils/pointer"
)
ownerRef.Controller = pointer.BoolPtr(true)
设置BlockOwnerDeletion
OwnerReference 对于 Controller 或其他进程来说是非常有用的,因为他们不需要关心自有资源的删除问题:当所有者被删除时,自有资源将被 Kubernetes 垃圾收集器删除。
这种行为是可配置的。在资源上使用删除操作时,你可以使用传播策略(Propagation Policy )选项:
- Orphan:向 Kubernetes API 表示将拥有的资源变成孤儿,因此它们不会被垃圾收集器删除。
- Background: 指示 Kubernetes API 在所有者资源被删除后立即从 DELETE 操作中返回,而不是等待自有资源被垃圾收集器删除。
- Foreground: 指示 Kubernetes API 在所有者和 BlockOwnerDeletion 设置为 “true “的自有资源被删除后,从 DELETE 操作中返回。Kubernetes API 将不会等待其他拥有的资源被删除。
因此,如果你正在编写一个 Controller 或其他进程,需要等待所有拥有的资源被删除,该进程将需要在所有拥有的资源上将 BlockOwnerDeletion 字段设置为true,并在删除所有者资源时使用 Foreground 传播策略。
规格和状态
在常见的 Type 和 Metadata 之后,资源定义一般由两部分组成:Spec 和 Status。
请注意,并非所有资源都是如此。例如,核心的 ConfigMap 和 Secret 资源,仅举几例,不包含 Spec 和 Status 部分。更普遍的是,包含配置数据的资源,不由任何 Controller 管理,不包含这些字段。
Spec 是用户将定义的部分,它表示用户所期望的状态。管理该资源的 Controller 将读取Spec,它将根据 Spec 在集群上创建、更新或删除资源,并将其操作的状态检索到资源的 Status 部分。Controller 用于读取 Spec、应用于集群并检索状态的这个过程被称为 Reconcile Loop。
与编写 YAML 清单 的比较
当您编写 Kubernetes 清单 以用于kubectl时:
-
清单以 apiVersion 和 kind 开始。
-
metadata 字段包含该资源的所有元数据。
-
Spec 和 Status 字段(或其他)紧随其后。
当您用 Go 编写 Kubernetes 结构体时,会发生以下情况:
-
结构体的类型决定了apiVersion 和 Kind;不需要指定它们。
-
metadata 可以通过嵌入metav1.ObjectMeta 结构体来定义,也可以通过在资源上使用元数据设置器(metadata setters )。
-
Spec 和 Status 字段(或其他)紧随其后,使用它们自己的 Go 结构体或其他类型。
举个例子,下面是用 YAML 清单和 Go 定义 Pod 的方法。在 YAML 中:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
- component: my-component,
spec:
containers:
- image: nginx
name: nginx
在 Go 中,当你为元数据使用设置器时:
pod := corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "runtime",
Image: "nginx",
},
},
},
}
pod.SetName("my-pod")
pod.SetLabels(map[string]string{
"component": "my-component",
})
或者,在 Go 中,当你将 metav1.ObjectMeta
结构体嵌入到 Pod 结构体中时:
pod2 := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "nginx",
Labels: map[string]string{
"component": "mycomponent",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "runtime",
Image: "nginx",
},
},
},
}
完整的例子
这个完整的例子使用到此为止学到的概念,使用 POST 请求在集群上创建一个 Pod。
-
➊ 使用 Go 结构体建立一个 Pod 对象,如本章前面所示
-
➋ 使用序列化器将 Pod 对象序列化为 JSON 格式(详见第五章)。
-
➌ 建立一个 HTTP POST 请求,其主体包含要创建的 Pod,并以 JSON 形式序列化
-
➍ 用建立的请求调用 Kubernetes API
-
➎ 从响应中获取主体
-
➏ 根据响应的状态代码:
如果请求返回 2xx 状态代码:
- ➐ 将响应主体反序列化为一个Pod Go结构体
- ➑ 将创建的Pod对象显示为JSON格式的信息;
否则:
-
➒ 将响应体反序列化为一个状态Go结构体。
-
➓ 将Status对象显示为JSON格式,以获取信息:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
)
func createPod() error {
pod := createPodObject() ➊
serializer := getJSONSerializer()
postBody, err := serializePodObject(serializer, pod) ➋
if err != nil {
return err
}
reqCreate, err := buildPostRequest(postBody) ➌
if err != nil {
return err
}
client := &http.Client{}
resp, err := client.Do(reqCreate) ➍
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body) ➎
if err != nil {
return err
}
if resp.StatusCode < 300 { ➏
createdPod, err := deserializePodBody(serializer, body) ➐
if err != nil {
return err
}
json, err := json.MarshalIndent(createdPod, "", " ")
if err != nil {
return err
}
fmt.Printf("%s\n", json) ➑
} else {
status, err := deserializeStatusBody(serializer, body) ➒
if err != nil {
return err
}
json, err := json.MarshalIndent(status, "", " ")
if err != nil {
return err
}
fmt.Printf("%s\n", json) ➓
}
return nil
}
func createPodObject() *corev1.Pod { ➊
pod := corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "runtime",
Image: "nginx",
},
},
},
}
pod.SetName("my-pod")
pod.SetLabels(map[string]string{
"app.kubernetes.io/component": "my-component",
"app.kubernetes.io/name": "a-name",
})
return &pod
}
func serializePodObject( ➋
serializer runtime.Serializer,
pod *corev1.Pod,
) (
io.Reader,
error,
) {
var buf bytes.Buffer
err := serializer.Encode(pod, &buf)
if err != nil {
return nil, err
}
return &buf, nil
}
func buildPostRequest( ➌
body io.Reader,
) (
*http.Request,
error,
) {
reqCreate, err := http.NewRequest(
"POST",
"http://127.0.0.1:8001/api/v1/namespaces/default/pods",
body,
)
if err != nil {
return nil, err
}
reqCreate.Header.Add(
"Accept",
"application/json",
)
reqCreate.Header.Add(
"Content-Type",
"application/json",
)
return reqCreate, nil
}
func deserializePodBody( ➐
serializer runtime.Serializer,
body []byte,
) (
*corev1.Pod,
error,
) {
var result corev1.Pod
_, _, err := serializer.Decode(body, nil, & result)
if err != nil {
return nil, err
}
return &result, nil
}
func deserializeStatusBody( ➒
serializer runtime.Serializer,
body []byte,
) (
*metav1.Status,
error,
) {
var status metav1.Status
_, _, err := serializer.Decode(body, nil, & status)
if err != nil {
return nil, err
}
return & status, nil
}
func getJSONSerializer() runtime.Serializer {
scheme := runtime.NewScheme()
scheme.AddKnownTypes(
schema.GroupVersion{
Group: "",
Version: "v1",
},
&corev1.Pod{},
&metav1.Status{},
)
return json.NewSerializerWithOptions(
json.SimpleMetaFactory{},
nil,
scheme,
json.SerializerOptions{},
)
}
总结
在本章中,你已经发现了在 Go 中与 Kubernetes 合作的第一个库– API库。它本质上是一个 Go 结构体的集合,用于声明 Kubernetes 资源。本章还探讨了 API Machinery 中定义的所有资源共有的元数据字段的定义。
在本章的最后,有一个使用 API Machinery 构建Pod定义的程序实例,然后通过使用HTTP请求调用API服务器在集群中创建这个Pod。
下一章将探讨其他基本库–API Machinery 和 client-go,你将不再需要建立HTTP请求。
5.4 - [第4章]使用通用类型
上一章介绍了如何使用Go结构来定义Kubernetes资源。特别是,它解释了Kubernetes API 库的包的内容,以及与 Kubernetes Kind 相关的每个资源的常见字段。
本章研究了在定义 Kubernetes 资源时可在不同地方使用的常见类型。
指针
Go 结构体中的可选值通常被声明为指向一个值的指针。因此,如果你不想指定可选值,你只需要把它作为一个 nil 指针,如果你需要指定一个值,你必须创建该值并传递其引用。
Kubernetes Utils 库的 pointer 包定义了声明这种可选值的实用函数。
import (
"k8s.io/utils/pointer"
)
获取值的引用
Int, Int32, Int64, Bool, String, Float32, Float64, 和 Duration 函数接受一个相同类型的参数,并返回一个指向作为参数的值的指针。例如,Int32 函数的定义如下:
func Int32(i int32) *int32 {
return &i
}
然后,你可以这样使用它:
spec := appsv1.DeploymentSpec{
Replicas: pointer.Int32(3),
[...]
}
对指针的解引用
另一种方法是,你可以得到指针所引用的值,或者在指针为零时得到一个默认值。
IntDeref、Int32Deref、Int64Deref、BoolDeref、StringDeref、Float32Deref、Float64Deref 和 DurationDeref 函数接受一个指针和一个默认值作为同一类型的参数,如果指针不为零,它就返回引用的值,否则就是默认值。例如,Int32Deref 函数定义如下:
func Int32Deref(ptr *int32, def int32) int32 {
if ptr != nil {
return *ptr
}
return def
}
然后,你可以这样使用它:
replicas := pointer.Int32Deref(spec.Replicas, 1)
比较两个引用的值
比较两个指针值可能很有用,考虑到如果它们都是 nil,或者它们引用了两个相等的值,那么它们就是相等的。
Int32Equal, Int64Equal, BoolEqual, StringEqual, Float32Equal, Float64Equal, 和 DurationEqual 函数接受两个相同类型的指针,如果这两个指针为 nil,或者它们引用的值相同,则返回真。例如,Int32Equal 函数定义如下:
func Int32Equal(a, b *int32) bool {
if (a == nil) != (b == nil) {
return false
}
if a == nil {
return true
}
return *a == *b
}
然后,你可以这样使用它:
eq := pointer.Int32Equal(
spec1.Replicas,
spec2.Replicas,
)
请注意,要测试一个可选值的平等性,同时考虑其默认值,你应该使用以下方法:
isOne := pointer.Int32Deref(spec.Replicas, 1) == 1
Quantities
Quantities 是一个数字的定点(fixed-point)表示,用于定义要分配的资源数量(例如,内存、CPU等)。Quantities 能代表的最小值是一纳米(10-9)。
在内部,Quantities 由一个Integer(64位)和一个 Scale 表示,或者,如果 int64 不够大,则由一个 inf.Dec 值表示(由软件包定义在 https://github.com/go-inf/inf )
import (
"k8s.io/apimachinery/pkg/api/resource"
)
将字符串解析为数量
定义 Quantity 的第一种方法是通过使用以下函数之一从字符串中解析其值:
func MustParse(str string) Quantity
- 从字符串中解析出 Quantity,如果字符串不代表一个 Quantity,则会出现 panic。当你应用一个你知道是有效的硬编码值时,就可以使用它。func ParseQuantity(str string) (Quantity, error)
- 从字符串中解析 Quantity,如果字符串不代表一个 Quantity,则返回错误。当你不确定该值是否有效时,可以使用该方法。
Quantity 可以用一个符号、一个数字和一个后缀来写。符号和后缀是可选的。后缀可以是二进制,十进制,或十进制指数。
定义的二进制后缀是 Ki (210), Mi (220), Gi (230), Ti (240), Pi (250) 和 Ei (260)。定义的十进制后缀是 n(10-9),u(10-6),m(10-3),""(100),k(103),M(106),G(109),T(1012),P(1015),和E(1018)。十进制指数后缀用 e 或 E 符号书写,后面是十进制指数–例如,E2代表102。
后缀格式(二进制、十进制或十进制指数)被保存在 Quantity 中,并在序列化 Quantity 时使用。
使用这些函数,数量的内部表示方式(要么是按比例的整数,要么是inf.Dec)将根据解析的值是否可以表示为按比例的整数来决定。
使用 inf.Dec 作为Quantity
使用下面的方法来使用 inf.Dec 作为一个Quantity:
func NewDecimalQuantity(b inf.Dec, format Format) *Quantity
- 通过给出一个inf.Dec
值,并指出你希望它被序列化的后缀格式来声明 Quantity。func (q *Quantity) ToDec() *Quantity
- 通过解析一个字符串或使用一个新的函数来初始化一个 Quantity,强制将它存储为一个 inf.Dec。func (q *Quantity) AsDec() *inf.Dec
- 获得一个作为inf.Dec
的 Quantity 表示,而不修改内部表示。
使用带刻度的整数作为Quantity
使用下面的方法来使用一个带尺度的整数作为 Quantity:
-
func NewScaledQuantity(value int64, scale Scale) *Quantity
- 通过给出一个 int64 值和一个刻度来声明 Quantity。后缀的格式将是十进制格式。 -
func (q *Quantity) SetScaled(value int64, scale Scale)
- 用一个带刻度的整数覆盖 Quantity 值。后缀的格式将保持不变。 -
func (q *Quantity) ScaledValue(scale Scale) int64
- 获得带刻度的整数表示,考虑到给定的刻度,不修改内部表示。 -
func NewQuantity(value int64, format Format) *Quantity
- 通过给出一个 int64 的值来声明 Quantity,刻度被固定为0,并且在序列化过程中使用一个后缀格式。 -
func (q *Quantity) Set(value int64)
- 用一个整数和一个固定为0的刻度来重写 Quantity 值,后缀的格式将保持不变。 -
func (q *Quantity) Value() int64
- 获得一个数量的整数表示,比例为0,不修改内部表示。 -
func NewMilliQuantity(value int64, format Format) *Quantity - 通过给出一个int64的值来声明 Quantity,比例固定为-3,后缀格式在序列化时使用。
-
func (q *Quantity) SetMilli(value int64) - 用整数和固定为-3的刻度重写一个 Quantity 值。
-
func (q *Quantity) MilliValue() int64 - 得到 Quantity 的整数表示,比例为-3,不需要修改内部表示。
对Quantity的操作
以下是对 “数量 “进行操作的方法:
-
func (q *Quantity) Add(y Quantity) - 将 y Quantity加入到 q Quantity 中。
-
func (q *Quantity) Sub(y Quantity) - 从q数量中减去y数量。
-
func (q *Quantity) Cmp(y Quantity) int - 比较q和y的数量。如果两个数量相等返回0,如果q大于y返回1,如果q小于y返回-1。
-
func (q *Quantity) CmpInt64(y int64) int - 比较q数量和y的整数。如果两个数量相等,返回0;如果q大于y,返回1;如果q小于y,返回1。
-
func (q *Quantity) Neg() - 使q成为它的负值。
-
func (q 数量) Equal(v 数量) bool - 测试q和v数量是否相等。
IntOrString
Kubernetes 资源的一些字段接受整数值或字符串值。例如,端口可以用端口号或 IANA 服务名称来定义(如https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml)。
另一个例子是可以接受整数或百分比的字段。
import (
"k8s.io/apimachinery/pkg/util/intstr"
)
IntOrString结构定义如下:
type IntOrString struct {
Type Type
IntVal int32
StrVal string
}
TBD
Time
TBD
总结
本章介绍了在Go中定义Kubernetes资源时使用的常见类型。指针值用于指定可选的值,数量用于指定内存和CPU数量,IntOrString类型用于可写为整数或字符串的值(例如,可以通过数字或名称定义的端口),以及时间–一种可序列化的类型,包装Go时间类型。
5.5 - [第5章]API Machinery
前面几章探讨了Kubernetes API在HTTP层面的工作方式。还探讨了 Kubernetes API 库,该库在Go中定义了由 Kubenretes API 提供的资源。
本章探讨了 Kubernetes API Machinery,它提供了用于处理遵循 Kubernetes API 对象约定的API对象的实用工具。这些约定包括:
-
API对象嵌入了一个共同的元数据结构体,TypeMeta,包含两个字段: APIVersion 和 Kind。
-
API 对象是在一个单独的包中提供的。
-
API 对象是有版本的。
-
提供了转换函数来转换不同的版本。
API Machinery 将提供以下实用程序:
-
Scheme抽象,用于:
-
将 API 对象注册为 Group-Version-Kinds
-
不同版本的API对象之间的转换
-
对API对象进行序列化/反序列化
-
-
RESTMapper,在 API 对象(基于嵌入式 APIVersion 和 Kind)和资源名称(REST意义上的)之间进行映射。
本章详细介绍了 API Machinery 所提供的功能。
Schema 包
API Machinery 的 schema 包定义了有用的结构体和函数来处理分组、版本、种类和资源。
import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
定义了 GroupVersionResource、GroupVersionKind、GroupVersion、GroupResource 和 GroupKind 结构,以及从一个到另一个的转换方法。
此外,还提供了在 GroupVersionKind 和(apiVersion, kind)之间转换的函数: ToAPIVersionAndKind 和 FromAPIVersionAndKind。
Scheme
Scheme 是一个抽象概念,用于将 API 对象注册为 Group-Version-Kinds,在不同版本的 API 对象之间进行转换,并将 API 对象序列化/反序列化。Scheme 是由运行时包中的 API Machinery 提供的一个结构体。这个结构体的所有字段都是未导出(unexported)的。
初始化
Scheme 结构体可以通过 NewScheme 函数进行初始化:
import (
"k8s.io/apimachinery/pkg/runtime"
)
Scheme := runtime.NewScheme()
在结构体被初始化后,你可以用 AddKnownTypes 方法注册新的 API 对象,方法如下:
func (s *Scheme) AddKnownTypes(gv schema.GroupVersion, types ...Object)
例如,为了将 Pod 和 ConfigMap 对象注册到 core/v1
分组,你可以使用:
import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
Scheme := runtime.NewScheme()
func init() {
Scheme.AddKnownTypes(
schema.GroupVersion{
Group: "",
Version: "v1",
},
&corev1.Pod{},
&corev1.ConfigMap{},
)
}
通过这样做,API Machinery 将能够知道在执行与 pod 相关的请求时要使用的 Group-Version-Kind core-v1-Pod
必须是 corev1.Pod
结构体,而在执行与configmaps 相关的请求时要使用的 core-v1-ConfigMap
必须是 corev1.ConfigMap
结构体。
已经表明,API 对象可以被版本化。你可以通过这种方式为不同的版本注册同一个种类–例如,使用下面的方法来添加部署对象的v1和v1beta1版本:
import (
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
Scheme := runtime.NewScheme()
func init() {
Scheme.AddKnownTypes(
schema.GroupVersion{
Group: "apps",
Version: "v1",
},
&appsv1.Deployment{},
)
Scheme.AddKnownTypes(
schema.GroupVersion{
Group: "apps",
Version: "v1beta1",
},
&appsv1beta1.Deployment{},
)
}
建议在执行的一开始就初始化 Scheme 结构体并向其添加已知类型–例如,使用 init 函数。
Mapping/映射
初始化后,你可以使用结构体上的各种方法来映射 Goup-Version-Kinds 和 Go Types:
-
KnownTypes(gv schema.GroupVersion) map[string]reflect.Type
- 获得所有为特定 Group-Version 注册的 go 类型- 这里是app/v1
types := Scheme.KnownTypes(schema.GroupVersion{ Group: "apps", Version: "v1", }) -> ["Deployment": appsv1.Deployment]
-
VersionsForGroupKind(gk schema.GroupKind) []schema.GroupVersion
- 获取所有为特定种类(这里是 Deployment)注册的 Group-Version:groupVersions := Scheme.VersionsForGroupKind( schema.GroupKind{ Group: "apps", Kind: "Deployment", }) -> ["apps/v1" "apps/v1beta1"]
-
ObjectKinds(obj Object) ([]schema.GroupVersionKind, bool, error)
- 获得一个给定对象的所有可能的 Group-Version-Kinds,这里是appsv1.Deployment
:gvks, notVersioned, err := Scheme.ObjectKinds(&appsv1.Deployment{}) -> ["apps/v1 Deployment"]
-
New(kind schema.GroupVersionKind) (Object, error)
- 给定 Group-Version-Kind构建对象:obj, err := Scheme.New(schema.GroupVersionKind{ Group: "apps", Version: "v1", Kind: "Deployment", })
这个方法返回一个类型为 runtime.Object 的值,这是一个由所有 API 对象实现的接口。该值的具体类型将是映射 Group-Version-Kind
到的对象,这里是appsv1.Deployment 。
转换
Scheme 结构体按分组-版本(Group-Version)注册种类(kind)。通过向 Scheme 提供同一分组和不同版本的 kind 之间的转换函数,就有可能在同一分组的任何种类之间进行转换。
我们可以定义两个层次的转换函数:转换函数和生成的转换函数。转换函数是用手写的函数,而生成的转换函数是用转换生成工具生成的。
当在两个版本之间进行转换时,如果存在转换函数,它将优先于生成的转换函数。
添加转换函数
这两个方法在 a 和 b 之间添加了一个转换函数,这两个对象的类型属于同一个分组。
AddConversionFunc(
a, b interface{},
fn conversion.ConversionFunc,
) error
AddGeneratedConversionFunc(
a, b interface{},
fn conversion.ConversionFunc,
) error
a 和 b 的值必须是指向结构体的指针,可以是 nil 指针。转换函数的签名定义如下:
type ConversionFunc func(
a, b interface{},
scope Scope,
) error
下面是一个例子,在 apps/v1
和 apps/v1beta1
deployment 之间添加一个转换功能:
Scheme.AddConversionFunc(
(*appsv1.Deployment)(nil),
(*appsv1beta1.Deployment)(nil),
func(a, b interface{}, scope conversion.Scope) error{
v1deploy := a.(*appsv1.Deployment)
v1beta1deploy := b.(*appsv1beta1.Deployment)
// make conversion here
return nil
})
至于将已知类型注册到 schema 中,建议在执行的最开始注册转换函数–例如,使用 init 函数。
转换
一旦转换函数被注册,就可以用转换函数在同一类型的两个版本之间进行转换。
Convert(in, out interface{}, context interface{}) error
这个例子定义了一个 v1.Deployment
,然后将其转换为 v1beta1
版本:
v1deployment := appsv1.Deployment{
[...]
}
v1deployment.SetName("myname")
var v1beta1Deployment appsv1beta1.Deployment
scheme.Convert(&v1deployment, &v1beta1Deployment, nil)
序列化
API Machinery 的包提供了各种格式的序列化器: JSON、YAML 和 Protobuf。这些序列化器实现了序列化器接口,它嵌入了编码器和解码器接口。首先,你可以看到如何为不同的格式实例化序列化器,然后如何使用它们对 API 对象进行编码和解码。
JSON和YAML序列化器
json 包为 JSON 和 YAML 格式提供了一个序列化器。
import (
"k8s.io/apimachinery/pkg/runtime/serializer/json"
)
NewSerializerWithOptions 函数被用来创建一个新的序列化器。
NewSerializerWithOptions(
meta MetaFactory,
creater runtime.ObjectCreater,
typer runtime.ObjectTyper,
options SerializerOptions,
) *Serializer
这些选项提供了在 JSON 和 YAML 序列化器之间进行选择的可能性(Yaml 字段),为J SON 输出选择人类可读的输出(漂亮字段),并检查 JSON 和 YAML 中的重复字段(Stric /严格字段)。
type SerializerOptions struct {
Yaml bool
Pretty bool
Strict bool
}
Scheme 可以用于 creator 和 typer ,因为它实现了这两个接口,而 SimpleMetaFactory 结构体可以作为 meta。
serializer := jsonserializer.NewSerializerWithOptions(
jsonserializer.SimpleMetaFactory{},
Scheme,
Scheme,
jsonserializer.SerializerOptions{
Yaml: false, // or true for YAML serializer
Pretty: true, // or false for one-line JSON
Strict: false, // or true to check duplicates
},
)
Protobuf序列化器
protobuf 包为 Protobuf 格式提供了一个序列化器。
import (
"k8s.io/apimachinery/pkg/runtime/serializer/protobuf"
)
NewSerializer 函数被用来创建一个新的序列化器。
NewSerializer(
creater runtime.ObjectCreater,
typer runtime.ObjectTyper,
) *Serializer
Scheme 可以用于 creator 和 typer ,因为它实现了这两个接口。
serializer := protobuf.NewSerializer(Scheme, Scheme)
编码和解码
各种序列化器实现了 Serializer 接口,它嵌入了 Decoder 和 Encoder 接口,定义了 Encode 和 Decode 方法。
-
Encode(obj Object, w io.Writer) error
- Encode函数将一个API对象作为参数,对该对象进行编码,并使用 Writer 写入结果。 -
decode 函数接收一个字节数组作为参数,并尝试解码其内容。如果要解码的内容没有指定 apiVersion 和 Kind,将使用默认的 GroupVersionKind(GVK)。
Decode( data []byte, defaults *schema.GroupVersionKind, into Object, ) ( Object, *schema.GroupVersionKind, error, )
如果不是 nil,并且 into 的具体类型与内容GVK(初始类型或默认类型)相匹配,结果将被放置在 into 对象中。在任何情况下,结果将作为一个对象返回,应用于它的 GVK 将作为一个 GroupVersionKind 结构体返回。
RESTMapper
API Machinery 提供了一个 RESTMapper 的概念,用于在 REST 资源和 Kinds 之间映射。
import (
"k8s.io/apimachinery/pkg/api/meta"
)
RESTMapping 类型提供了使用 RESTMapper 进行映射的结果:
type RESTMapping struct {
Resource schema.GroupVersionResource
GroupVersionKind schema.GroupVersionKind
Scope RESTScope
}
正如第一章所讨论的,GVR(Group-Version-Resource,简称Resource)是用来建立请求的路径的。例如,要获得所有命名空间中的 deployment 列表,你将使用 /apis/apps/v1/deployments
这个路径,其中 apps 是分组,v1是版本,deployments 是(复数)资源名称。所以,一个由 API 管理的资源可以通过它的GVR来唯一识别。
当向这个路径发出请求时,通常你想交换数据,要么在创建或更新资源的请求中,要么在获取或列出资源的响应中。这种交换数据的格式被称为Kind(或GroupVersionKind),与资源有关。
RESTMapping 结构体将资源和其相关的 GroupVersionKind 结合起来。API machinery 提供了一个 RESTMapper 接口和一个默认实现 DefaultRESTMapper。
type RESTMapper interface {
RESTMapping(gk schema.GroupKind, versions ...string)
(*RESTMapping, error)
RESTMappings(gk schema.GroupKind, versions ...string)
([]*RESTMapping, error)
KindFor(resource schema.GroupVersionResource)
(schema.GroupVersionKind, error)
KindsFor(resource schema.GroupVersionResource)
([]schema.GroupVersionKind, error)
ResourceFor(input schema.GroupVersionResource)
(schema.GroupVersionResource, error)
ResourcesFor(input schema.GroupVersionResource)
([]schema.GroupVersionResource, error)
ResourceSingularizer(resource string)
(singular string, err error)
}
Kind 到 Resource
RESTMapping 和 RESTMappings 方法返回一个元素或一个 RESTMapping 结构体数组作为结果,给定一个分组和Kind。一个可选的版本列表表示首选版本。
RESTMappings 方法返回所有匹配,RESTMapping 方法返回单个匹配,如果有多个匹配,则返回错误。得到的 RESTMapping 元素将包含完全合格的Kind(包括版本)和完全合格的资源。
总而言之,这些方法是用来将 Kind 映射到资源。
资源到 kind
KindFor 和 KindsFor 方法返回一个 GroupVersionKind 的元素或数组,给定一个部分 Group-Version-Resource。部分意味着你可以省略分组,版本,或两者。资源名称可以是资源的单数或复数名称。
KindsFor 方法返回所有的匹配,KindFor 方法返回单个匹配,如果有多个匹配,则返回错误。
总而言之,这些方法是用来将资源映射到 kind。
寻找资源
ResourceFor 和 ResourcesFor 方法返回一个 GroupVersionResource 的元素或数组,给出一个部分 Group-Version-Resource。部分的意思是你可以省略分组,版本,或两者。资源名称可以是资源的单数或复数名称。
ResourcesFor 方法返回所有的匹配结果,ResourceFor 方法返回单个匹配结果,如果有多个匹配结果,则返回一个错误。
总而言之,这些方法是用来根据单数或复数的资源名称寻找完全合格的资源。
默认的RESTMapper实现
API Machinery 提供了一个 RESTMapper 的默认实现。
-
NewDefaultRESTMapper
NewDefaultRESTMapper( defaultGroupVersions []schema.GroupVersion, ) *DefaultRESTMapper
该工厂方法用于构建一个新的 DefaultRESTMapper,并接受一个默认 Group-Versions 列表,当所提供的GVR是部分的时候,该列表将被用来查找资源或Kinds。
- Add
Add(kind schema.GroupVersionKind, scope RESTScope)
- 该方法用于添加 Kind 和资源之间的映射。资源名称将从 Kind 中猜测出来,方法是获得小写的单词,并将其复数化(在以 “s “结尾的单词中添加 “es”,在以 “y “结尾的单词中用 “ies “替换终端 “y”,并在其他单词中添加 “s”)。
-
AddSpecific
AddSpecific( kind schema.GroupVersionKind、 plural, singular schema.GroupVersionResource、 scope RESTScope)
这个方法用于通过明确给出单数和复数的名称来添加 Kind 和资源之间的映射。
创建 DefaultRESTMapper 实例后,你可以通过调用同名接口中定义的方法将其作为 RESTMapper 使用。
结语
本章探讨了API Machinery,介绍了用于在 Go 和 JSON 或 YAML 之间序列化资源的 Scheme 抽象,以及在几个版本之间转换资源。本章还介绍了 RESTMapper 接口,帮助在资源和 kind 之间进行映射。
下一章介绍了 Client-go 库,这是一个高层次的库,开发者用来调用 Kubernetes API,而不需要使用HTTP调用。
5.6 - [第6章]client-go类库
前几章探讨了 Kubernetes API 库和 API Machinery,前者是用于处理Kubernetes API对象的Go结构体的集合,后者则提供了用于处理遵循 Kubernetes API 对象约定的API对象的实用程序。具体来说,你已经看到API Machinery 提供了 Scheme 和 RESTMapper 的抽象。
本章探讨了 Client-go 库,它是一个高级别库,开发者可以使用 Go 语言与 Kubernetes API 进行交互。Client-go 库汇集了 Kubernetes API 和 API Machinery,提供了一个预先配置了 Kubernetes API 对象的 Scheme 和一个用于 Kubernetes API 的 RESTMapper 实现。它还提供了一套客户端,用于以简单的方式对Kubernetes API 的资源执行操作。
要使用这个库,你需要从其中导入包,前缀为 k8s.io/client-go
。例如,要使用包 kubernetes,让我们使用以下内容:
import (
"k8s.io/client-go/kubernetes"
)
你还需要下载一个 client-go 库的版本。为此,你可以使用 go get
命令来获得你要使用的版本:
go get k8s.io/client-go@v0.24.4
Client-go 库的版本与 Kubernetes 的版本是一致的–0.24.4版本对应服务器的1.24.4版本。
Kubernetes 是向后兼容的,所以你可以在较新版本的集群中使用旧版本的Client-go,但你很可能希望得到一个最新的版本,以便能够使用当前的功能,因为只有bug修复被回传到以前的 Client-go 版本,而不是新功能。
连接到集群
连接到 Kubernetes API 服务器之前的第一步是让配置连接到它–即服务器的地址、证书、连接参数等。
rest 包提供了一个 rest.Config
结构体,它包含了一个应用程序连接到 REST API 服务器所需的所有配置信息。
集群内配置
默认情况下,在 Kubernetes Pod 上运行的容器包含连接到API服务器所需的所有信息:
-
Pod 使用的 ServiceAccount 提供的令牌和根证书可以在这个目录中找到:
/var/run/secrets/kubernetes.io/serviceaccount/
-
请注意,可以通过在 Pod 使用的 ServiceAccount 中设置
automountServiceAccountToken: false
,或直接在 Pod 的 Spec 中设置automountServiceAccountToken: false
来禁用这种行为。 -
环境变量,
KUBERNETES_SERVICE_HOST
和KUBERNETES_SERVICE_PORT
,定义在容器环境中,由 kubelet 添加,定义了联系API server 的主机和端口。
当一个应用程序专门在 Pod 的容器内运行时,你可以使用以下函数来创建一个适当的 rest.Config
结构体,利用刚才描述的信息:
import "k8s.io/client-go/rest"
func InClusterConfig() (*Config, error)
集群外的配置
Kubernetes 工具通常依赖于 kubeconfig 文件–即一个包含一个或几个 Kubernetes 集群的连接配置的文件。
你可以通过使用 clientcmd 包中的以下函数之一,根据这个 kubeconfig 文件的内容建立一个 rest.Config
结构体。
从内存中的kubeconfig
RESTConfigFromKubeConfig 函数可以用来从作为一个字节数组的 kubeconfig 文件的内容中建立一个 rest.Config
结构体:
func RESTConfigFromKubeConfig(
configBytes []byte,
) (*rest.Config, error)
如果 kubeconfig 文件包含几个上下文(context),将使用当前的上下文,而其他的上下文将被忽略。例如,你可以先读取一个 kubeconfig 文件的内容,然后使用以下函数:
import "k8s.io/client-go/tools/clientcmd"
configBytes, err := os.ReadFile(
"/home/user/.kube/config",
)
if err != nil {
return err
}
config, err := clientcmd.RESTConfigFromKubeConfig(
configBytes,
)
if err != nil {
return err
}
从磁盘上的kubeconfig
BuildConfigFromFlags 函数可用于从 API server 的URL中建立 rest.Config
结构体,或基于给定路径的 kubeconfig 文件,或两者都是。
func BuildConfigFromFlags(
masterUrl,
kubeconfigPath string,
) (*rest.Config, error)
下面的代码可以让你得到一个 rest.Config
结构体:
import "k8s.io/client-go/tools/clientcmd"
config, err := clientcmd.BuildConfigFromFlags(
"",
"/home/user/.kube/config",
)
下面的代码从 kubeconfig 获取配置,并重写了 api server 的 URL:
config, err := clientcmd.BuildConfigFromFlags(
"https://192.168.1.10:6443",
"/home/user/.kube/config",
)
来自个性化的kubeconfig
前面的函数在内部使用一个 api.Config
结构体,代表 kubeconfig 文件中的数据(不要与包含 REST HTTP 连接参数的 rest.Config
结构体混淆)。
如果你需要操作这个中间数据,你可以使用 BuildConfigFromKubeconfigGetter 函数,接受一个 kubeconfigGetter 函数作为参数,它本身将返回一个 api.Config
结构体。
BuildConfigFromKubeconfigGetter(
masterUrl string,
kubeconfigGetter KubeconfigGetter,
) (*rest.Config, error)
type KubeconfigGetter
func() (*api.Config, error)
例如,以下代码将用 clientcmd.Load
或 clientcmd.LoadFromFile
函数从 kubeconfigGetter
函数加载 kubeconfig
文件:
import (
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
)
config, err :=
clientcmd.BuildConfigFromKubeconfigGetter(
"",
func() (*api.Config, error) {
apiConfig, err := clientcmd.LoadFromFile(
"/home/user/.kube/config",
)
if err != nil {
return nil, nil
}
// TODO: manipulate apiConfig
return apiConfig, nil
},
)
来自多个kubeconfig文件
kubectl 工具默认使用 $HOME/.kube/config
kubeconfig`文件,你可以使用 KUBECONFIG 环境变量指定另一个 kubeconfig 文件路径。
不仅如此,你还可以在这个环境变量中指定一个 kubeconfig 文件路径的列表,这些 kubeconfig 文件在被使用之前将被合并成一个而已。你可以用这个函数获得同样的行为: NewNonInteractiveDeferredLoadingClientConfig。
func NewNonInteractiveDeferredLoadingClientConfig(
loader ClientConfigLoader,
overrides *ConfigOverrides,
) ClientConfig
clientcmd.ClientConfigLoadingRules 类型实现了 ClientConfigLoader 接口,你可以用下面的函数得到这个类型的值:
func NewDefaultClientConfigLoadingRules()
*ClientConfigLoadingRules
这个函数将获得 KUBECONFIG 环境变量的值,如果它存在的话,以获得要合并的 kubeconfig 文件的列表,或者将退回到使用位于 $HOME/.kube/config
的默认kubeconfig文件。
使用下面的代码来创建 rest.Config
结构体,你的程序将具有与 kubectl 相同的行为,如前所述:
import (
"k8s.io/client-go/tools/clientcmd"
)
config, err :=
clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
nil,
).ClientConfig()
用CLI标志重写kubeconfig
已经表明,这个函数的第二个参数,NewNonInteractiveDeferredLoadingClientConfig,是一个 ConfigOverrides 结构。这个结构包含覆盖合并 kubeconfig 文件结果的一些字段的值。
你可以自己在这个结构体中设置特定的值,或者,如果你正在使用 spf13/pflag
库(即 github.com/spf13/pflag
)创建一个CLI,你可以使用下面的代码为你的CLI自动声明默认标志,并将它们绑定到 ConfigOverrides 结构体:
import (
"github.com/spf13/pflag"
"k8s.io/client-go/tools/clientcmd"
)
var (
flags pflag.FlagSet
overrides clientcmd.ConfigOverrides
of = clientcmd.RecommendedConfigOverrideFlags("")
)
clientcmd.BindOverrideFlags(&overrides, &flags, of)
flags.Parse(os.Args[1:])
config, err :=
clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&overrides,
).ClientConfig()
注意,你可以在调用函数 RecommendedConfigOverrideFlags 时为添加的标志声明一个前缀。
获取 ClientSet
Kubernetes 包提供了创建 kubernetes.Clientset
类型的 ClientSet 的函数。
-
func NewForConfig(c *rest.Config) (*Clientset, error)
- NewForConfig 函数返回 ClientSet,使用提供的rest.Config
与上一节中看到的方法之一构建。 -
func NewForConfigOrDie(c *rest.Config) *Clientset
- 这个函数和前一个函数一样,但是在出错的情况下会 panic ,而不是返回错误。这个函数可以与硬编码的配置一起使用,你会想要断言其有效性。 -
NewForConfigAndClient( c *rest.Config、 httpClient *http.Client、 ) (*Clientset, error)
这个 NewForConfigAndClient 函数使用提供的
rest.Config
和提供的http.Client
返回一个 Clientset。
之前的函数 NewForConfig 使用的是用函数 rest.HTTPClientFor 构建的默认 HTTP 客户端。如果你想在构建客户集之前个性化HTTP客户端,你可以使用这个函数来代替。
使用 ClientSet
kubernetes.Clientset 类型实现了 kubernetes.Interface 接口,定义如下:
type Interface interface {
Discovery() discovery.DiscoveryInterface
[...]
AppsV1() appsv1.AppsV1Interface
AppsV1beta1() appsv1beta1.AppsV1beta1Interface
AppsV1beta2() appsv1beta2.AppsV1beta2Interface
[...]
CoreV1() corev1.CoreV1Interface
[...]
}
第一个方法 Discovery() 提供了对一个接口的访问,该接口提供了发现集群中可用的分组、版本和资源的方法,以及资源的首选版本。这个接口还提供对服务器版本和 OpenAPI v2 和v3定义的访问。这将在发现客户端部分详细研究。
除了 Discovery() 方法外,kubernetes.Interface 由一系列方法组成,Kubernetes API 定义的每个 Group/Version 都有一个。当你看到这个接口的定义时,就可以理解 ClientSet 是一组客户端,每个客户端都专门用于自己的分组/版本。
每个方法都会返回一个值,该值实现了该分组/版本的特定接口。例如,kubernetes.Interface的CoreV1()
方法返回一个值,实现 corev1.CoreV1Interface
接口,定义如下:
type CoreV1Interface interface {
RESTClient() rest.Interface
ComponentStatusesGetter
ConfigMapsGetter
EndpointsGetter
[...]
}
这个 CoreV1Interface 接口的第一个方法是 RESTClient() rest.Interface
,它是一个用来获取特定 Group/Version 的 REST 客户端的方法。这个低级客户端将被 Group/Version 客户端内部使用,你可以使用这个 REST 客户端来构建这个 CoreV1Interface 接口的其他方法所不能原生提供的请求。
由 REST 客户端实现的接口 rest.Interface
定义如下:
type Interface interface {
GetRateLimiter() flowcontrol.RateLimiter
Verb(verb string) *Request
Post() *Request
Put() *Request
Patch(pt types.PatchType) *Request
Get() *Request
Delete() *Request
APIVersion() schema.GroupVersion
}
正如你所看到的,这个接口提供了一系列的方法–Verb、Post、Put、Patch、Get 和 Delete–它们返回一个带有特定 HTTP Verb 的 Request 对象。在 “如何使用这些Request对象来完成操作 “一节中,将进一步研究这个问题。
CoreV1Interface 中的其他方法被用来获取分组/版本中每个资源的特定方法。例如,ConfigMapsGetter 嵌入式接口的定义如下:
type ConfigMapsGetter interface {
ConfigMaps(namespace string) ConfigMapInterface
}
然后,接口 ConfigMapInterface由方法 ConfigMaps 返回,定义如下:
type ConfigMapInterface interface {
Create(
ctx context.Context,
configMap *v1.ConfigMap,
opts metav1.CreateOptions,
) (*v1.ConfigMap, error)
Update(
ctx context.Context,
configMap *v1.ConfigMap,
opts metav1.UpdateOptions,
) (*v1.ConfigMap, error)
Delete(
ctx context.Context,
name string,
opts metav1.DeleteOptions,
) error
[...]
}
你可以看到,这个接口提供了一系列的方法,每个 Kubernetes API 动词都有一个。
每个与操作相关的方法都需要一个 Option 结构体作为参数,以操作的名称命名: CreateOptions, UpdateOptions, DeleteOptions,等等。这些结构体和相关的常量都定义在这个包中:k8s.io/apimachinery/pkg/apis/meta/v1
。
最后,要对一个 Group-Version 的资源进行操作,你可以按照这个模式对 namespaced 资源进行连锁调用,其中 namespace 可以是空字符串,以表示一个集群范围的操作:
clientset.
GroupVersion().
NamespacedResource(namespace).
Operation(ctx, options)
那么,以下是不带命名空间的资源的模式:
clientset.
GroupVersion().
NonNamespacedResource().
Operation(ctx, options)
例如,使用下面的方法来列出命名空间 project1 中 core/v1 分组/版本的 Pods:
podList, err := clientset.
CoreV1().
Pods("project1").
List(ctx, metav1.ListOptions{})
要获得所有命名空间的 pod 列表,你需要指定一个空的命名空间名称:
podList, err := clientset.
CoreV1().
Pods("").
List(ctx, metav1.ListOptions{})
要获得节点的列表(这些节点是没有命名的资源),请使用这个:
nodesList, err := clientset.
CoreV1().
Nodes().
List(ctx, metav1.ListOptions{})
下面的章节详细描述了使用 Pod 资源的各种操作。在处理非命名空间的资源时,你可以通过删除命名空间参数来应用同样的例子。
检查请求
如果你想知道在调用 client-go 方法时,哪些 HTTP 请求被执行,你可以为你的程序启用日志记录。Client-go库使用klog库(https://github.com/kubernetes/klog),你可以用以下代码为你的命令启用日志标志:
import (
"flag"
"k8s.io/klog/v2"
)
func main() {
klog.InitFlags(nil)
flag.Parse()
[...]
}
现在,你可以用标志 -v <level>
来运行你的程序–例如,-v 6
来获得每个请求的URL调用。你可以在表2-1中找到更多关于定义的日志级别的细节。
创建资源
要在集群中创建一个新的资源,你首先需要使用专用的 Kind 结构体在内存中声明这个资源,然后为你要创建的资源使用创建方法。例如,使用下面的方法,在 project1 命名空间中创建一个名为 nginx-pod 的 Pod:
wantedPod := corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: "nginx",
},
},
},
}
wantedPod.SetName("nginx-pod")
createdPod, err := clientset.
CoreV1().
Pods("project1").
Create(ctx, &wantedPod, v1.CreateOptions{})
在创建资源时,用于声明 CreateOptions 结构体的各种选项是:
-
DryRun - 这表明API服务器端的哪些操作应该被执行。唯一可用的值是 metav1.DryRunAll,表示执行所有的操作,除了将资源持久化到存储。
使用这个选项,你可以得到命令的结果,即在集群中创建的确切对象,而不是真正的创建,并检查在这个创建过程中是否会发生错误。
-
FieldManager - 这表示这个操作的字段管理器的名称。这个信息将被用于未来的服务器端应用操作。
-
FieldValidation - 这表明当结构中出现重复或未知字段时,服务器应该如何反应。以下是可能的值:
metav1.FieldValidationIgnore
忽略所有重复的或未知的字段metav1.FieldValidationWarn
当出现重复或未知字段时发出警告。metav1.FieldValidationStrict
当重复字段或未知字段出现时失败。
请注意,使用这个方法,你将无法定义重复或未知的字段,因为你是使用结构体来定义对象。
如果出现错误,你可以用 k8s.io/apimachinery/pkg/api/errors
包中定义的函数测试其类型。所有可能的错误都在 “错误和状态 “一节中定义,这里是针对 Create 操作的可能错误:
-
IsAlreadyExists - 这个函数指示请求是否因为集群中已经存在同名的资源而失败:
if errors.IsAlreadyExists(err) { // ... }
-
IsNotFound - 这个函数表示你在请求中指定的命名空间是否不存在。
-
IsInvalid - 这个函数表示传入结构体的数据是否无效。
获取资源的信息
要获得集群中某一特定资源的信息,可以使用 Get 方法,从该资源中获取信息。例如,要获得 project1 命名空间中名为 nginx-pod 的 pod 的信息:
pod, err := clientset.
CoreV1().
Pods("project1").
Get(ctx, "nginx-pod", metav1.GetOptions{})
在获取资源的信息时,声明到 GetOptions 结构体中的各种选项是:
-
ResourceVersion - 请求一个不早于指定版本的资源版本。
-
如果 ResourceVersion 是 “0”,表示返回该资源的任何版本。你通常会收到资源的最新版本,但这并不保证;由于分区或陈旧的缓存,在高可用性集群上可能会收到较旧的版本。
-
如果没有设置该选项,你将保证收到资源的最新版本。
获取操作特有的可能错误是:
- IsNotFound - 这个函数表示你在请求中指定的命名空间不存在,或者指定名称的资源不存在。
获取资源列表
要获得集群中的资源列表,你可以为你想要列出的资源使用 List 方法。例如,使用下面的方法来列出 project1 命名空间中的 pods:
podList, err := clientset.
CoreV1().
Pods("project1").
List(ctx, metav1.ListOptions{})
或者,要获得所有命名空间中的 pod 列表,使用:
podList, err := clientset.
CoreV1().
Pods("").
List(ctx, metav1.ListOptions{})
在列出资源时,需要向 ListOptions 结构体声明的各种选项如下:
-
LabelSelector, FieldSelector - 这是用来按标签或按字段过滤列表的。这些选项在 “过滤列表结果 “部分有详细介绍。
-
Watch, AllowWatchBookmarks - 这是用来运行 watch 操作。这些选项在 “观察资源 “部分有详细介绍。
-
ResourceVersion, ResourceVersionMatch - 这表明你想获得哪个版本的资源列表。
请注意,当收到 List 操作的响应时,List元素本身的 ResourceVersion 值,以及List中每个元素的 ResourceVersion 值都会被指出。选项中指出的资源版本指的是 List 的资源版本。
-
对于没有分页的列表操作(你可以参考 “分页结果 “和 “观察资源” 部分,了解这些选项在其他情况下的行为):
-
当 ResourceVersionMatch 没有被设置时,其行为与Get操作相同:
-
ResourceVersion 表示你应该返回一个不比指定版本早的列表。
-
如果 ResourceVersion是 “0”,这表明有必要返回列表的任何版本。一般来说,你会收到它的最新版本,但这并不保证;在高可用性的集群上,由于分区或陈旧的缓存,收到旧版本的情况可能发生。
-
如果不设置该选项,你就能保证收到列表的最新版本。
-
当 ResourceVersionMatch 被设置为
metav1.ResourceVersionMatchExact
时,ResourceVersion 值表示你想获得的列表的确切版本。 -
将 ResourceVersion 设置为 “0”,或者不定义它,是无效的。
-
当ResourceVersionMatch设置为
metav1.ResourceVersionMatchNotOlderThan
时,ResourceVersion 表示你将获得一个不比指定版本老的列表。 -
如果ResourceVersion是 “0”,这表示将返回列表的任何版本。你通常会收到列表的最新版本,但这并不保证;在高可用性集群中,由于分区或陈旧的缓存,收到旧版本的情况可能发生。
-
不定义ResourceVersion是无效的。
-
-
TimeoutSeconds - 这将请求的持续时间限制在指定的秒数内。
-
Limit, Continue - 这用于对列表的结果进行分页。这些选项在第二章的 “分页结果 “部分有详细说明。
以下是 List 操作特有的可能错误:
- IsResourceExpired - 这个函数表示指定的 ResourceVersion 与 ResourceVersionMatch,设置为
metav1.ResourceVersionMatchExact
,已经过期。
注意,如果你为 List 操作指定一个不存在的命名空间,你将不会收到 NotFound 错误。
筛选列表的结果
正如第2章 “过滤列表结果"一节所述,可以用标签选择器和字段选择器来过滤列表操作的结果。本节展示了如何使用API Machinery 库的字段和标签包来创建一个适用于 LabelSelector 和 FieldSelector 选项的字符串。
使用标签包设置LabelSelector
下面是使用API Machinery 库的 labels 包的必要导入信息。
import (
"k8s.io/apimachinery/pkg/labels"
)
该包提供了几种建立和验证 LabelsSelector 字符串的方法:使用 Requirements,解析 labelSelector 字符串,或使用一组键值对。
使用 Requirements
你首先需要使用下面的代码创建一个 label.Selector
对象:
labelsSelector := labels.NewSelector()
然后,你可以使用 labs.NewRequirement
函数创建 Requirement
对象:
func NewRequirement(
key string,
op selection.Operator,
vals []string,
opts ...field.PathOption,
) (*Requirement, error)
op 的可能值的常量在 selection 包中定义(即 k8s.io/apimachinery/pkg/selection
)。vals 字符串数组中的值的数量取决于操作:
-
selection.In; selection.NotIn - 附加到 key 的值必须等于(In)/必须不等于(NotIn)vals定义的值中的一个。
vals必须不是空的。
-
selection.Equals; selection.DoubleEquals; selection.NotEquals - 附加到key的值必须等于(Equals, DoubleEquals)或者不等于(NotEquals)vals中定义的值。
vals必须包含一个单一的值。
-
selection.Exists; selection.DoesNotExist - 键必须被定义(Exists)或必须不被定义(DoesNotExist)。
vals必须是空的。
-
selection.Gt; selection.Lt - 附加在键上的值必须大于(Gt)或小于(Lt)vals中定义的值。
vals必须包含一个单一的值,代表一个整数。
例如,为了要求键 mykey 的值等于 value1,你可以声明 Requirement:
req1, err := labels.NewRequirement(
"mykey",
selection.Equals,
[]string{"value1"},
)
在定义 Requirement 后,你可以使用选择器上的 Add 方法将需求添加到选择器中:
labelsSelector = labelsSelector.Add(*req1, *req2)
最后,你可以用以下方法获得 LabelSelector 选项所要传递的字符串:
s := labelsSelector.String()
解析 LabelSelector 字符串
如果你已经有一个描述标签选择器的字符串,你可以用 Parse 函数检查其有效性。Parse 函数将验证该字符串并返回一个 LabelSelector 对象。您可以在这个 LabelSelector 对象上使用 String 方法来获得由 Parse 函数验证的字符串。
作为一个例子,下面的代码将解析、验证并返回标签选择器的典型形式,“mykey = value1, count < 5”:
selector, err := labels.Parse(
"mykey = value1, count < 5",
)
if err != nil {
return err
}
s := selector.String()
// s = "mykey=value1,count<5"
使用键值对的集合
当你只想使用等价(Equal)操作时,可以使用 ValidatedSelectorFromSet 这个函数,以满足一个或几个要求:
func ValidatedSelectorFromSet(
ls Set
) (Selector, error)
在这种情况下,Set 将定义你想检查的键值对的集合,以确保等价(Equal)。
作为一个例子,下面的代码将声明一个标签选择器,要求键 key1,等于value1,键 key2,等于value2:
set := labels.Set{
"key1": "value1",
"key2": "value2",
}
selector, err = labels.ValidatedSelectorFromSet(set)
s = selector.String()
// s = "key1=value1,key2=value2"
使用 Fields 包设置 Fieldselector
下面是用于从 API Machinery 中导入 Fields 包的必要代码。
import (
"k8s.io/apimachinery/pkg/fields"
)
该包提供了几种建立和验证 FieldSelector 字符串的方法:组装术语选择器(term selector),解析 fieldSelector 字符串,或使用一组键值对。
组装术语选择器
你可以用函数 OneTermEqualSelector 和 OneTermNotEqualSelector 创建一个术语选择器,然后用函数 AndSelectors 组装选择器来建立一个完整的字段选择器。
func OneTermEqualSelector(
k, v string,
) Selector
func OneTermNotEqualSelector(
k, v string,
) Selector
func AndSelectors(
selectors ...Selector,
) Selector
例如,这段代码建立了一个字段选择器,在字段 status.Phase
上有一个 Equal 条件,在字段 spec.restartPolicy
上有一个 NotEqual 条件:
fselector = fields.AndSelectors(
fields.OneTermEqualSelector(
"status.Phase",
"Running",
),
fields.OneTermNotEqualSelector(
"spec.restartPolicy",
"Always",
),
)
fs = fselector.String()
解析字段选择器字符串
如果你已经有一个描述字段选择器的字符串,你可以用 ParseSelector 或 ParseSelectorOrDie 函数检查其有效性。ParseSelector 函数将验证该字符串并返回一个 fields.Selector 对象。你可以在这个 fields.Selector 对象上使用 String 方法来获得由 ParseSelector 函数验证的字符串。
作为一个例子,这段代码将解析、验证并返回字段选择器的典型形式 “status.Phase = Running, spec.restartPolicy != Always”:
selector, err := fields.ParseSelector(
"status.Phase=Running, spec.restartPolicy!=Always",
)
if err != nil {
return err
}
s := selector.String()
// s = "spec.restartPolicy!=Always,status.Phase=Running"
使用键值对的集合
当你想对一个或几个单一的选择器只使用等价操作时,可以使用SelectorFromSet这个函数。
func SelectorFromSet(ls Set) Selector
在这种情况下,Set将定义你要检查的键值对的集合,以确保平等。
作为一个例子,下面的代码将声明一个字段选择器,要求键 key1 等于 value1,键 key2,等于value2:
set := fields.Set{
"field1": "value1",
"field2": "value2",
}
selector = fields.SelectorFromSet(set)
s = selector.String()
// s = "key1=value1,key2=value2"
删除资源
要从集群中删除资源,可以对你要删除的资源使用删除方法。例如,要从 project1 命名空间中删除一个名为 nginx-pod 的 Pod,可以使用:
err = clientset.
CoreV1().
Pods("project1").
Delete(ctx, "nginx-pod", metav1.DeleteOptions{})
请注意,不保证操作终止时资源被删除。删除操作不会有效地删除资源,但会标记资源被删除(通过设置字段 .metadata.deletionTimestamp
),并且删除将以异步方式发生。
DryRun - 这表明API服务器端的哪些操作应该被执行。唯一可用的值是 metav1.DryRunAll
,表示要执行所有的操作,除了(将资源持久化到存储的操作)。使用这个选项,你可以得到命令的结果,而不是真的删除资源,并检查在这个删除过程中是否会发生错误。
GracePeriodSeconds - 这个值只在删除 pod 时有用。它表示在删除 pod 之前的持续时间,单位是秒。
该值必须是一个指向非负整数的指针。值为零表示立即删除。如果这个值为 nil,将使用 pod 的默认宽限期,如 pod spec 中的TerminationGracePeriodSeconds 字段所示。
你可以使用 metav1.NewDeleteOptions
函数来创建一个定义了 GracePeriodSeconds的DeleteOptions 的结构体:
err = clientset.
CoreV1().
Pods("project1").
Delete(ctx,
"nginx-pod",
*metav1.NewDeleteOptions(5),
)
Preconditions(前提条件) - 当你删除一个对象时,你可能想确保删除预期的对象。前提条件字段让你指出你期望删除的资源,可以通过以下方式:
-
指明UID,所以如果预期的资源被删除,而另一个资源被创建了相同的名字,那么删除将失败,产生一个冲突错误。你可以使用
metav1.NewPreconditionDeleteOptions
函数来创建一个 DeleteOptions 结构体,并设置 Preconditions 的UID:uid := createdPod.GetUID() err = clientset. CoreV1(). Pods("project1"). Delete(ctx, "nginx-pod", *metav1.NewPreconditionDeleteOptions( string(uid), ), ) if errors.IsConflict(err) { [...] }
-
指定 ResourceVersion,所以如果在此期间资源被更新,删除将失败,并出现 Conflict 错误。你可以使用
metav1.NewRVDeletionPrecondition
函数来创建一个 DeleteOptions 结构体,并设置前提条件的 ResourceVersion:rv := createdPod.GetResourceVersion() err = clientset. CoreV1(). Pods("project1"). Delete(ctx, "nginx-pod", *metav1.NewRVDeletionPrecondition( rv, ), ) if errors.IsConflict(err) { [...] }
OrphanDependents - 这个字段已被废弃,转而使用 PropagationPolicy。
PropagationPolicy - 这表明是否以及如何进行垃圾收集。参见第三章的 “OwnerReferences” 部分。可接受的值是:
-
metav1.DeletePropagationOrphan
- 向Kubernetes API表示将你正在删除的资源所拥有的资源变成孤儿,这样它们就不会被垃圾收集器删除。 -
metav1.DeletePropagationBackground
- 指示Kubernetes API在所有者资源被标记为删除后立即返回删除操作,而不是等待拥有的资源被垃圾收集器删除。 -
metav1.DeletePropagationForeground
- 指示 Kubernetes API 在所有者和 BlockOwnerDeletion 设置为 true 的自有资源被删除后,从 Delete 操作中返回。Kubernetes API将不会等待其他拥有的资源被删除。
以下是删除操作特有的可能错误:
- IsNotFound - 这个函数表示你在请求中指定的资源或命名空间不存在。
- IsConflict - 这个函数表示请求失败,因为一个前提条件没有被遵守(UID或ResourceVersion)。
删除资源集合
要从集群中删除资源集合,你可以为你要删除的资源使用 DeleteCollection 方法。例如,要从 project1 命名空间中删除 Pod 的集合:
err = clientset.
CoreV1().
Pods("project1").
DeleteCollection(
ctx,
metav1.DeleteOptions{},
metav1.ListOptions{},
)
必须向该函数提供两组选项:
- DeleteOptions,表示对每个对象进行删除操作的选项,如 “删除资源” 部分所述。
- ListOptions,细化要删除的资源集合,如 “获取资源列表” 部分所述。
更新资源
要更新集群中的资源,你可以为你要更新的资源使用更新方法。例如,使用以下方法来更新 project1 命名空间中的 deployment:
updatedDep, err := clientset.
AppsV1().
Deployments("project1").
Update(
ctx,
myDep,
metav1.UpdateOptions{},
)
当更新资源时,要声明到 UpdateOptions 结构体中的各种选项,与 “创建资源"一节中描述的 CreateOptions 中的选项相同。
更新操作可能出现的特定错误是:
- IsInvalid - 这个函数表示传递到结构中的数据是无效的。
- IsConflict(冲突)–该函数表示纳入结构中的 ResourceVersion(这里是 myDep)比集群中的版本要早。更多信息请参见第2章的 “更新资源管理冲突” 部分。
使用 Strategic Merge Patch 来更新资源
在第二章 “使用Strategic Merge Patch(战略合并补丁)更新资源 “一节中,你已经看到了用战略合并补丁对资源进行修补的过程。总而言之,你需要:
-
使用 “Patch” 操作:
-
为 content-type 头指定特定的值
-
在正文中传递你想修改的唯一字段
使用 Client-go 库,你可以对你要修补的资源使用 Patch 方法。
Patch(
ctx context.Context,
name string,
pt types.PatchType,
data []byte,
opts metav1.PatchOptions,
subresources ...string,
) (result *v1.Deployment, err error)
PatchType 表明你是想使用 StrategicMerge patch(types.StrategicMergePatchType
)还是合并补丁(types.MergePatchType
)。这些常数在k8s.io/apimachinery/pkg/types
包中定义。
data 字段包含你想应用到资源的补丁。你可以直接写这个补丁数据,就像在第二章中做的那样,或者你可以使用 controller-runtime 的以下功能来帮助你建立这个补丁。这个库将在第10章中进行更深入的探讨。
import "sigs.k8s.io/controller-runtime/pkg/client"
func StrategicMergeFrom(
obj Object,
opts ...MergeFromOption,
) Patch
StrategicMergeFrom 函数的第一个参数接受一个 Object 类型,代表任何 Kubernetes 对象。你将通过这个参数传递你想要修补的对象,在任何改变之前。
然后,该函数接受一系列的选项。目前唯一接受的选项是 client.MergeFromWithOptimisticLock{}
值。这个值要求库将 ResourceVersion 添加到补丁数据中,因此服务器将能够检查你要更新的资源版本是否是最后一个。
在你使用 StrategicMergeFrom 函数创建了 Patch 对象后,你可以创建你想打补丁的对象的深度拷贝,然后修改它。然后,当你完成更新对象后,你可以用 Patch 对象的专用数据方法建立补丁的数据。
作为例子,要为 Deploymen 建立补丁数据,包含乐观锁的资源版本(ResourceVersion),你可以使用下面的代码(createdDep 是一个反映在集群中创建的Deployment 的结构体):
patch := client.StrategicMergeFrom(
createdDep,
pkgclient.MergeFromWithOptimisticLock{},
)
updatedDep := createdDep.DeepCopy()
updatedDep.Spec.Replicas = pointer.Int32(2)
patchData, err := patch.Data(updatedDep)
// patchData = []byte(`{
// "metadata":{"resourceVersion":"4807923"},
// "spec":{"replicas":2}
// }`)
patchedDep, err := clientset.
AppsV1().Deployments("project1").Patch(
ctx,
"dep1",
patch.Type(),
patchData,
metav1.PatchOptions{},
)
注意 MergeFrom 和 MergeFromWithOptions 函数也是可用的,如果你喜欢执行一个合并补丁。
Patch 对象的 Type 方法可以用来检索补丁类型,而不是使用类型包中的常量。你可以在调用补丁操作时传递 PatchOptions。可能的选项有:
-
DryRun - 这表明API服务器端的哪些操作应该被执行。唯一可用的值是
metav1.DryRunAll
,表示执行所有操作,除了将资源持久化到存储。 -
Force - 这个选项只能用于 Apply patch 请求,在处理 StrategicMergePatch 或 MergePatch 请求时必须取消设置。
-
FieldManager - 这表示该操作的字段管理器的名称。这个信息将被用于未来的服务器端 Apply 操作。这个选项对于 StrategicMergePatch 或 MergePatch 请求是可选的。
-
FieldValidation - 这表明当结构体中出现重复或未知字段时,服务器应该如何反应。以下是可能的值:
- metav1.FieldValidationIgnore - 忽略所有重复的或未知的字段
- metav1.FieldValidationWarn - 当出现重复或未知字段时发出警告
- metav1.FieldValidationStrict - 当出现重复字段或未知字段时失败。
注意,Patch 操作接受 subresources 参数。这个参数可以用来修补应用补丁方法的资源的子资源。例如,要修补一个 Deployment 的 Status,你可以使用subresources 参数的值 “status”。
MergePatch 操作特有的可能的错误是:
- IsInvalid - 这个函数指示作为补丁传递的数据是否无效。
- IsConflict - 这个函数表示并入补丁的资源版本(如果你在构建补丁数据时使用优化锁)是否比集群中的版本更早。更多信息可在第二章 “更新资源管理冲突 “部分找到。
用补丁在服务器端应用资源
第二章的 “在服务器端应用资源” 部分描述了服务器端应用补丁是如何工作的。总而言之,我们需要:
-
使用 “补丁 “操作
-
为 content-type 头指定一个特定的值
-
在正文中传递你想修改的唯一字段
-
提供一个 fieldManager 名称
使用 Client-go 库,你可以对你要修补的资源使用 Patch 方法。注意,你也可以使用 Apply 方法;见下一节,“使用Apply在服务器端应用资源”。
Patch(
ctx context.Context,
name string,
pt types.PatchType,
data []byte,
opts metav1.PatchOptions,
subresources ...string,
) (result *v1.Deployment, err error)
PatchType 表示补丁的类型,这里是 type.ApplyPatchType
,定义于 k8s.io/apimachinery/pkg/types
包。
data 字段包含你想应用到资源的补丁。你可以使用 client.Apply 值来构建这个数据。这个值实现了 client.Patch 接口,提供了Type和Data方法。
注意,你需要在你想打补丁的资源结构体中设置 APIVersion 和 Kind 字段。还要注意,这个 Apply 操作也可以用来创建资源。
补丁操作接受 subresources 参数。这个参数可以用来修补应用Patch方法的资源的子资源。例如,要修补 Deployment 的 Status,你可以使用 subresources 参数的值 “status”。
import "sigs.k8s.io/controller-runtime/pkg/client"
wantedDep := appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Replicas: pointer.Int32(1),
[...]
}
wantedDep.SetName("dep1")
wantedDep.APIVersion, wantedDep.Kind =
appsv1.SchemeGroupVersion.
WithKind("Deployment").
ToAPIVersionAndKind()
patch := client.Apply
patchData, err := patch.Data(&wantedDep)
patchedDep, err := clientset.
AppsV1().Deployments("project1").Patch(
ctx,
"dep1",
patch.Type(),
patchData,
metav1.PatchOptions{
FieldManager: "my-program",
},
)
你可以在调用 Patch 操作时传递 PatchOptions。以下是可能的选项:
- DryRun - 这表明API服务器端的哪些操作应该被执行。唯一可用的值是
metav1.DryRunAll
,表示执行所有操作,除了将资源持久化到存储。 - Force - 这个选项表示强制应用请求。这意味着这个请求的字段管理器将获得其他字段管理器所拥有的冲突字段。
- FieldManager - 这表示该操作的字段管理器的名称。这个信息将被用于未来的服务器端 Apply 操作。
- FieldValidation - 这表明当结构体中出现重复或未知字段时,服务器应该如何反应。以下是可能的值:
- metav1.FieldValidationIgnore - 忽略所有重复的或未知的字段
- metav1.FieldValidationWarn - 当出现重复或未知字段时发出警告
- metav1.FieldValidationStrict - 当出现重复字段或未知字段时失败。
ApplyPatch 操作特有的可能的错误是:
- IsInvalid - 这个函数指示作为补丁传递的数据是否无效。
- IsConflict - 这个函数表示被补丁修改的一些字段是否有冲突,因为它们被另一个字段管理器拥有。为了解决这个冲突,你可以使用强制选项,这样这些字段就会被这个操作的字段管理器获得。
Server-side Apply Using Apply Configurations
TBD
Building an ApplyConfiguration from Scratch
TBD
Building an ApplyConfiguration from an Existing Resource
监视资源
第二章的 “监视资源 “部分描述了 Kubernetes API 如何观察资源。使用 Client-go 库,你可以对你想观察的资源使用Watch方法。
Watch(
ctx context.Context,
opts metav1.ListOptions,
) (watch.Interface, error)
这个 Watch 方法返回一个实现了 watch.Interface 接口的对象,并提供以下方法:
import "k8s.io/apimachinery/pkg/watch"
type Interface interface {
ResultChan() <-chan Event
Stop()
}
ResultChan 方法返回一个 Go通道(只能读取),你将能够接收所有的事件。
Stop 方法将停止 Watch 操作并关闭使用 ResultChan 接收的通道。
使用通道接收的 watch.Event 对象的定义如下:
type Event struct {
Type EventType
Object runtime.Object
}
Type 字段可以得到第2章表2-2中早先描述的值,你可以在 watch 包中找到这些不同值的常量:watch.Added, watch.Modified, watch.Deleted, watch.Bookmark, 和 watch.Error。
Object 字段实现了 runtime.Object 接口,它的具体类型可以根据 Type 的值而不同。
对于除 Error 以外的类型,Object 的具体类型将是你正在监视的资源的类型(例如,如果你正在监视 Deployment,则是 Deployment 类型)。
对于 Error 类型,具体类型通常是 metav1.Status
,但它可以是任何其他类型,取决于你正在观察的资源。作为一个例子,这里有一段观察部署的代码:
import "k8s.io/apimachinery/pkg/watch"
watcher, err := clientset.AppsV1().
Deployments("project1").
Watch(
ctx,
metav1.ListOptions{},
)
if err != nil {
return err
}
for ev := range watcher.ResultChan() {
switch v := ev.Object.(type) {
case *appsv1.Deployment:
fmt.Printf("%s %s\n", ev.Type, v.GetName())
case *metav1.Status:
fmt.Printf("%s\n", v.Status)
watcher.Stop()
}
}
在观察资源时,需要在 ListOptions 结构体中声明的各种选项如下:
-
LabelSelector, FieldSelector - 这是用来过滤按标签或按字段观察的元素。这些选项在 “过滤列表结果” 部分有详细说明。
-
Watch, AllowWatchBookmarks - Watch 选项表示正在运行一个观察操作。这个选项是在执行 Watch 方法时自动设置的;你不需要明确地设置它。
-
AllowWatchBookmarks 选项要求服务器定期返回 Bookmarks。书签的使用在第二章的 “允许书签有效地重启观察请求” 一节中有所描述。
-
ResourceVersion, ResourceVersionMatch - 这表明你想在资源列表的哪个版本上开始观察操作。
请注意,当收到 List 操作的响应时,会为列表元素本身指出一个ResourceVersion值,以及列表中每个元素的ResourceVersion值。选项中指出的ResourceVersion是指列表的ResourceVersion。
-
ResourceVersionMatch 选项不用于观察操作。对于观察操作,请执行以下操作:
-
当 ResourceVersion 没有设置时,API将从最近的资源列表开始观察操作。该通道首先接收 ADDED 事件以声明资源的初始状态,然后在集群上发生变化时接收其他事件。
-
当 ResourceVersion 被设置为一个特定的版本时,API将从资源列表的指定版本开始观察操作。该通道将不接收声明资源初始状态的 ADDED 事件,而只接收该版本之后集群上发生变化时的事件(可以是指定版本和你运行Watch操作之间发生的事件)。
-
一个用例是观察一个特定资源的删除情况。为此,你可以
1. 列出资源,包括你想删除的那个,并保存收到的列表的ResourceVersion。
2. 对资源执行删除操作(删除是异步的,当操作终止时,资源可能不会被删除)。
3. 通过指定在步骤1中收到的ResourceVersion,启动一个Watch操作。即使删除发生在步骤2和步骤3之间,你也能保证收到DELETED事件。
-
-
当 ResourceVersion 被设置为 “0 “时,API将在任何资源列表中启动Watch操作。该通道首先接收ADDED事件,以声明资源的初始状态,然后在这个初始状态之后集群上发生变化时接收其他事件。
在使用这种语义时,你必须特别小心,因为Watch操作通常会从最新的版本开始;但是,从较早的版本开始也是可能的。
-
TimeoutSeconds - 这将请求的持续时间限制在指定的秒数内。
-
Limit, Continue - 这用于对列表操作的结果进行分页。这些选项不支持观察操作。
注意,如果你为 Watch 操作指定一个不存在的命名空间,你将不会收到 NotFound 错误。
还要注意的是,如果你指定了过期的 ResourceVersion,你在调用 Watch 方法时不会收到错误,但会得到ERROR事件,其中包含 metav1.Status 对象,表示一个Reason的值 metav1.StatusReasonExpired
。
metav1.Status 是一个基础对象,用来构建使用客户集的调用所返回的错误。你将能够在 “错误和状态” 部分了解更多。
错误和状态
如第一章所示,Kubernetes API 定义了 Kinds 来与调用者交换数据。目前,你应该考虑 Kinds 与资源有关,要么 Kind 有资源的单数名称(如Pod),要么 Kind 为资源列表(如PodList)。当一个 API 操作既没有返回资源也没有返回资源列表时,它使用一个普通的Kind,metav1.Status
,来表示操作的状态。
metav1.Status结构体的定义
metav1.Status
结构体的定义如下:
type Status struct {
Status string
Message string
Reason StatusReason
Details *StatusDetails
Code int32
}
-
Status - 这表示操作的状态,是
metav1.StatusSuccess
或metav1.StatusFailure
。 -
Message - 这是对操作状态的自由形式的人类可读描述。
-
Code - 这表示为操作返回的 HTTP 状态代码。
-
Reason(原因)–这表示操作处于失败状态的原因。原因与给定的HTTP状态代码有关。定义的原因有:
-
StatusReasonBadRequest (400) - 这个请求本身是无效的。这与
StatusReasonInvalid
不同,后者表明 API 调用可能成功,但数据无效。回复StatusReasonBadRequest 的请求永远不可能成功,无论数据如何。 -
StatusReasonUnauthorized (401) - 授权凭证丢失、不完整或无效。
-
StatusReasonForbidden (403) - 授权证书是有效的,但对资源的操作对这些证书是禁止的。
-
StatusReasonNotFound (404) - 请求的资源或资源无法找到。
-
StatusReasonMethodNotAllowed (405) - 在资源中请求的操作是不允许的,因为它没有实现。一个回复StatusReasonMethodNotAllowed的请求永远不会成功,不管是什么数据。
-
StatusReasonNotAcceptable (406) - 客户端在 Accept 头中指出的接受类型都不可能。回复 StatusReasonNotAcceptable 的请求永远不会成功,无论数据如何。
-
StatusReasonAlreadyExists (409) - 正在创建的资源已经存在。
-
StatusReasonConflict (409) - 由于冲突,请求无法完成–例如,由于操作试图用旧的资源版本更新资源,或者由于删除操作中的前提条件没有被遵守。
-
StatusReasonGone (410) - 项目已不再可用。
-
StatusReasonExpired (410) - 内容已经过期,不再可用–例如,当用过期的资源版本执行List或Watch操作时。
-
StatusReasonRequestEntityTooLarge (413) - 请求实体太大。
-
StatusReasonUnsupportedMediaType (415) - 此资源不支持 Content-Type 标头中的内容类型。回复 StatusReasonUnsupportedMediaType 的请求永远不会成功,不管是什么数据。
-
StatusReasonInvalid (422) - 为创建或更新操作发送的数据是无效的。Causes字段列举了数据的无效字段。
-
StatusReasonTooManyRequests (429) - 客户端应该至少等待 Details 字段 RetryAfterSeconds 中指定的秒数,才能再次执行操作。
-
StatusReasonUnknown (500) - 服务器没有指出任何失败的原因。
-
StatusReasonServerTimeout (500) - 可以到达服务器并理解请求,但不能在合理时间内完成操作。客户端应该在 Details 字段 RetryAfterSeconds 中指定的秒数后重试该请求。
-
StatusReasonInternalError (500) - 发生了一个内部错误;它是意料之外的,调用的结果是未知的。
-
StatusReasonServiceUnavailable (503) - 请求是有效的,但是所请求的服务在这个时候不可用。一段时间后重试该请求可能会成功。
-
StatusReasonTimeout (504) - 在请求中指定的超时时间内不能完成操作。如果指定了 Details 字段的RetryAfterSeconds字段,客户端应该在再次执行该操作之前等待这个秒数。
-
-
Details – 这些可以包含更多关于原因的细节,取决于 Reason 字段。
Details 字段的 StatusDetails 类型定义如下:
type StatusDetails struct {
Name string
Group string
Kind string
UID types.UID
Causes []StatusCause
RetryAfterSeconds int32
}
-
如果指定的话,Name、Group、Kind 和 UID 字段表明哪个资源受到了故障的影响。
-
RetryAfterSeconds 字段,如果指定的话,表示客户端在再次执行操作之前应该等待多少秒。
-
Causes 字段列举了失败的原因。当执行创建或更新操作导致 StatusReasonInvalid 原因的失败时,Causes 字段列举了无效的字段和每个字段的错误类型。
-
Causes 字段的 StatusCause 类型定义如下:
type StatusCause struct { Type CauseType Message string Field string }
CllientSet 操作返回的错误
本章前面包含了对 Clientset 提供的各种操作的描述,这些操作一般会返回一个错误,你可以使用 errors 包中的函数来测试错误的原因–例如,用 IsAlreadyExists 这个函数。
这些错误的具体类型是 errors.StatusError
,定义为:
type StatusError struct {
ErrStatus metav1.Status
}
可以看出,这个类型只包括本节前面已经探讨过的 metav1.Status
结构体。为这个 StatusError 类型提供了函数来访问底层的Status。
Is<ReasonValue>(err error) bool
- 本节前面列举的每个 Reason 值都有一个,表示错误是否属于特定状态。FromObject(obj runtime.Object) error
- 当你在 Watch 操作中接收到metav1.Status
时,你可以用这个函数建立一个 StatusError 对象。(e *StatusError) Status() metav1.Status
- 返回基础状态。ReasonForError(err error) metav1.StatusReason
- 返回基础状态的原因。HasStatusCause(err error, name metav1.CauseType) bool
- 这表明一个错误是否声明了一个特定的原因,并给出了CauseType。StatusCause(err error, name metav1.CseType) (metav1.StatusCause, bool)
- 如果给定的CauseType存在,返回该原因,否则返回false。SuggestsClientDelay(err error) (int, bool)
- 这表明错误是否在状态的RetryAfterSeconds字段中指示了一个值以及该值本身。
RESTClient
在本章前面的 “使用clientset” 部分,你可以为 Kubernetes API 的每个分组/版本获得一个 REST 客户端。例如,下面的代码返回 Core/v1
分组的REST客户端:
restClient := clientset.CoreV1().RESTClient()
restClient 对象实现了 rest.Interface 接口,定义如下:
type Interface interface {
GetRateLimiter() flowcontrol.RateLimiter
Verb(verb string) *Request
Post() *Request
Put() *Request
Patch(pt types.PatchType) *Request
Get() *Request
Delete() *Request
APIVersion() schema.GroupVersion
}
在这个接口中,你可以看到通用方法 Verb 和辅助方法 Post、Put、Patch、Get 和 Delete,它们返回 Request 对象。
构建Request
Request 结构体只包含私有字段,它提供了对 Request 进行个性化处理的方法。如第1章所示,Kubernetes 资源或子资源的路径形式(根据操作和资源的不同,有些段可能不存在)如下:
/apis/<group>/<version>
/namespaces/<namesapce_name>
/<resource>
/<resource_name>
/<subresource>
以下方法可以用来建立这个路径。注意,
-
Namespace(namespace string) *Request;
NamespaceIfScoped(namespace string, scoped bool) *Request
- 这些表明要查询的资源的名称空间。NamespaceIfScoped 只有在请求被标记为作用域时才会添加命名空间部分。 -
Resource(resource string) *Request
- 这表示要查询的资源。 -
Name(resourceName string) *Request
- 这表示要查询的资源的名称。 -
SubResource(subresources ...string) *Request
- 这表示要查询的资源的子资源。 -
Prefix(segments ...string) *Request; Suffix(segments ...string) *Request
- 在请求路径的开头或结尾添加段。前缀段将被添加到 “命名空间 “段之前。后缀段将被添加到子资源段之后。对这些方法的新调用将在现有的基础上增加前缀和后缀。 -
AbsPath(segments ...string) *Request
- 用所提供的段重设前缀。
下面的方法用查询参数、正文和头文件完成请求:
TBD
执行请求
一旦建立了Request,我们就可以执行它。可以使用Request对象上的以下方法:
Do(ctx context.Context) Result
- 这将执行请求并返回 Result 对象。我们将在下一节看到如何利用这个结果对象。Watch(ctx context.Context) (watch.Interface, error)
- 在请求的位置上执行 Watch 操作,并返回实现watch.Interface
接口的对象,用来接收事件。你可以参阅本章的 “观察资源"一节,了解如何使用返回的对象。Stream(ctx context.Context) (io.ReadCloser, error)
- 这将执行请求,并通过 ReadCloser 将结果体流出来。DoRaw(ctx context.Context) ([]byte, error)
- 这将执行请求并将结果作为字节数组返回。
对结果的利用
当你在 Request 上执行 Do() 方法时,该方法返回 Result 对象。
结果结构没有任何公共字段。以下方法可以用来获取结果的信息:
Into(obj runtime.Object) error
- 如果可能的话,这将解码并将结果体的内容存储到对象中。作为参数传递的对象的具体类型必须与正文中定义的类型相匹配。同时返回执行请求的错误。Error() error
- 这将返回执行请求的错误。这个方法在执行一个没有返回正文内容的请求时很有用。Get() (runtime.Object, error)
- 这个方法将结果体的内容解码并作为一个对象返回。返回对象的具体类型将与正文中定义的类型相匹配。同时返回执行请求的错误。Raw() ([]byte, error)
- 这将返回作为字节数组的body,以及执行请求的错误。StatusCode(statusCode *int) Result
- 这将状态代码存储到传递的参数中,并返回结果,因此该方法可以被链起来。WasCreated(wasCreated *bool) Result
- 这存储了一个值,表明请求创建的资源是否已经被成功创建,并返回结果,因此该方法可以被链起来。Warnings() []net.WarningHeader
- 这将返回包含在Result中的Warnings列表。
以表格形式获取结果
TBD
发现客户端
Kubernetes API 提供了发现 API 所提供的资源的端点。kubectl 正在使用这些端点来显示 kubectl api-resources 命令的结果(图6-1)。
客户端可以通过调用 clientset 上的 Discovery() 方法来获得(参见第6章 “获得clientset” 部分中如何获得 clientset),或者使用 discovery 包提供的函数。
import "k8s.io/client-go/discovery"
所有这些函数,都希望有一个 rest.Config
,作为一个参数。你可以在第6章的 “连接到集群” 部分看到如何获得这样一个 rest.Config
对象。
-
NewDiscoveryClientForConfig 将返回一个使用所提供的
rest.Config
的 DiscoveryClient。NewDiscoveryClientForConfig( c *rest.Config, ) (*DiscoveryClient, error)
-
NewDiscoveryClientForConfigOrDie 与前一个函数类似,但在出错的情况下会 panic ,而不是返回错误。这个函数可以用在一个硬编码的配置上,我们要断言它的有效性。
NewDiscoveryClientForConfigOrDie( c *rest.Config, ) *DiscoveryClient
-
NewDiscoveryClientForConfigAndClient
NewDiscoveryClientForConfigAndClient( c *rest.Config, httpClient *http.Client, ) (*DiscoveryClient, error)
之前的函数 NewDiscoveryClientForConfig 使用了一个用函数
rest.HTTPClientFor
构建的默认HTTP客户端。如果你想在构建DiscoveryClient
之前个性化HTTP客户端,你可以使用这个函数来代替。
RESTMapper
TBD
总结
在本章中,你已经看到了如何连接到集群以及如何获得 Clientset。它是一组客户端,每个 Group-Version 都有一个,你可以用它来执行对资源的操作(获取、列表、创建等)。
你还涵盖了 REST 客户端,在内部由 Clientset 使用,开发者可以用它来建立更具体的请求。最后,本章介绍了 Discovery 客户端,用于以动态方式发现Kubernetes API 提供的资源。
下一章介绍了如何测试用 Client-go 库编写的应用程序,使用它所提供的客户端的 fack 实现。
5.7 - [第7章]测试使用Client-go的应用程序
Client-go 库提供了一些可以与 Kubernetes API 一起行动的客户端。
-
kubernetes.Clientset
提供了一组客户端,每个分组/版本的API都有一个,用于执行对资源的Kubernetes操作(创建、更新、删除等)。 -
rest.RESTClient 提供了一个客户端,用于对资源执行REST操作(获取、发布、删除等)。
-
discovery.DiscoveryClient 提供了一个客户端来发现由API提供的资源。
所有这些客户端都实现了由 Client-go 库定义的接口:kubernetes.Interface、rest.Interface 和 discovery.DiscoveryInterface。
此外,Client-go库提供了这些接口的假实现,以帮助你为你的功能编写单元测试。这些假的实现被定义在 fack 包中,每个包都位于真实实现的目录内:kubernetes/fake、rest/fake 和 discovery/fake。
testing 目录包含 fack 客户端使用的常用工具–例如,对象跟踪器或跟踪调用的系统。你将在本章中了解到这些工具。
为了能够使用这些客户端测试你的函数,函数需要定义一个参数来传递客户端的实现,而且参数的类型必须是接口,而不是具体类型。比如说:
func CreatePod(
ctx context.Context,
clientset kubernetes.Interface,
name string,
namespace string,
image string,
) (pod *corev1.Pod, error)
这样,你将在函数之外创建客户端,并简单地在函数内使用任何实现。
对于真正的代码,你将创建客户端,如第6章所定义。在测试中,你将使用 fack 包中的辅助函数来替代客户端。
fack clientset
下面的函数用于创建 fack clientset :
import "k8s.io/client-go/kubernetes/fake"
func NewSimpleClientset(
objects ...runtime.Object,
) *Clientset
fack clientset 是由一个对象跟踪器支持的,它处理创建、更新和删除操作,没有任何验证或突变,并且它在响应获取和列表操作时返回对象。
你可以把 Kubernetes 对象的列表作为参数传递,这些对象将被添加到 clientset 的对象跟踪器中。例如,使用下面的方法来创建一个 fack clientset,并调用前面定义的 CreatePod 函数:
import "k8s.io/client-go/kubernetes/fake"
clientset := fake.NewSimpleClientset()
pod, err := CreatePod(
context.Background(),
clientset,
aName,
aNs,
anImage,
)
在测试过程中,当你调用被测试的函数(在本例中是 CreatePod)后,你有几种方法来验证该函数是否完成了你所期望的。让我们考虑一下 CreatePod 函数的这个实现:
func CreatePod(
ctx context.Context,
clientset kubernetes.Interface,
name string,
namespace string,
image string,
) (pod *corev1.Pod, err error) {
podToCreate := corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "runtime",
Image: image,
},
},
},
}
podToCreate.SetName(name)
return clientset.CoreV1().
Pods(namespace).
Create(
ctx,
&podToCreate,
metav1.CreateOptions{},
)
}
检查函数的结果
当用 fack clientset 调用 CreatePod
函数时,实际的 Kubernetes API 将不会被调用,资源不会在 etcd 数据库中生成,也不会对资源进行验证和变异。相反,资源将被原封不动地存储在内存存储中,只进行最小的转换。
在这个例子中,传递给 Create 函数的 podToCreate 和 Create 函数返回的 pod 之间的唯一转换是命名空间,它通过调用传递到 Pods(namespace),并被添加到返回的 Pod 中。
为了测试 CreatePod 函数返回的值是否是你所期望的,你可以编写以下测试:
func TestCreatePod(t *testing.T) {
var (
name = "a-name"
namespace = "a-namespace"
image = "an-image"
wantPod = &corev1.Pod{
ObjectMeta: v1.ObjectMeta{
Name: "a-name",
Namespace: "a-namespace",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "runtime",
Image: "an-image",
},
},
},
}
)
clientset := fake.NewSimpleClientset()
gotPod, err := CreatePod(
context.Background(),
clientset,
name,
namespace,
image,
)
if err != nil {
t.Errorf("err = %v, want nil", err)
}
if !reflect.DeepEqual(gotPod, wantPod) {
t.Errorf("CreatePod() = %v, want %v",
gotPod,
wantPod,
)
}
}
这个测试将断言–给定一个名字、一个命名空间和一个图像–函数的结果将是wantPod,此时Pod中没有发生任何验证或变异。
不可能用这个测试来了解真正的客户端会发生什么,因为结果会有所不同–也就是说,真正的客户端和底层API会对对象进行突变,增加默认值,等等。
对行动的反应
假的客户集在没有任何验证或变异的情况下按原样存储资源。在测试中,你可能想模拟各种控制器对资源所做的改变。为此,假客户集提供了添加反应器的方法。Reactors是在特定资源上进行特定操作时执行的函数。
除了Watch和Proxy,所有操作的Reactors函数的类型定义如下:
TBD
5.8 - [第8章]用自定义资源定义扩展Kubernetes API
在本书的第一部分,你了解到Kubernetes API是按分组组织的。这些分组包含一个或多个资源,每个资源都是有版本的。
要在Go中使用 Kubernetes API,有两个基本库。API Machinery 提供了与API通信的工具,独立于API提供的资源。API库提供了 Kubernetes API 提供的本地Kubernetes 资源的定义,以便与 API Machinery 一起使用。
client-go 库利用 API Machinery 和 API库来提供对 Kubernetes API 的访问。
Kubernetes API 通过其自定义资源定义(CRD)机制是可扩展的。
CustomResourceDefinition 是一种特定的 Kubernetes 资源,用于以动态方式定义由API提供的新的 Kubernetes 资源。
为 Kubernetes 定义新资源是用来代表特定领域的资源,例如数据库、CI/CD作业或证书。
结合自定义控制器,这些自定义资源可以由控制器在集群中实现。
像其他资源一样,你可以获得、列出、创建、删除和更新这类资源。CRD资源是一种非命名空间的资源。
该CRD资源被定义在 apiextensions.k8s.io/v1
分组和版本中。访问该资源的 HTTP 路径尊重访问非核心和非命名资源的标准格式,即 /apis/<group>/<version>/<plural_resource_name>
,并且是 /apis/apiextensions.k8s.io/v1/customresourcedefinitions/
。
这个资源的 go 定义并不像其他本地资源那样在 API 库中声明,而是在 apiextensions-apiserver
库中。要从Go资源中访问定义,你需要使用以下导入:
import (
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)
在Go中执行操作
要使用 Go 对 CRD 资源进行操作,你可以使用 clientset,类似于 client-go 库提供的 clientset,但包含在 apiextensions-apiserver
库中。
要使用这个 clientset ,你需要使用下面的导入:
import (
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
)
你可以使用这个 CRD clientset,与使用 Client-go clientset 的方式完全相同。
作为一个例子,你可以在这里列出申报到一个集群中的CRD:
import (
"context"
"fmt"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// config is a standard rest.Config defined in client-go
clientset, err := clientset.NewForConfig(config)
if err != nil {
return err
}
ctx := context.Background()
list, err := clientset.ApiextensionsV1().
CustomResourceDefinitions().
List(ctx, metav1.ListOptions{})
CustomResourceDefinition的详细介绍
CustomReourceDefinition Go 结构体的定义是:
type CustomResourceDefinition struct {
metav1.TypeMeta
metav1.ObjectMeta
Spec CustomResourceDefinitionSpec
Status CustomResourceDefinitionStatus
}
像其他 Kubernetes 资源一样,CRD 资源嵌入了 TypeMeta 和 ObjectMeta 结构。ObjectMeta 字段中的 Name 的值必须等于 <Spec.Names.Plural> + “. ” + <Spec.Group>
。
像被 Controller 或 Operator 管理的资源一样,该资源包含一个 Spec 结构体来定义期望的状态,以及一个 Status 结构体来包含 Controller 或 Operator 描述的资源的状态。
type CustomResourceDefinitionSpec struct {
Group string
Scope ResourceScope
Names CustomResourceDefinitionNames
Versions []CustomResourceDefinitionVersion
Conversion *CustomResourceConversion
PreserveUnknownFields bool
}
Spec 的 Group 字段表示资源所在的分组的名称。
Scope 字段表示该资源是命名空间还是非命名空间。ResourceScope 类型包含两个值: ClusterScoped 和 NamespaceScoped。
命名资源
name 字段表示资源和相关信息的不同名称。这些信息将被 Kubernetes API 的发现机制使用,以便能够在发现调用的结果中包括 CRD。CustomResourceDefinitionNames 类型被定义为:
type CustomResourceDefinitionNames struct {
Plural string
Singular string
ShortNames []string
Kind string
ListKind string
Categories []string
}
- Plural(复数)是资源的小写复数名称,在访问资源的URL中使用–例如,pods。这个值是必须的。
- Singular(单数)是资源的小写单数名称。例如,pod。这个值是可选的,如果不指定,则使用Kind字段的小写值。
- ShortNames 是资源的小写短名列表,可用于在
kubectl get <shortname>
等命令中调用该资源。举例来说,服务资源声明了svc的短名,所以你可以执行kubectl get svc 而不是 kubectl get services。 - Kind 是资源的 CamelCase 和单数名称,在资源序列化时使用–例如,Pod或ServiceAccount。这个值是必须的。
- ListKind 是该资源的列表序列化过程中使用的名称–例如,PodList。这个值是可选的,如果没有指定,将使用以List为后缀的Kind值。
- Categories 是该资源所属的分组资源的列表,可以被
kubectl get <category>
这样的命令使用。所有类别是最著名的类别,但也存在其他类别(api-extensions),你可以定义自己的类别名称。
资源版本的定义
到此为止提供的所有信息都对资源的所有版本有效。
Versions 字段包含特定版本的信息,是一个定义列表,每个版本的资源都有一个定义。CustomResourceDefinitionVersion 类型被定义为:
type CustomResourceDefinitionVersion struct {
Name string
Served bool
Storage bool
Deprecated bool
DeprecationWarning *string
Schema *CustomResourceValidation
Subresources *CustomResourceSubresources
AdditionalPrinterColumns []CustomResourceColumnDefinition
}
Name 表示版本名称。Kubernetes 资源使用标准的版本格式:v<number>[(alpha|beta)<number>]
,但你可以使用任何你想要的格式。
Served 布尔值表示该特定版本是否必须由 API server 提供服务。如果不是,该版本仍然被定义,并且可以作为 Storage 使用(见下文),但用户不能创建或获得该特定版本的资源实例。
Storage 布尔值表示该特定版本是否是用于持久化资源的版本。恰好有一个版本必须定义这个字段为 true 。你在第五章的 “转换” 部分已经看到,转换功能存在于同一资源的不同版本之间。用户可以在任何可用的版本中创建资源,API服务器会将其转换为 Storage=true
的版本,然后再将数据持久化在 etcd 中。
Deprecated 布尔值表示该资源的特定版本是否被废弃。如果为真,服务器将为这个版本的响应添加一个 Warning 头。
DeprecationWarning 是当 Deprecated 为 true 时返回给调用者的警告信息。如果 Deprecated 为真,且该字段为零,则会发送一个默认的警告信息。
Schema 描述了这个版本的资源的模式。该模式用于在创建或更新资源时验证发送到 API 的数据。这个字段是可选的。模式将在 “资源的模式” 一节中详细讨论。
Subresources 定义了将为这个版本的资源提供的子资源。这个字段是可选的。该字段的类型定义为:
type CustomResourceSubresources struct {
Status *CustomResourceSubresourceStatus
Scale *CustomResourceSubresourceScale
}
-
如果 Status 不是 nil,"/status" 子资源将被提供。
-
如果 Scale 不是 nil,"/scale" 子资源将被提供。
AdditionalPrinterColumns是当用户要求表的输出格式时要返回的额外列的列表。这个字段是可选的。关于表格输出格式的更多信息可以在第二章 “以表格形式获取结果” 部分找到。你将在本章的 “附加打印机列” 部分看到如何定义附加打印机列。
资源的模式
特定版本的资源的模式是用 OpenAPI v3 模式格式来定义的。这种格式的完整描述不在本书的范围之内。这里有一个简短的介绍,可以帮助你为你的资源定义模式。
自定义资源通常有 Spec 部分和 Status 部分。为此,你必须声明对象类型的顶层模式,并使用属性声明字段。在这个例子中,顶层模式将有Spec和Status属性。然后你可以递归地描述每个属性。
Spec 和 Status 字段也将是对象类型的,并且包含属性。可接受的数据类型是:
- string
- number
- integer
- boolean
- array
- object
可以对字符串和数字类型给出具体的格式:
- string: date, date-time, byte, int-or-string
- number: float, double, int32, int64
在对象中,必须的字段用 required 属性表示。
你可以声明属性只接受一组值,使用枚举属性。要声明值的 map,你需要使用对象类型并指定 additionalProperties 属性。这个属性可以接受一个 true 值(表示条目可以有任何类型),或者通过给出一个类型作为值来定义 map 的条目类型:
type: object
additionalProperties:
type: string
或者
type: object
additionalProperties:
type: object
properties:
code:
type: integer
text:
type: string
在声明数组时,你必须定义数组中的 item 的类型:
type: array
items:
type: string
作为例子,这里是一个自定义资源的模式,包含 Spec 中的三个字段(image, replicas, and port),以及 Status 中的一个 state 字段。
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
image:
type: string
replicas:
type: integer
port:
type: string
format: int-or-string
required: [image,replicas]
status:
type: object
properties:
state:
type: string
enum: [waiting,running]
部署自定义资源定义
要将新的资源定义部署到集群中,你需要在 CRD 对象上执行 Create 操作。你可以使用 apiextensions-apiserver 库提供的客户端,并使用你在前几节看到的结构体在 Go 中编写资源定义,或者你可以创建一个 YAML 文件并使用 kubectl “apply " 它。
举个例子,你将在 mygroup.example.com
分组中创建一个名为 myresources
的新资源,其版本为 v1alpha1
,使用 YAML 格式和 kubectl 来部署它。
apiVersion: apiextensions.k8s.io/v1 ❶
kind: CustomResourceDefinition ❷
metadata:
name: myresources.mygroup.example.com ❸
spec:
group: mygroup.example.com ❹
scope: Namespaced ❺
names:
plural: myresources ❻
singular: myresource ❼
shortNames:
- my ❽
- myres
kind: MyResource
categories:
- all ❾
versions:
- name: v1alpha1 ❿
served: true
storage: true
schema:
openAPIV3Schema:
type: object ⓫
-
❶ CRD 资源的分组和版本
-
❷ 该 CRD 资源的种类(kind)
-
❸ 新资源的完整名称,包括其组别
-
❹ 新资源所属的组别
-
❺ 新资源可以在特定的命名空间创建
-
❻ 新资源的复数名称,在访问该新资源的路径中使用
-
❼ 资源的单数名称,你可以使用 kubectl get myresource
-
❾ 新资源的短名,你可以使用 kubectl get my, kubectl get myres
-
❾ 将资源添加到所有类别中;运行 kubectl get all 时,会出现这类资源
-
❿ v1alpha1版本是新资源唯一定义的版本。
-
⓫ 将新资源模式定义为一个对象,没有字段
现在,你可以使用 kubectl “apply” 这个资源,使用以下命令:
$ kubectl apply -f myresource.yaml
customresourcedefinition.apiextensions.k8s.io/myresources.mygroup.example.com created
从这开始,你可以使用这个新资源。
例如,你可以使用 HTTP 请求获得整个集群或特定命名空间的资源列表(在运行这些命令之前,你需要从其他终端执行 kubectl proxy):
$ curl
http://localhost:8001/apis/mygroup.example.com/v1alpha1/myresources
{"apiVersion":"mygroup.example.com/v1alpha1","items":[],"kind":"MyResourceList","metadata":{"continue":"","resourceVersion":"186523407"}}
$ curl
http://localhost:8001/apis/mygroup.example.com/v1alpha1/namespaces/default/myresources
{"apiVersion":"mygroup.example.com/v1alpha1","items":[],"kind":"MyResourceList","metadata":{"continue":"","resourceVersion":"186524840"}}
或者,你可以用 kubectl 获得这些列表:
$ kubectl get myresources
No resources found in default namespace.
你可以使用 YAML 格式定义一个新的资源,并使用 kubectl 将其 “apply” 于集群:
kubectl apply -f - <<EOF
apiVersion: mygroup.example.com/v1alpha1
kind: MyResource
metadata:
name: myres1
EOF
$ kubectl get myresources
NAME AGE
myres1 10s
额外的打印列
TBD
总结
在本章中,你已经看到由 Kubernetes API 提供的资源列表是可以通过在集群中创建 CustomResourceDefinition(CRD)资源来扩展的。你已经看到了CRD资源在 Go 中的结构体,以及如何使用专用的 clientset 来操作 CRD 资源。
之后,你看到了如何用 YAML 编写 CRD,以及如何使用 kubectl 将其部署到集群中。然后,一些字段被添加到 CRD 方案中,以及一些额外的打印列,以便在 kubectl get
的输出中显示这些字段。
最后,你已经看到,一旦 CRD 在集群中被创建,你就可以开始使用新的相关种类的资源。
5.9 - [第9章]使用自定义资源
在上一章中,你已经看到了如何使用 CustomResourceDefinition 资源声明一个由 Kubernetes API 提供的新的自定义资源,以及如何使用 kubectl 创建这个自定义资源的新实例。但就目前而言,你还没有任何 Go 库可以让你处理自定义资源的实例。
本章探讨了在 Go 中处理自定义资源的各种可能性:
- 为自定义资源的专用 Clientset 生成代码。
- 使用 API Machinery 的非结构化(unstructured)包和 Client-go 库的 dynamic 包。
生成 Clientset
存储库 https://github.com/kubernetes/code-generator 包含 Go 代码生成器。第三章的 “包的内容” 部分包含了对这些生成器的一个非常快速的概述,其中对API库的内容进行了探讨。
要使用这些生成器,你需要首先为自定义资源所定义的种类编写 Go 结构体。在这个例子中,您将为 MyResource 和 MyResourceList 类型编写结构体。
为了与 API 库中的组织结构体保持一致,您将在 types.go 文件中编写这些类型,放置在 pkg/apis/<group>/<version>/
目录中。
同样地,为了与生成器正常工作,你项目的根目录必须在你项目的 Go 包之后,以 Go 包命名的目录。例如,如果项目的包是 github.com/myid/myproject
(在项目的 go.mod 文件的第一行定义),项目的根目录必须在 github.com/myid/myproject/
目录下。
作为例子,让我们开始一个新项目。你可以在你选择的目录中执行这些命令,一般是包含你所有 Go 项目的目录。
$ mkdir -p github.com/myid/myresource-crd
$ cd github.com/myid/myresource-crd
$ go mod init github.com/myid/myresource-crd
$ mkdir -p pkg/apis/mygroup.example.com/v1alpha1/
$ cd pkg/apis/mygroup.example.com/v1alpha1/
然后,在这个目录中,你可以创建 types.go
文件,其中包含 kind 的结构体定义。下面是与前一章 CRD 中定义的模式相匹配的结构体定义。
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
type MyResource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyResourceSpec `json:"spec"`
}
type MyResourceSpec struct {
Image string `json:"image"`
Memory resource.Quantity `json:"memory"`
}
type MyResourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []MyResource `json:"items"`
}
现在,你需要运行两个生成器:
deepcopy-gen
- 这将为每个 Kind 结构体生成一个 DeepCopyObject() 方法,这是这些类型实现runtime.Object
接口所需要的。client-gen
- 这将为该分组/版本生成 clientset。
使用deepcopy-gen
安装 deepcopy-gen
要安装 deepcopy-gen 可执行文件,你可以使用 go install 命令:
go install k8s.io/code-generator/cmd/deepcopy-gen@v0.24.4
你可以使用 @latest 标签来使用 Kubernetes 代码的最新版本,或者选择一个特定的版本。
添加注解
deepcopy-gen 生成器需要注释才能工作。它首先需要 //+k8s:deepcopy-gen=package
注解在包级别上被定义。这个注解要求 deepcopy-gen 为包的所有结构体生成 deepcopy 方法。
为此,你可以在 types.go 所在的目录中创建一个 doc.go 文件,以添加这个注释:
// pkg/apis/mygroup.example.com/v1alpha1/doc.go
// +k8s:deepcopy-gen=package
package v1alpha1
默认情况下,deepcopy-gen 将生成 DeepCopy() 和 DeepCopyInto() 方法,但没有 DeepCopyObject()。为此,你需要在每个种类结构体之前添加另一个注释(//+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
)。
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type MyResource struct {
[...]
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type MyResourceList struct {
[...]
运行 deepcopy-gen
生成器需要一个文件,包含在生成的文件开头添加的文本(一般是许可证)。为此,你可以创建一个空文件(或者用你喜欢的内容,别忘了这些文字应该是Go注释),命名为 hack/boilerplate.go.txt
。
你需要运行 go mod tidy 来使生成器工作(如果你喜欢 vendor Go dependencies,也可以运行 go mod vendor
)。最后,你可以运行 deepcopy-gen 命令,它将生成一个文件 pkg/apis/mygroup.example.com/v1alpha1/zz_generated.deepcopy.go
:
$ go mod tidy
$ deepcopy-gen --input-dirs github.com/myid/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1
-O zz_generated.deepcopy
--output-base ../../..
--go-header-file ./hack/boilerplate.go.txt
注意 “…/…/… " 作为 output-base 。它将把 output-base 放在你为该项目创建目录的目录中:
$ mkdir -p github.com/myid/myresource-crd
如果你创建的子目录数量与这三个不同,你将需要对其进行调整。
在这一点上,你的项目应该有以下文件结构:
├── go.mod
├── hack
│ └── boilerplate.go.txt
├── pkg
│ └── apis
│ └── mygroup.example.com
│ └── v1alpha1
│ ├── doc.go
│ ├── types.go
│ └── zz_generated.deepcopy.go
使用 client-gen
安装 client-go
要安装 client-gen 可执行文件,你可以使用 go install命令:
go install k8s.io/code-generator/cmd/client-gen@v0.24.4
你可以使用 @latest 标签来使用 Kubernetes 代码的最新版本,如果你想以可复制的方式运行该命令,则可以选择一个特定的版本。
添加注解
你需要向 types.go 文件中定义的结构体添加注释,以表明你想为哪些类型定义 clientset。要使用的注释是 // +genclient
。
// +genclient
(没有选项)将要求 client-gen 为一个有命名空间的资源生成 Clientset。// +genclient:nonNamespaced
将为非namespaced资源生成 Clientset。+genclient:onlyVerbs=create,get
将只生成这些动词,而不是默认生成的所有动词。+genclient:skipVerbs=watch
将生成除这些动词以外的所有动词,而不是默认的所有动词。+genclient:noStatus
- 如果注释的结构体中存在 Status 字段,将生成一个 updateStatus 函数。通过这个选项,你可以禁止生成 updateStatus 函数(注意,如果 Status 字段不存在,就没有必要)。
你所创建的自定义资源是有命名空间的,所以你可以使用注解而不使用选项:
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +genclient
type MyResource struct {
[...]
添加 AddToScheme 函数
生成的代码依赖于定义在 resource 包中的 AddToScheme 函数。为了与 API 库中的惯例保持一致,你需要在 register.go
文件中编写这个函数,放在目录 **pkg/apis/<group>/<version>/**
中。
通过从 Kubernetes API 库的本地 Kubernetes 资源中获取作为模板的 register.go 文件,你会得到以下文件。唯一的变化是 group name(❶)、version name(❷)和要注册到该 schema 的资源列表(❸)。
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const GroupName = "mygroup.example.com" ❶
var SchemeGroupVersion = schema.GroupVersion{
Group: GroupName,
Version: "v1alpha1", ❷
}
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&MyResource{}, ❸
&MyResourceList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
运行 client-go
client-gen 需要一个包含文本(通常是许可证)的文件,添加在生成文件的开头。你将使用与 deepcopy-gen 相同的文件: hack/boilerplate.go.txt
。
你可以运行 client-gen 命令,它将在 pkg/clientset/clientset
目录下生成文件:
client-gen \
--clientset-name clientset
--input-base ""
--input github.com/myid/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1
--output-package github.com/myid/myresource-crd/pkg/clientset
--output-base ../../..
--go-header-file hack/boilerplate.go.txt
注意 ".../.../..."
作为 output-base 。它将把 output-base 放在你为该项目创建目录的目录中:
$ mkdir -p github.com/myid/myresource-crd
如果你创建的子目录数量与三个不同,你将需要对其进行调整。
注意,当你更新自定义资源的定义时,你将需要再次运行这个命令。建议把这个命令放在 Makefile 中,以便在每次修改定义自定义资源的文件时自动运行它。
使用生成的 clientset
现在,clientset 已经生成,并且类型实现了 runtime.Object 接口,你可以像处理本地 Kubernetes 资源一样处理自定义资源。例如,这段代码将使用专用的 clientset 来列出默认命名空间上的自定义资源:
import (
"context"
"fmt"
"github.com/myid/myresource-crd/pkg/clientset/clientset"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
)
config, err :=
clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
nil,
).ClientConfig()
if err != nil {
return err
}
clientset, err := clientset.NewForConfig(config)
if err != nil {
return err
}
list, err := clientset.MygroupV1alpha1().
MyResources("default").
List(context.Background(), metav1.ListOptions{})
if err != nil {
return err
}
for _, res := range list.Items {
fmt.Printf("%s\n", res.GetName())
}
使用生成的 fake Clientset
客户端生成工具也会生成 fake Clientset,你可以像使用 Client-go 库中的 fake Clientset 一样使用它。更多信息,请参见第7章的 “fake Clientset” 部分。
使用非结构化包和动态客户端
Unstructured 和 UnstructuredList 类型被定义在API Machinery 的 unstructured 包中。要使用的导入方式如下:
import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
这些类型可以用来表示任何 Kubernetes Kind,无论是列表还是非列表。
非结构化类型
非结构化类型被定义为一个结构体,包含一个唯一的对象字段:
type Unstructured struct {
// Object is a JSON compatible map with
// string, float, int, bool, []interface{}, or
// map[string]interface{}
// children.
Object map[string]interface{}
}
使用这种类型,可以定义任何 Kubernetes 资源,而不必使用类型结构体(例如,API 库中的 Pod 结构体)。
该类型定义了 Getters 和 Setters 方法,以访问 TypeMeta 和 ObjectMeta 字段中的通用字段,这些字段对代表 Kubernetes Kinds 的所有结构体都是通用的。
访问 TypeMeta 字段的 Getters 和 Setters
APIVersion 和 Kind Getters/Setters 可以被用来直接获取和设置 TypeMeta 的 apiVersion 和 Kind 字段。
GroupVersionKind Getters/Setters 可用于将对象中指定的 apiVersion 和 Kind 转换为 GroupVersionKind 值。
GetAPIVersion() string
GetKind() string
GroupVersionKind() schema.GroupVersionKind
SetAPIVersion(version string)
SetKind(kind string)
SetGroupVersionKind(gvk schema.GroupVersionKind)
访问 ObjectMeta 字段的Getters和Setters
Getters 和 Setters 被定义为 ObjectMeta 结构体的所有字段。该结构体的细节可以在第三章的 “ObjectMeta字段” 部分找到。
作为例子,访问 Name 字段的 getter 和 setter是 GetName() string
和 SetName(name string)
。
创建和转换的方法
-
NewEmptyInstance() runtime.Unstructured
- 这将返回一个新的实例,只有 apiVersion 和 kind 字段是从接收者那里复制的。 -
MarshalJSON() ([]byte, error)
- 这返回接收器的JSON表示。 -
UnmarshalJSON(b []byte) error
- 这将用传递的JSON表示法填充接收器。 -
UnstructuredContent() map[string]interface{}
- 这将返回接收器的Object字段的值。 -
SetUnstructuredContent()
content map[string]interface{},
)
这将设置接收方的 Object 字段的值。
-
IsList() bool
- 如果接收方描述的是一个列表,通过检查项目字段是否存在,并且是一个数组,返回true。 -
ToList() (*UnstructuredList, error)
- 这将接收器转换为一个非结构化列表。
访问非meta字段的助手
以下 helper 可以用来获取和设置非结构化实例的 Object 字段中的特定字段的值。
注意,这些 helper 是函数,而不是非结构化类型的方法。
它们都接受:
- 第一个参数 obj 是
map[string]interface{}
类型,用于传递非结构化实例的 Object 字段、 - 最后一个参数 fields,类型为
...string
,用于传递导航到对象的键。注意,不支持数组/slice 的语法。
Setters 接受第二个参数,给出给定对象中特定字段的设置值。Getters 返回三个值:
-
要求的字段的值,如果可能的话
-
一个布尔值,表示是否已经找到了所请求的字段
-
如果找到了字段,但不属于请求的类型,则为错误。
helper 函数的名称是:
-
RemoveNestedField - 这将删除请求的字段
-
NestedFieldCopy, NestedFieldNoCopy - 返回请求字段的副本或原始值
-
NestedBool, NestedFloat64, NestedInt64, NestedString, SetNestedField - 获取和设置 bool/float64/int64/string 字段。
-
NestedMap, SetNestedMap - 获取和设置
map[string]interface{}
类型的字段。 -
NestedSlice, SetNestedSlice - 获取并设置
[]interface{}
类型的字段。 -
NestedStringMap, SetNestedStringMap - 获取和设置
map[string]string
类型的字段。 -
NestedStringSlice, SetNestedStringSlice - 获取和设置
[]string
类型的字段。
例子:
作为例子,下面是一些定义 MyResource 实例的代码:
import (
myresourcev1alpha1 "github.com/myid/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func getResource() (*unstructured.Unstructured, error) {
myres := unstructured.Unstructured{}
myres.SetGroupVersionKind(
myresourcev1alpha1.SchemeGroupVersion.
WithKind("MyResource"))
myres.SetName("myres1")
myres.SetNamespace("default")
err := unstructured.SetNestedField(
myres.Object,
"nginx",
"spec", "image",
)
if err != nil {
return err
}
// Use int64
err = unstructured.SetNestedField(
myres.Object,
int64(1024*1024*1024),
"spec", "memory",
)
if err != nil {
return err
}
// or use string
err = unstructured.SetNestedField(
myres.Object,
"1024Mo",
"spec", "memory",
)
if err != nil {
return err
}
return &myres, nil
}
UnstructuredList 类型
UnstructuredList 类型被定义为一个结构体,包含一个 Object 字段和一个非结构化实例的 slice 作为项目:
type UnstructuredList struct {
Object map[string]interface{}
// Items is a list of unstructured objects.
Items []Unstructured
}
TBD
Dynamic Client
正如你在第6章中所看到的,Client-go 为客户端提供了与 Kubernetes API 协同工作的能力:Clientet 用于访问类型化资源,REST 客户端用于对API进行低级别的REST调用,Discovery 客户端用于获取API提供的资源信息。
它提供了另一个客户端,即 Dynamic Client / 动态客户端,用于处理非类型的资源,用非结构化类型描述。
获取动态客户端
dynamic 包提供了创建 dynamic.Interface
类型的动态客户端的函数。
总结
本章探讨了在 Go 中使用自定义资源的各种解决方案。
第一个解决方案是根据自定义资源的定义生成Go代码,为这个特定的自定义资源定义生成一个客户端,这样你就可以像处理本地 Kubernetes 资源一样处理自定义资源实例。
另一个解决方案是使用 Client-go 库的动态客户端,并依靠非结构化类型来定义自定义资源。
5.10 - [第10章]用Controller-Runtime库编写Operator
5.10.1 - controller-runtime简介
正如你在第1章中所看到的,Controller Manager 是 Kubernetes 架构的一个重要部分。它嵌入了几个 Controller,每个控制器的作用是观察特定高层资源(Deployments 等)的实例,并使用低层资源(Pod等)来实现这些高层实例。
举例,Kubernetes 用户在部署无状态的应用程序时可以创建 Deployment。这个 Deployment 定义了 Pod 模板,用来在集群中创建 Pod 以及一些规范。以下是最重要的规格:
-
副本的数量:控制器必须为 Deployment 实例部署相同的 Pod。
-
部署策略:当 Pod 模板更新时,Pod 被替换的方式。
-
默认策略:滚动更新(Rolling Update)可用于在不中断服务的情况下将应用程序更新到新版本,它接受各种参数。还存在一个更简单的策略:Recreate,它将首先停止Pod,然后再开始替换它。
Deployment 控制器将创建 ReplicaSet 资源的实例,每个新版本的 Pod 模板都有一个,并更新这些 ReplicaSets 的副本数量,以满足副本和策略规范。你会发现退役版本的 ReplicaSet 的副本数为零,而实际应用版本的 ReplicaSet 的副本数为正数。在滚动更新期间,两个 ReplicaSets 将拥有正数的副本(新的副本数量增加,以前的副本数量减少),以便能够在两个版本之间过渡而不中断服务。
在它这边,ReplicaSet 控制器将负责维护由 Deployment 控制器创建的每个 ReplicaSet 实例所要求的 Pod 副本数量。
第8章已经表明,可以定义新的 Kubernetes 资源来扩展 Kubernetes 的 API。即使有原生控制器管理器运行控制器来处理原生 Kubernetes 资源,你也需要编写控制器来处理自定义资源。
一般来说,这样的控制器被称为 operator,处理第三方资源,并为处理原生 Kubernetes 资源的控制器保留 Controller 这个名字。
Client-go 库提供了使用 Go 语言编写控制器和 operator 的工具,controller-runtime 库利用这些工具提供围绕控制器模式的抽象,帮助你编写 operator 。这个库可以用下面的命令来安装:
go get sigs.k8s.io/controller-runtime@v0.13.0
你可以从 github.com/kubernetes-sigs/controller-runtime/releases
的源码库中获得可用的修订版。
5.10.2 - Manager
controller-runtime 库提供的第一个重要抽象是 Manager / 管理器,它为在管理器内运行的所有控制器提供共享资源,包括:
-
读取和写入 Kubernetes 资源的 Kubernetes 客户端
-
用于从本地缓存中读取 Kubernetes 资源的缓存
-
用于注册所有 Kubernetes 本地和自定义资源的 scheme
要创建管理器,你需要使用提供的 New 函数,如下所示:
import (
"flag"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/manager"
)
flag.Parse() ❶
mgr, err := manager.New(
config.GetConfigOrDie(),
manager.Options{},
)
-
❶解析命令行标志,如 GetConfigOrDie 来处理
--kubeconfig
标志;见下文。第一个参数是 rest.Config 对象,见第 6 章 “连接到集群” 部分。请注意,在这个例子中,选择了 controller-runtime 库提供的 GetConfigOrDie() 实用函数,而不是使用 client-go 库的函数。
GetConfigOrDie() 函数将尝试获得一个配置来连接到集群:
-
通过获取
--kubeconfig
标志的值,如果定义了的话,并在这个路径上读取 kubeconfig 文件。为此,首先你需要执行flag.Parse()
-
通过获取 KUBECONFIG 环境变量的值(如果定义了的话),并读取此路径下的 kubeconfig 文件
-
通过查看 in-cluster 配置(见第6章的 “集群内配置 “部分),如果定义了的话
-
通过读取
$HOME/.kube/config
文件
如果前面的情况都不可行,该函数将使程序退出,代码为1。第二个参数是一个用于选项的结构体。
一个重要的选项是 “Scheme"。默认情况下,如果你没有为这个选项指定任何值,将使用 Client-go 库提供的 Scheme。如果控制器只需要访问本地 Kubernetes 资源,这就足够了。然而,如果你想让控制器访问自定义资源,你将需要提供一个能够解决自定义资源的 Scheme。
例如,如果你想让控制器访问第九章中定义的自定义资源,你将需要在初始化时运行以下代码:
import (
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
mygroupv1alpha1 "github.com/myid/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
)
scheme := runtime.NewScheme() ❶
clientgoscheme.AddToScheme(scheme) ❷
mygroupv1alpha1.AddToScheme(scheme) ❸
mgr, err := manager.New(
config.GetConfigOrDie(),
manager.Options{
Scheme: scheme, ❹
},
)
❶ 创建一个新的空 scheme
❷ 使用 Client-go 库添加本地 Kubernetes 资源
❷ 将 mygroup/v1alpha1 的资源添加到 scheme 中,其中包含我们的自定义资源
❹ 在这个管理器中使用这个 scheme
5.10.3 - Controller
第二个重要的抽象是控制器 / Controller。控制器负责实现特定 Kubernetes 资源的实例所给出的规范(Spec)。(在 operator 的情况下,自定义资源是由 operator 处理的)。
为此,控制器观察特定的资源(至少是相关的自定义资源,在本节中称为 “主要资源”),并接收这些资源的 watch 事件(即,创建、更新、删除)。当事件发生在资源上时,控制器用包含受事件影响的 “主要资源” 实例的名称和命名空间的请求填充一个队列。
请注意,入队的对象只是被 operator 监视的主要资源的实例。如果事件是由另一个资源的实例接收的,则通过跟踪所有者参考(ownerReference)找到主资源。例如,Deployment 控制器观察 Deployment 资源和 ReplicaSet 资源。所有 ReplicaSet 实例都包含一个指向 Deployment 实例的 ownerReference。
-
当 Deployment 被创建时,控制器会收到一个 Create 事件,刚刚创建的 Deployment 实例被入队
-
当 ReplicaSet 被修改时(例如,被一些用户修改),这个 ReplicaSet 会收到一个 Update 事件,控制器会使用 ReplicaSet 中包含的 ownerReference 找到被更新的 ReplicaSet 引用的 Deployment。然后,被引用的 Deployment 实例被入队。
控制器实现 Reconcile 方法,每当队列中出现一个 Request 时,它就会被调用。这个 Reconcile 方法接收 Request 作为参数,其中包含要调和(Reconcile)的主要资源的名称和命名空间。
请注意,触发请求的事件不是请求的一部分,因此,Reconcile 方法不能依赖这个信息。此外,如果事件发生在一个被拥有的(owned)资源上,只有主要资源被入队,Reconcile 方法不能依赖哪个被拥有的(owned)资源触发了该事件。
此外,由于多个事件可能在短时间内发生,并与同一主要资源有关,请求可以被批量处理,以限制入队的请求数量。
创建 Controller
要创建控制器,你需要使用提供的 New 函数:
import (
"sigs.k8s.io/controller-runtime/pkg/controller"
)
controller, err = controller.New(
"my-operator", mgr,
controller.Options{
Reconciler: myReconciler,
})
Reconciler 选项是必需的,其值是一个必须实现 Reconciler 接口的对象,定义为:
type Reconciler interface {
Reconcile(context.Context, Request) (Result, error)
}
作为一种设施,提供了 reconcile.Func
类型,它实现了 Reconciler 接口,并且与 Reconcile 方法的签名相同的函数类型。
type Func func(context.Context, Request) (Result, error)
func (r Func) Reconcile(ctx context.Context, o Request) (Result, error) {
return r(ctx, o)
}
由于这个 reconcile.Func
类型,你可以用 Reconcile 签名构造一个函数,并把它分配给 Reconciler 选项。比如说:
controller, err = controller.New(
"my-operator", mgr,
controller.Options{
Reconciler: reconcile.Func(reconcileFunction),
})
func reconcileFunction(
ctx context.Context,
r reconcile.Request,
) (reconcile.Result, error) {
[...]
return reconcile.Result{}, nil
}
监控资源
在控制器创建后,你需要向容器指出哪些资源需要观察,以及这些资源是主要资源还是被拥有的(owned)资源。
控制器上的 Watch 方法被用来添加一个 Watch。该方法定义如下:
Watch(
src source.Source,
eventhandler handler.EventHandler,
predicates ...predicate.Predicate,
) error
第一个参数表示什么是要观察的事件的来源,其类型是 source.Source
接口。controller-runtime 库为 Source 接口提供了两种实现方式:
-
Kind source 用于监视特定种类(kind)的 Kubernetes 对象的事件。Kind 结构体中的 Type 字段是必需的,其值是所需类型的对象。例如,如果我们想监视 Deployment,src 参数的值将是:
controller.Watch( &source.Kind{ Type: &appsv1.Deployment{}, }, ...
-
Channel source 是用来观察来自集群外的事件的。Channel 结构体的 Source 字段是必需的,它的值是一个发射
event.GenericEvent
类型对象的通道。
第二个参数是事件处理程序,其类型是 handler.EventHandler
接口。controller-runtime 库为 EventHandler 接口提供了两种实现方式:
-
EnqueueRequestForObject 事件处理程序用于控制器处理的主要资源。在这种情况下,控制器将把连接到事件的对象放入队列中。例如,如果控制器是处理第9章中创建的自定义资源的 operator,你将写道:
controller.Watch( &source.Kind{ Type: &mygroupv1alpha1.MyResource{}, }, &handler.EnqueueRequestForObject{}, )
-
EnqueueRequestForOwner 事件处理程序用于由主资源拥有( owned)的资源。EnqueueRequestForOwner 的一个字段是必需的: OwnerType。这个字段的值是主资源类型的对象;控制器将跟踪 ownerReferences,直到找到这种类型的对象,并将这个对象放入队列。
例如,如果控制器处理 MyResource 主资源,并且正在创建 Pod 来实现 MyResource,它将希望使用这个事件处理程序来观察 Pod 资源,并指定一个 MyResource 对象作为 OwnerType。
-
如果字段 IsController 被设置为true,控制器将只考虑
Controller: true
的 ownerReferences。controller.Watch( &source.Kind{ Type: &corev1.Pod{}, }, &handler.EnqueueRequestForOwner{ OwnerType: &mygroupv1alpha1.MyResource{}, IsController: true, }, )
第三个参数是一个可选的谓词列表,其类型为 predicate.Predicate
。controller-runtime 库为 Predicate 接口提供了几种实现:
-
Funcs 是最通用的实现。Funcs 的结构体定义如下:
type Funcs struct { // Create returns true if the Create event // should be processed CreateFunc func(event.CreateEvent) bool // Delete returns true if the Delete event // should be processed DeleteFunc func(event.DeleteEvent) bool // Update returns true if the Update event // should be processed UpdateFunc func(event.UpdateEvent) bool // Generic returns true if the Generic event // should be processed GenericFunc func(event.GenericEvent) bool }
你可以把这个结构体的一个实例传递给 Watch 方法,作为 Predicate。
未定义的字段将表示匹配类型的事件应该被处理。
对于非 nil 字段,匹配事件的函数将被调用(注意,当源是一个通道时,GenericFunc 将被调用;见前文),如果函数返回 true,事件将被处理。
-
使用 Predicate 的这种实现,你可以为每个事件类型定义一个特定的函数。
func NewPredicateFuncs( filter func(object client.Object) bool, ) Funcs
该函数接受一个 filter 函数,并返回一个 Funcs 结构体,该过滤器被应用于所有事件。使用这个函数,你可以定义一个适用于所有事件类型的单一过滤器。
-
ResourceVersionChangedPredicate
结构体将定义一个只用于 UpdateEvent 的 filter。使用这个 predicate ,所有的Create、Delete 和 Generic 事件将被处理,不需要过滤,而 Update 事件将被过滤,以便只处理有
metadata.resourceVersion
变化的更新。每次保存资源的新版本时,
metadata.resourceVersion
字段都会被更新,不管资源的变化是什么。 -
GenerationChangedPredicate
结构体定义了一个只用于更新事件过滤器。使用这个谓词,所有的 Create、Delete 和 Generic 事件都将被处理,无需过滤,而 Update 事件将被过滤,因此只有具有
metadata.Generation
增量的更新才会被处理。每次发生资源的 Spec 部分的更新时,API 服务器都会按顺序递增
metadata.Generation
。请注意,有些资源并不尊重这个假设。例如,当 Annotations 字段被更新时,Deployment 的 Generation 也会被递增。
对于自定义资源,只有当状态子资源被启用时,生成才会被递增。
-
AnnotationChangedPredicate
结构体定义了一个只用于更新事件过滤器。使用这个谓词,所有的创建、删除和通用事件都将被处理,而更新事件将被过滤,因此只有元数据.注释变化的更新才会被处理。
第一个例子
在第一个例子中,你将创建管理器和控制器。控制器将管理主要的自定义资源 MyResource,并观察这个资源以及 Pod 资源。
Reconcile 函数将只显示 MyResource 实例的命名空间和名称来进行调节。
package main
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
mygroupv1alpha1 "github.com/myid/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
)
func main() {
scheme := runtime.NewScheme() ❶
clientgoscheme.AddToScheme(scheme)
mygroupv1alpha1.AddToScheme(scheme)
mgr, err := manager.New( ❷
config.GetConfigOrDie(),
manager.Options{
Scheme: scheme,
},
)
panicIf(err)
controller, err := controller.New( ❸
"my-operator", mgr,
controller.Options{
Reconciler: &MyReconciler{},
})
panicIf(err)
err = controller.Watch( ❹
&source.Kind{
Type: &mygroupv1alpha1.MyResource{},
},
&handler.EnqueueRequestForObject{},
)
panicIf(err)
err = controller.Watch( ❺
&source.Kind{
Type: &corev1.Pod{},
},
&handler.EnqueueRequestForOwner{
OwnerType: &corev1.Pod{},
IsController: true,
},
)
panicIf(err)
err = mgr.Start(context.Background()) ❻
panicIf(err)
}
type MyReconciler struct{} ➐
func (o *MyReconciler) Reconcile( ➑
ctx context.Context,
r reconcile.Request,
) (reconcile.Result, error) {
fmt.Printf("reconcile %v\n", r)
return reconcile.Result{}, nil
}
// panicIf panic if err is not nil
// Please call from main only!
func panicIf(err error) {
if err != nil {
panic(err)
}
}
➊ 用本地资源和自定义资源 MyResource 创建 scheme。
➋ 使用刚刚创建的 scheme 创建管理器
➌ 创建控制器,连接到管理器,并传递 Reconciler 实现。
➍ 开始观察作为主要资源的 MyResource 实例
➎ 开始观察 Pod 实例,作为自有(owned)资源
➏ 启动管理器。这个函数是长期运行的,只有在发生错误时才会返回
➐ 实现 Reconciler 接口的类型
➑ 实现 Reconcile 方法。这将显示要 reconcile 的实例的名称空间和名称
使用Controller Builder
Controller-runtime 库提出了一个控制器生成器 / Controller Builder,使控制器的创建更加简洁。
import (
"sigs.k8s.io/controller-runtime/pkg/builder"
)
func ControllerManagedBy(m manager.Manager) *Builder
ControllerManagedBy 函数被用来启动一个新的 ControllerBuilder。构建的控制器将被添加到 m manager 中。一个流畅的接口帮助配置构建:
-
For(object client.Object, opts ...ForOption) *Builder
- 该方法用于指示控制器处理的主要资源。它只能被调用一次,因为一个控制器只能有一个主要资源。这将在内部调用 Watch 函数与事件处理程序 EnqueueRequestForObject 。可以用 WithPredicates 函数为这个 watch 添加谓词(Predicates),其结果实现了 ForOption 接口。
-
Owns(object client.Object, opts ...OwnsOption) *Builder
- 这个方法用来表示控制器拥有(owned)的资源。这将在内部调用 Watch 函数与事件处理程序 EnqueueRequestForOwner。可以用 WithPredicates 函数为这个Watch添加谓词,该函数的结果实现了OwnsOption接口。
-
Watches(src source.Source, eventhandler handler.EventHandler, opts ...WatchesOption) *Builder
- 这个方法可以用来添加更多For或Owns方法没有涵盖的 watcher –例如,具有 Channel source 的观察者。可以用 WithPredicates 函数为该 watch 添加谓词,该函数的结果实现了 WatchesOption 接口。
-
WithEventFilter(p predicate.Predicate) *Builder
- 这个方法可以用来添加所有用 For、Owns 和 Watch 方法创建的观察者共有的谓词。 -
WithOptions(options controller.Options) *Builder
- 此处设置将在内部传递给controller.New
函数的选项。 -
WithLogConstructor(- 这设置了 logConstructor 选项。
func(*reconcile.Request) logr.Logger, ) *Builder
-
Named(name string) *Builder
- 这设置了构造函数的名称。它应该只使用下划线和字母数字字符。默认情况下,它是主要资源的 kind 的小写版本。 -
build() 构建并返回控制器。
Build( r reconcile.Reconciler, ) (controller.Controller, error)
-
Complete(r reconcile.Reconciler) error
–这就建立了控制器。你一般不需要直接访问控制器,所以你可以使用这个不返回控制器值的方法,而不是Build。
使用ControllerBuilder的第二个例子
在这个例子中,你将使用 ControllerBuilder 构建控制器,而不是使用 Controller.New 函数和控制器上的 Watch 方法。
package main
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
mygroupv1alpha1 "github.com/feloy/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
)
func main() {
scheme := runtime.NewScheme()
clientgoscheme.AddToScheme(scheme)
mygroupv1alpha1.AddToScheme(scheme)
mgr, err := manager.New(
config.GetConfigOrDie(),
manager.Options{
Scheme: scheme,
},
)
panicIf(err)
err = builder.
ControllerManagedBy(mgr).
For(&mygroupv1alpha1.MyResource{}).
Owns(&corev1.Pod{}).
Complete(&MyReconciler{})
panicIf(err)
err = mgr.Start(context.Background())
panicIf(err)
}
type MyReconciler struct {}
func (a *MyReconciler) Reconcile(
ctx context.Context,
req reconcile.Request,
) (reconcile.Result, error) {
fmt.Printf("reconcile %v\n", req)
return reconcile.Result{}, nil
}
func panicIf(err error) {
if err != nil {
panic(err)
}
}
5.10.4 - 将管理器资源注入 Reconciler 中
管理器为控制器提供共享资源,包括读取和写入 Kubernetes 资源的客户端、从本地缓存中读取资源的缓存以及解析资源的方案。Reconcile 函数需要访问这些共享资源。有两种方法来共享它们:
创建 Reconciler 结构体时传递数值
当控制器被创建时,你正在传递一个 Reconcile 结构体的实例,实现 Reconciler 接口:
type MyReconciler struct {}
err = builder.
ControllerManagedBy(mgr).
For(&mygroupv1alpha1.MyResource{}).
Owns(&corev1.Pod{}).
Complete(&MyReconciler{})
在这之前,管理器已经被创建,你可以使用管理器上的 Getters 来访问共享资源。
作为例子,下面是如何从新创建的管理器中获取客户端、缓存和 schema :
mgr, err := manager.New(
config.GetConfigOrDie(),
manager.Options{
manager.Options{
Scheme: scheme,
},
},
)
// handle err
mgrClient := mgr.GetClient()
mgrCache := mgr.GetCache()
mgrScheme := mgr.GetScheme()
你可以在 Reconciler 结构体中添加字段来传递这些值:
type MyReconciler struct {
client client.Client
cache cache.Cache
scheme *runtime.Scheme
}
最后,你可以在创建控制器的过程中传递这些值:
err = builder.
ControllerManagedBy(mgr).
For(&mygroupv1alpha1.MyResource{}).
Owns(&corev1.Pod{}).
Complete(&MyReconciler{
client: mgr.GetClient(),
cache: mgr.GetCache(),
scheme: mgr.GetScheme(),
})
使用 Injector
controller-runtime 库提供了一个注入器(Injectors)系统,用于将共享资源注入 Reconcilers,以及其他结构体,如你自己实现的 Sources、EventHandlers 和 Predicates。
Reconciler 的实现需要实现 inject 包中的特定 Injector 接口:inject.Client
、inject.Cache
、inject.Scheme
,等等。
这些方法将在初始化时被调用,当你调用 controller.New
或 builder.Complete
。为此,需要为每个接口创建一个方法,例如:
type MyReconciler struct {
client client.Client
cache cache.Cache
scheme *runtime.Scheme
}
func (a *MyReconciler) InjectClient( c client.Client,) error {
a.client = c
return nil
}
func (a *MyReconciler) InjectCache( c cache.Cache, ) error {
a.cache = c
return nil
}
func (a *MyReconciler) InjectScheme( s *runtime.Scheme, ) error {
a.scheme = s
return nil
}
5.10.5 - 使用客户端
客户端可以用来读取和写入集群上的资源,并更新资源的状态。
Read 方法在内部使用一个基于 Informers 和 Listers 的 Cache 系统,以限制对 API 服务器的读取访问。使用这个缓存,同一个管理器的所有控制器都有对资源的读取权限,同时限制对 API 服务器的请求。
必须注意:
由读操作返回的对象是指向缓存的值的指针。你决不能直接修改这些对象。相反,你必须在修改这些对象之前创建一个返回对象的深度拷贝。
客户端的方法是通用的:它们与任何 Kubernetes 资源一起工作,无论是原生资源还是自定义资源,如果它们被传递给管理器的 schema 所知道的话。
所有的方法都会返回错误,其类型与 Client-go Clientset 方法所返回的错误相同。你可以参考第6章的 “错误和状态” 部分以了解更多信息。
获取资源的信息
Get 方法用来获取资源的信息。
Get(
ctx context.Context,
key ObjectKey,
obj Object,
opts ...GetOption,
) error
它需要一个 ObjectKey 值作为参数,表示资源的名称空间和名称,以及一个Object,表示要获得的资源的类型,并存储结果。Object 必须是一个指向类型资源的指针-例如,一个 Pod 或 MyResource 结构体。ObjectKey 类型是 type.NamespacedName
的别名,定义在 API Machinery 库中。
NamespacedName 也是嵌入在作为参数传递给 Reconcile 函数的 Request 中的对象的类型。你可以直接将 req.NamespacedName 作为 ObjectKey 传递,以获得要调和(reconcile)的资源。例如,使用下面的方法来获取要调和(reconcile)的资源:
myresource := mygroupv1alpha1.MyResource{}
err := a.client.Get(
ctx,
req.NamespacedName,
&myresource,
)
可以向 Get 请求传递一个特定的 resourceVersion
值,传递一个 client.GetOptions
结构体实例作为最后一个参数。
GetOptions 结构体实现了 GetOption 接口,包含一个具有 metav1.GetOptions
值的单一Raw字段。例如,指定一个值为 “0 " 的 resourceVersion 来获取资源的任何版本:
err := a.client.Get(
ctx,
req.NamespacedName,
&myresource,
&client.GetOptions{
Raw: &metav1.GetOptions{
ResourceVersion: "0",
},
},
)
列出资源
List 方法用于列出特定种类的资源:
List(
ctx context.Context,
list ObjectList,
opts ...ListOption,
) error
list 参数是一个 ObjectList 值,表示要列出并存储结果的资源的种类(kind)。默认情况下,list 是在所有命名空间中进行的。
List 方法接受实现 ListOption 接口的对象的零个或多个参数。这些类型由以下支持:
-
InNamespace,string 的别名,用于返回特定命名空间的资源。
-
MatchingLabels,
map[string]string
的别名,用来表示标签的列表和它们的精确值,这些标签必须被定义为资源的返回。下面的例子建立了一个MatchingLabels 结构体来过滤带有标签 “app=myapp” 的资源。matchLabel := client.MatchingLabels{ "app": "myapp", }
-
HasLabels,别名为
[]string
,用来表示标签的列表,独立于它们的值,必须为一个资源的返回而定义。下面的例子建立了一个 HasLabels 结构体来过滤带有 “app” 和 “debug” 标签的资源。hasLabels := client.HasLabels{"app", “debug”}
-
MatchingLabelsSelector,嵌入了一个
labels.Selector
接口,用来传递更高级的标签选择器。关于如何建立一个选择器的更多信息,请参见第6章的 “过滤列表结果” 部分。下面的例子建立了一个 MatchingLabelsSelector 结构,它可以作为 List 的一个选项来过滤标签 mykey 不同于 ignore 的资源。selector := labels.NewSelector() require, err := labels.NewRequirement( "mykey", selection.NotEquals, []string{"ignore"}, ) // assert err is nil selector = selector.Add(*require) labSelOption := client.MatchingLabelsSelector{ Selector: selector, }
-
MatchingFields 是
fields.Set
的别名,它本身是map[string]string
的别名,用来指示要匹配的字段和它们的值。下面的例子建立了一个 MatchingFields 结构体,用来过滤字段 “status.phase” 为 “Running” 的资源:matchFields := client.MatchingFields{ "status.phase": "Running", }
-
MatchingFieldsSelector,嵌入了一个
fields.Selector
,用来传递更高级的字段选择器。关于如何构建fields.Selector
的更多信息,请参见第6章的 “过滤列表结果” 部分。下面的例子建立了一个 MatchingFieldsSelector 结构体来过滤字段 “status.phase” 与 “Running” 不同的资源:fieldSel := fields.OneTermNotEqualSelector( "status.phase", "Running", ) fieldSelector := client.MatchingFieldsSelector{ Selector: fieldSel, }
-
Limit(别名 int64)和Continue(别名 string)用于对结果进行分页。这些选项在第二章的 “分页结果” 部分有详细介绍。
创建资源
Create 方法用来在集群中创建一个新的资源。
Create(
ctx context.Context,
obj Object,
opts ...CreateOption,
) error
作为参数传递的 obj 定义了要创建的对象的种类(kind),以及它的定义。下面的例子将在集群中创建一个 Pod:
podToCreate := corev1.Pod{ [...] }
podToCreate.SetName("nginx")
podToCreate.SetNamespace("default")
err = a.client.Create(ctx, &podToCreate)
以下选项可以作为 CreateOption 传递,以使 Create 请求参数化。
-
DryRunAll 值表示所有的操作都应该被执行,除了那些将资源持久化到存储的操作。
-
FieldOwner,别名为字符串,表示创建操作的字段管理器的名称。这个信息对于服务器端应用操作的正常工作很有用。
删除资源
删除方法是用来从集群中删除资源。
Delete(
ctx context.Context,
obj Object, k
opts ...DeleteOption,
) error
作为参数传递的 obj 定义了要删除的对象的种类(kind),以及它的命名空间(如果资源有命名空间)和它的名字。下面的例子可以用来删除 Pod。
podToDelete := corev1.Pod{}
podToDelete.SetName("nginx")
podToDelete.SetNamespace("prj2")
err = a.client.Delete(ctx, &podToDelete)
以下选项可以作为 DeleteOption 被传递,以便对 Delete 请求进行参数化。
- DryRunAll - 这个值表示所有的操作都应该被执行,除了那些将资源持久化到存储的操作。
- GracePeriodSeconds, alias to int64 - 这个值只在删除 pod 时有用。它表示在删除 pod 之前的持续时间(秒)。更多细节见第6章的 “删除资源” 部分。
- Preconditions / 前提条件,别名
metav1.Preconditions
- 这表明你期望删除的资源。更多细节请参见第6章 “删除资源” 部分。 - PropagationPolicy,别名
metav1.DeletionPropagation
- 这表明是否以及如何进行垃圾回收。更多细节见第6章 “删除资源” 部分。
删除资源集合
DeleteAllOf 方法是用来从集群中删除所有给定类型的资源。
DeleteAllOf(
ctx context.Context,
obj Object,
opts ...DeleteAllOfOption,
) error
作为参数传递的 obj 定义了要删除的对象的种类。
DeleteAllOf 操作的 opts 可选项是 List 操作(见 “列出资源” 部分)和 Delete 操作(见 “删除资源” 部分)的选项的组合。
作为例子,下面是如何删除给定命名空间的所有 deployment:
err = a.client.DeleteAllOf(
ctx,
&appsv1.Deployment{},
client.InNamespace(aNamespace))
更新资源
TBD
修补资源
Strategic Merge Patch
Merge Patch
TBD
更新资源的状态
在和 Operator 工作时,您要在自定义资源的 status 部分修改数值,以表明它的当前状态。
Update(
ctx context.Context,
obj Object,
opts ...UpdateOption,
) error
请注意,您不需要为状态进行特定的创建、获取或删除操作,因为您将在创建自定义资源本身时将状态创建为自定义资源的一部分。另外,资源的 Get 操作将返回资源的 Status 部分,删除资源将删除其 Status 部分。
然而,API 服务器强迫你对 Status 和资源的其他部分使用不同的 Update 方法,以保护你不会在同一时间错误地修改这两部分。
为了使状态的更新发挥作用,自定义资源定义必须在自定义资源的子资源列表中声明 status 字段。参见第8章的 “资源版本的定义” 部分,了解如何声明这个状态子资源。
这个方法的工作原理和资源本身的 Update 方法一样,但是要调用这个方法,你需要在 client.Status() 返回的对象上调用这个方法:
err = client.Status().Update(ctx, obj)
修补资源的状态
与更新方法一样,修补资源的状态需要一个专门的方法来处理 client.Status()
的结果。
Patch(
ctx context.Context,
obj Object,
patch Patch,
opts ...PatchOption,
) error
对 Status 的 Patch 方法与对资源本身的 Patch 方法工作相同,只是它只对资源的 Status 部分进行修补。
err = client.Status().Patch(ctx, obj, patch)
5.10.6 - 日志
TBD
5.10.7 - 事件
Kubernetes API提供了一个 事件/Event 资源,事件实例被附加到任何类型的特定实例。事件由控制器发送,以通知用户与某个对象相关的一些事件发生。在执行kubectl describe
时,会显示这些事件。例如,你可以看到与一个 pod 执行相关的事件:
$ kubectl describe pods nginx
[...]
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 1s default-scheduler Successfully assigned...
Normal Pulling 0s kubelet Pulling image "nginx"
Normal Pulled <invalid> kubelet Successfully pulled...
Normal Created <invalid> kubelet Created container nginx
Normal Started <invalid> kubelet Started container nginx
为了从 Reconcile 函数中发送此类事件,你需要访问由管理器提供的 EventRecorder 实例。在初始化过程中,你可以用管理器上的 GetEventRecorderFor 方法获得这个实例,然后在构建 Reconcile 结构体时传递这个值:
type MyReconciler struct {
client client.Client
EventRecorder record.EventRecorder
}
func main() {
[...]
eventRecorder := mgr.GetEventRecorderFor(
"MyResource",
)
err = builder.
ControllerManagedBy(mgr).
Named(controller.Name).
For(&mygroupv1alpha1.MyResource{}).
Owns(&appsv1.Deployment{}).
Complete(&controller.MyReconciler{
EventRecorder: eventRecorder,
})
[...]
}
然后,从 Reconcile 函数中,你可以调用 Event、Eventf 和 AnnotatedEventf 方法。
func (record.EventRecorder) Event(
object runtime.Object,
eventtype, reason, message string,
)
func (record.EventRecorder) Eventf(
object runtime.Object,
eventtype, reason, messageFmt string,
args ...interface{},
)
func (record.EventRecorder) AnnotatedEventf(
object runtime.Object,
annotations map[string]string,
eventtype, reason, messageFmt string,
args ...interface{},
)
object 参数表示将事件附加到哪个对象。你将传递被调和的自定义资源。
eventtype 参数接受两个值:corev1.EventTypeNormal
和 corev1.EventTypeWarning
。
reason 参数是一个 UpperCamelCase 格式的短值。message 参数是一个人类可读的文本。
Event 方法用于传递静态消息,Eventf 方法可以使用 Sprintf 创建一个消息,AnnotatedEventf 方法也可以给事件附加注释。
结论
在本章中,你已经使用 controller-runtime 库开始创建 operator。你已经看到了如何使用适当的 schema 创建 manager,如何创建控制器并声明 Reconcile 函数,并探索了库所提供的客户端的各种功能来访问集群中的资源。
下一章将探讨如何编写 Reconcile 函数。
5.11 - [第11章]编写 Reconcile 循环
在上一章中,我们已经看到了如何使用 controller-runtime 库来以引导一个新的项目来编写 Operator。在本章中,我们将重点讨论 Reconcile 函数的实现,它是实现 Operator 的重要部分。
Reconcile 函数包含 Operator 的所有业务逻辑。该函数将在一个单一的资源种类上工作 – 或者说 Operator 调和这个资源 – 并且可以在其他类型的对象触发事件时得到通知,通过使用所有者引用(Owner References)将这些其他类型映射到调和的对象上。
Reconcile 函数的作用是确保系统的状态与要 Reconcile 的资源中指定的内容相匹配。为此,它将创建 “低级” 资源来实现要调和的资源。这些资源反过来将由其他控制器或 Operator 进行调和。当这些资源的调和完成后,它们的状态将被更新以反映它们的新状态。此外,Operator 将能够检测到这些变化,并相应地调整被调和资源的状态。
举个例子,您在前一章开始实施的 Operator 调和了自定义资源 MyResource。Operator 将为实现 MyResource 实例创建 Deployment 实例,为此, Operator 也要观察 Deployment 资源。
当用于 Deployment 实例的事件被触发时,为其创建 Deployment 的 MyResource 实例将被调和。例如,在 Deployment 的 Pod 被创建后,Deployment 的状态将被更新,以表明所有副本都在运行。在这一点上,Operator 可以修改调和资源的状态,以表明它已准备就绪。
Reconcile 循环被称为观察资源的过程,当资源被创建、修改或删除时,调用 Reconcile 函数,用低级别的资源实现调和的资源,观察这些资源的状态,并相应地更新调和资源的状态。
编写 Reconcile 函数
Reconcile 函数从队列中接收一个要调和的资源。第一个要做的操作是获得关于这个资源的信息来进行调和。
事实上,只收到了资源的命名空间和名称(它的种类/kind 是由它的设计知道的,是由 operator 调和的种类),但你并没有收到资源的完整定义。
Get 操作被用来获取资源的定义。
检查资源是否存在
该资源可能由于各种原因被入队:它被创建、修改或删除(或者另一个拥有(owned)的资源被创建、修改或删除)。在前两种情况下(创建或修改),Get 操作将成功,Reconcile 函数将知道这时资源的定义。在删除的情况下,获取操作将以 Notfound 错误失败,因为该资源现在已经被删除了。
Operator 的良好做法是,在调和资源时为其创建的资源添加 OwnerReferences。主要目的是当这些创建的资源被修改时,能够调和其所有者,而添加这些 OwnerReferences 的结果是,当所有者资源被删除时,这些拥有(owned)的资源将被 Kubernetes 垃圾收集器删除。
出于这个原因,当一个被调和的对象被删除时,集群中没有什么可做的,因为相关的创建资源的删除将由集群处理。
实现调和的资源
如果在集群中找到了要调和的资源,operator’s 的下一步就是创建 “低级” 资源来实现这个要调和的资源。
因为 Kubernetes 是一个声明式平台,创建这些资源的好方法是声明低级资源应该是什么,与集群中存在或不存在什么无关,并依靠 Kubernetes 控制器来接管和调和这些低级资源。
由于这个原因,不可能盲目地使用 Create 方法,因为我们不确定资源是否存在,如果资源已经存在,操作就会失败。
你可以检查资源是否存在,如果不存在则创建,如果存在则修改。正如前几章所示,服务器端应用方法非常适合这种情况:在运行 Apply 操作时,如果资源不存在,就会被创建;如果资源存在,就会被修补,在资源被其他参与者修改的情况下解决冲突。
使用服务器端的 apply 方法,Operator 不需要检查资源是否存在,或是否被另一个参与者修改过。Operator 只需要从 Operator 的角度,用资源的定义运行服务器端 Apply 操作。
在应用低级别的资源后,应该考虑两种可能性。
- 情况1:如果低层资源已经存在,并且没有被 Apply 修改,那么将不会为这些资源触发 MODIFIED 事件,并且 Reconcile 函数将不会被再次调用(至少对于这个 Apply 操作)。
- 情况2:如果低层资源被创建或修改,这将触发这些资源的 CREATED 或 MODIFIED 事件,并且 Reconcile 函数将因为这些事件而被再次调用。这个函数的新执行将再次应用低层资源,如果在此期间没有更新这些资源,Operator 将落入情况1。
新的底层资源最终将由其各自的 Operator 或控制器处理。反过来,他们将调和这些资源,并更新它们的状态,宣布它们的当前状态。
一旦这些低层资源的状态被更新,这些资源的 MODIFIED 事件将被触发,Reconcile 函数将被再次调用。再一次,Operator 将应用这些资源,案例1和2必须被考虑。
Operator 在某些时候需要读取它所创建的低级资源的状态,以便计算出调和后的资源的状态。在简单的情况下,这可以在执行低级资源的服务器端应用后完成。
简单的实现例子
为了说明这一点,这里有一个完整的 Reconcile 函数,该函数适用于 Operator,该 Operator 用 MyResource 实例中提供的镜像和内存信息创建 Deployment。
func (a *MyReconciler) Reconcile(
ctx context.Context,
req reconcile.Request,
) (reconcile.Result, error) {
log := log.FromContext(ctx)
log.Info("getting myresource instance")
myresource := mygroupv1alpha1.MyResource{}
err := a.client.Get( ❶
ctx,
req.NamespacedName,
&myresource,
&client.GetOptions{},
)
if err != nil {
if errors.IsNotFound(err) { ❷
log.Info("resource is not found")
return reconcile.Result{}, nil
}
return reconcile.Result{}, err
}
ownerReference := metav1.NewControllerRef( ❸
&myresource,
mygroupv1alpha1.SchemeGroupVersion.
WithKind("MyResource"),
)
err = a.applyDeployment( ❹
ctx,
&myresource,
ownerReference,
)
if err != nil {
return reconcile.Result{}, err
}
status, err := a.computeStatus(ctx, &myresource) ❺
if err != nil {
return reconcile.Result{}, err
}
myresource.Status = *status
log.Info("updating status", "state", status.State)
err = a.client.Status().Update(ctx, &myresource) ❻
if err != nil {
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
-
❶ 获取要调和的资源的定义
-
❷ 如果资源不存在,立即返回
-
❸ 建立指向要调和的资源的 ownerReference
-
❹ 使用服务器端应用于 “低层次” deployment
-
❺ 根据 “低级别” deployment 计算资源的状态
-
❻ 更新资源的状态以进行调和
下面是一个为 operator 创建的 deployment 实现服务器端 Apply 操作的例子:
func (a *MyReconciler) applyDeployment(
ctx context.Context,
myres *mygroupv1alpha1.MyResource,
ownerref *metav1.OwnerReference,
) error {
deploy := createDeployment(myres, ownerref)
err := a.client.Patch( ❼
ctx,
deploy,
client.Apply,
client.FieldOwner(Name),
client.ForceOwnership,
)
return err
}
func createDeployment(
myres *mygroupv1alpha1.MyResource,
ownerref *metav1.OwnerReference,
) *appsv1.Deployment {
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"myresource": myres.GetName(),
},
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"myresource": myres.GetName(),
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"myresource": myres.GetName(),
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "main",
Image: myres.Spec.Image, ❽
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: myres.Spec.Memory, ❾
},
},
},
},
},
},
},
}
deploy.SetName(myres.GetName() + "-deployment")
deploy.SetNamespace(myres.GetNamespace())
deploy.SetGroupVersionKind(
appsv1.SchemeGroupVersion.WithKind("Deployment"),
)
deploy.SetOwnerReferences([]metav1.OwnerReference{ ❿
*ownerref,
})
return deploy
}
❼ 使用补丁方法执行服务器端的应用操作
❾ 使用资源中定义的镜像来进行调和
❾ 使用资源中定义的内存来进行调和
❿ 设置 OwnerReference 指向要调和的资源
然后,下面是一个如何实现状态的计算和更新的例子:
const (
_buildingState = "Building"
_readyState = "Ready"
)
func (a *MyReconciler) computeStatus(
ctx context.Context,
myres *mygroupv1alpha1.MyResource,
) (*mygroupv1alpha1.MyResourceStatus, error) {
logger := log.FromContext(ctx)
result := mygroupv1alpha1.MyResourceStatus{
State: _buildingState,
}
deployList := appsv1.DeploymentList{}
err := a.client.List( ⓫
ctx,
&deployList,
client.InNamespace(myres.GetNamespace()),
client.MatchingLabels{
"myresource": myres.GetName(),
},
)
if err != nil {
return nil, err
}
if len(deployList.Items) == 0 {
logger.Info("no deployment found")
return &result, nil
}
if len(deployList.Items) > 1 {
logger.Info(
"too many deployments found", "count",
len(deployList.Items),
)
return nil, fmt.Errorf(
"%d deployment found, expected 1",
len(deployList.Items),
)
}
status := deployList.Items[0].Status ⓬
logger.Info(
"got deployment status",
"status", status,
)
if status.ReadyReplicas == 1 {
result.State = _readyState ⓭
}
return &result, nil
}
-
⓫ 获取为该资源创建的 deployment 以进行调和
-
⓬ 获取找到的唯一 deployment 的状态
-
⓭ 当 replicas 为1时,为调和的资源设置就绪状态
总结
本章展示了如何为一个简单的 operator 实现 Reconcile 功能,该 operator 使用来自自定义资源的信息创建 deployment。
真正的 operator 通常会比这个简单的例子更复杂,他们会创建几个具有更复杂生命周期的资源,但它展示了开始编写 operator 时需要知道的要点: Reconcile 循环的声明性,所有者引用的使用,服务器端应用的使用,以及状态的更新方式。
下一章将展示如何测试 Reconcile 循环。
5.12 - [第12章]测试 Reconcile 循环
上一章描述了如何为 Operator 调和自定义资源实现一个简单但完整的 Reconcile 函数。
为了测试你在上一章编写的 Reconcile 函数,你将使用 ginkgo,它是Go的一个测试框架;以及 controller-runtime 库中的e nvtest 包,它提供了一个 Kubernetes 环境用于测试。
envtest 包
Controller-runtime 库提供了 envtest 包。这个包通过启动简单的本地控制平面来提供 Kubernetes 环境。
默认情况下,该包使用位于 /usr/local/kubebuilder/bin
的 etcd
和 kube-apiserver
的本地二进制文件,你也可以提供自己的路径来找到这些二进制文件。你可以安装 setup-envtest
来获得不同版本的 Kubernetes 的这些二进制文件。
安装envtest二进制文件
setup-envtest 工具可以用来安装 envtest 使用的二进制文件。要安装该工具,你必须运行:
$ go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
然后,你可以使用以下命令为特定的 Kubernetes 版本安装二进制文件:
$ setup-envtest use 1.23
Version: 1.23.5
OS/Arch: linux/amd64
Path: /path/to/kubebuilder-envtest/k8s/1.23.5-linux-amd64
该命令的输出将告诉你二进制文件被安装在哪个目录下。如果你想从默认目录 /usr/local/kubebuilder/bin
中使用这些二进制文件,你可以创建一个符号链接,从那里访问它们:
$ sudo mkdir /usr/local/kubebuilder
$ sudo ln -s /path/to/kubebuilder-envtest/k8s/1.23.5-linux-amd64 /usr/local/kubebuilder/bin
或者,如果你喜欢使用 KUBEBUILDER_ASSETS 环境变量来定义包含二进制文件的目录,你可以执行:
$ source <(setup-envtest use -i -p env 1.23.5)
$ echo $KUBEBUILDER_ASSETS
/path/to/kubebuilder-envtest/k8s/1.23.5-linux-amd64
这将定义并导出 KUBEBUILDER_ASSETS 变量,其路径包含 Kubernetes 1.23.5 的二进制文件。
使用envtest
控制平面将只运行 API 服务器和 etcd,但没有控制器。这意味着,当你想测试的 operator 将创建 Kubernetes 资源时,没有控制器会做出反应。例如,如果 operator 创建了 Deployment ,不会有 pod 被创建,而且 Deployment 状态也不会被更新。
这在一开始可能会令人惊讶,但这将有助于你只测试你的 operator,而不是 Kubernetes 控制器。为了创建测试环境,你首先需要创建一个 envtest.Environment
结构体的实例。
使用环境结构的默认值将启动一个本地控制平面,使用 /usr/local/kubebuilder/bin
中的二进制文件或来自 KUBEBUILDER_ASSETS 环境变量中定义的目录。
如果你正在编写 operator 调和自定义资源,你将需要为这个自定义资源添加自定义资源定义(CRD)。为此,你可以使用 CRDDirectoryPaths 字段来传递包含 YAML 或 JSON 格式的 CRD 定义的目录列表。所有这些定义将在初始化环境时被应用到本地集群。
如果你想在 CRD 目录不存在时被改变,ErrorIfCRDPathMissing 字段很有用。作为一个例子,下面是如何创建一个 Environment 结构体,CRD YAML或JSON文件位于 ././crd
目录下:
import (
"path/filepath"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "crd"),
},
ErrorIfCRDPathMissing: true,
}
要启动 environment ,你可以使用该 Environment 的 Start 方法:
cfg, err := testEnv.Start()
这个方法返回一个 rest.Config
值,这是用来连接到由环境启动的本地集群的 Config 值。在测试结束时,可以使用 Stop 方法停止 Environment:
err := testEnv.Stop()
一旦 Environment 启动,并且你有一个 Config (配置),你就可以创建管理器和控制器,并启动管理器,如第10章所述。
定义 ginkgo 套件
为了启动 Reconcile 函数的测试,你可以使用 go test
功能来启动 ginkgo specs:
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestMyReconciler_Reconcile(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t,
"Controller Suite",
)
}
然后,你可以声明 BeforeSuite 和 AfterSuite 函数,它们用于启动/停止环境和管理器。
下面是这些函数的例子,它将创建一个加载了 MyResource 的 CRD 的环境。在 BeforeSuite 函数的末尾,Manager 在 Go 例程中被启动,因此测试可以在主 Go 例程中执行。
注意,你正在创建一个可取消的上下文,在启动管理器时使用,所以你可以通过取消 AfterSuite 函数的上下文来停止管理器。
import (
"context"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/manager"
mygroupv1alpha1 "github.com/feloy/myresource-crd/pkg/apis/mygroup.example.com/v1alpha1"
)
var (
testEnv *envtest.Environment ❶
ctx context.Context
cancel context.CancelFunc
k8sClient client.Client ❷
)
var _ = BeforeSuite(func() {
log.SetLogger(zap.New(
zap.WriteTo(GinkgoWriter),
zap.UseDevMode(true),
))
ctx, cancel = context.WithCancel( ❸
context.Background(),
)
testEnv = &envtest.Environment{ ❹
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "crd"),
},
ErrorIfCRDPathMissing: true,
}
var err error
// cfg is defined in this file globally.
cfg, err := testEnv.Start() ❺
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
scheme := runtime.NewScheme() ❻
err = clientgoscheme.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
err = mygroupv1alpha1.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
mgr, err := manager.New(cfg, manager.Options{ ❼
Scheme: scheme,
})
Expect(err).ToNot(HaveOccurred())
k8sClient = mgr.GetClient() ❽
err = builder. ❾
ControllerManagedBy(mgr).
Named(Name).
For(&mygroupv1alpha1.MyResource{}).
Owns(&appsv1.Deployment{}).
Complete(&MyReconciler{})
go func() {
defer GinkgoRecover()
err = mgr.Start(ctx) ❿
Expect(err).ToNot(
HaveOccurred(),
"failed to run manager",
)
}()
})
var _ = AfterSuite(func() {
cancel() ⓫
err := testEnv.Stop() ⓬
Expect(err).NotTo(HaveOccurred())
})
TBD
结语
本章结束了对可用于编写 Kubernetes Operators 的各种概念和库的介绍。
第8章介绍了自定义资源,允许通过在服务资源列表中添加新资源来扩展 Kubernetes API。第9章介绍了使用Go语言处理自定义资源的各种方法,可以通过为该资源生成 clientset,也可以使用 DynamicClient。
第10章介绍了 controller-runtime 库,对于实现管理自定义资源生命周期的 Operator 很有用。第11章着重于编写 Operator 的业务逻辑,第12章用来测试这个逻辑。
下一章介绍了 kubebuilder SDK,这是一个使用前几章中介绍的工具的框架。这个框架通过生成新的自定义资源定义和相关 Operator 的代码,以及提供构建和将这些自定义资源定义和 Operator 部署到集群的工具,促进了 Operator 的开发。
5.13 - [第13章]使用 kubebuilder 创建 Operator
在前面的章节中,你已经看到了如何使用自定义资源定义(CRD)来定义由 API 服务器提供的新资源,以及如何使用 controller-runtime 库来构建操作器。
Kubebuilder SDK 致力于帮助你创建新的资源和相关的 Operators。它提供的命令可以启动一个定义管理器的项目,并将资源和它们相关的控制器添加到项目中。
一旦生成了新的自定义资源和控制器的源代码,你将需要根据资源的业务领域来实现缺少的部分。然后,Kubebuilder SDK 提供工具来构建和部署自定义资源定义和管理器到集群上。
安装Kubebuilder
Kubebuilder 是以一个单一的二进制文件提供的。你可以从项目的发布页面下载二进制文件,并将其安装到你的PATH中。二进制文件被提供给 Linux 和 MacOS 系统。
创建项目
第一步是创建项目。该项目最初将包含:
-
定义管理器的Go源代码(目前没有任何控制器)
-
Docker文件,用于构建包含管理器二进制文件的镜像,以部署到集群上
-
Kubernetes 清单,以帮助将管理器部署到集群中。
-
一个定义命令的Makefile,以帮助你测试、构建和部署管理器。
要创建项目,首先创建一个空目录, cd 进入这个目录,然后执行以下 kubebuilder init
命令:
$ mkdir myresource-kb
$ cd myresource-kb
$ kubebuilder init
--domain myid.dev ❶
--repo github.com/myid/myresource ❷
-
❶ 域名,作为 GVK 分组名称的后缀使用。在这个项目中定义的自定义资源可以属于不同的分组,但所有分组将属于同一个域。例如,
mygroup1.myid.dev
和mygroup2.myid.dev
-
❷ 用于生成管理器 Go 代码的 Go 模块的名称
你可以通过运行以下命令检查 Makefile 中的可用命令:
$ make help
你可以用以下命令为管理器构建二进制文件:
$ make build
然后,你可以在本地运行管理器:
$ make run
或者
$ ./bin/manager
在这一点上,启动一个源码控制项目(例如,一个git项目),并使用生成的文件创建第一个修订版是很有意思的。这样,你将能够检查接下来执行的 kubebuilder 命令所做的修改。例如,如果你使用git:
$ git init
$ git commit -am 'kubebuilder init --domain myid.dev --repo github.com/myid/myresource'
在项目中添加自定义资源
就目前而言,管理器并不管理任何控制器。即使你能构建和运行它,你也不能对它做任何事情。
下一个要执行的 kubebuilder 命令是 kubebuilder create api,以添加一个自定义资源和它相关的控制器到项目中。该命令会问你是否要创建资源和控制器。对每个问题回答Y。
$ kubebuilder create api
--group mygroup ❶
--version v1alpha1 ❷
--kind MyResource ❸
Create Resource [y/n]
y
Create Controller [y/n]
y
-
❶ 自定义资源的分组。它将以域名为后缀,形成完整的 GVK 分组。
-
❷ 该资源的版本
-
❸ 资源的种类/kind
你可以通过运行下面的 git 命令看到该命令所做的修改:
$ git status
On branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: PROJECT
modified: go.mod
modified: go.sum
modified: main.go
Untracked files:
(use "git add <file>..." to include in what will be committed)
api/
config/crd/
config/rbac/myresource_editor_role.yaml
config/rbac/myresource_viewer_role.yaml
config/samples/
controllers/
no changes added to commit (use "git add" and/or "git commit -a")
PROJECT 文件包含项目的定义。它最初包含了作为标志提供给 init 命令的域名和 repo。现在它也包含了第一个资源的定义。main.go 文件和 controllers 目录为自定义资源定义了一个控制器。
api/v1alpha1
目录已经创建,包含了使用 Go 结构体的自定义资源的定义,以及由 deepcopy-gen(见第8章 “运行deepcopy-gen” 部分)在 controller-gen 工具的帮助下生成的代码。它还包含了 AddToScheme 函数的定义,对于将这种新的自定义资源添加到 Scheme 中非常有用。
config/samples
目录包含一个新文件,定义了 YAML 格式的自定义资源的实例。config/rbac
目录包含两个新文件,定义了两个新的 ClusterRole 资源,一个用于查看,一个用于编辑 MyResource 实例。config/crd
目录包含用于构建 CRD 的 kustomize 文件。
添加RBAC注解
在本地运行 operator 时,operator 会使用您的 kubeconfig 文件,以及该 kubeconfig 的特定授权。如果您使用集群管理员账户进行连接,操作员就会拥有集群上的所有授权,并能进行任何操作。
然而,当 operator 被部署在集群上时,它是用一个特定的 Kubernetes Service Account 运行的,并被赋予有限的授权。这些授权是由 kubebuilder 构建和部署的 ClusterRole 中定义的。
为了帮助 Kubebuilder 构建这个 ClusterRole,在 Reconcile 函数的生成注释中出现了注解(为了清晰起见,加入了换行符):
//+kubebuilder:rbac:
groups=mygroup.myid.dev,
resources=myresources,
verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:
groups=mygroup.myid.dev,
resources=myresources/status,
verbs=get;update;patch
//+kubebuilder:rbac:
groups=mygroup.myid.dev,
resources=myresources/finalizers,
verbs=update
这些规则将给予对 MyResource 资源的完全访问权,但没有对其他资源的访问权。
Reconcile 函数需要在观察 deployment 资源时拥有对它的读写权限;为此,你需要添加这个新的注解(已添加换行符):
//+kubebuilder:rbac:
groups=apps,
resources=deployments,
verbs=get;list;watch;create;update;patch;delete
在群集上部署 operator
为了能够将 operator 部署到集群中,您需要构建容器镜像,并将其部署到容器镜像登记处(如DockerHub、Quay.io或其他许多地方)。
第一步是在您喜欢的容器镜像登记处创建一个新的存储库,包含 operator 容器的镜像。假设您在 quay.io/myid
中创建了一个名为 myresource 的存储库,镜像的全名将是 quay.io/myid/myresource
。
在每次构建时,你都需要为镜像使用不同的标签,这样才能正确地更新容器。要在本地构建镜像,你需要运行以下命令(注意标签 v1alpha1-1):
$ make docker-build
IMG=quay.io/myid/myresource:v1alpha1-1
要将其部署到注册表,必须执行以下命令:
$ make docker-push
IMG=quay.io/myid/myresource:v1alpha1-1
最后,将 Operator 部署到集群中:
$ make deploy
IMG=quay.io/myid/myresource:v1alpha1-1
这将创建一个新的命名空间,myresource-kb-system,其中包含一个新的 deployment,将执行 Operator。您可以用以下命令检查 Operator 的日志:
6 - Programming Kubernetes with go
https://learning.oreilly.com/library/view/programming-kubernetes/9781492047094/titlepage01.html