作为kuberntes生态系统中最核心的扩展之一 , Custom Resources(CR) 扮演这重要角色 , 许多成功的项目都离不开CR的使用 ,比如Istio, Linkerd 2.0, AWS App Mesh等等
Kubernetes cluster version 1.7之后, CR作为主要的 Kubernetes API 资源存储在etcd 中 , 如下图所示,如果请求不是以下任何一个请求,则返回到 apiextensions-apiserver,该服务器为通过 CRDs 定义的资源提供服务
请求如果是aggregation的就使用aggregated API servers处理
如果是原生请求则用Native Kubernetes resources处理
如果是自定义资源 , 则用 apiextensions-apiserver处理
一个典型的crd资源如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: ats.cnat.programming-kubernetes.info spec: group: cnat.programming-kubernetes.info names: kind: At listKind: AtList plural: ats singular: at scope: Namespaced subresources: status: {} version: v1alpha1 versions: - name: v1alpha1 served: true storage: true
kubectl 使用来自 API 服务器的发现信息来发现新资源
我们用kuberntes的verbosity level来看发生了什么
1 2 3 4 5 6 7 8 9 $ kubectl get ats -v=7 ... GET https://XXX.eks.amazonaws.com/apis/cnat.programming-kubernetes.info/ v1alpha1/namespaces/cnat/ats?limit =500 ... Request Headers: ... Accept: application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json User-Agent: kubectl/v1.14.0 (darwin/amd64) kubernetes/641856d ... Response Status: 200 OK in 607 milliseconds NAME AGE example-at 43s
起初 , kubectl并不知道自定义资源的存在
kubectl向API server的/apis的所有已存在的API groups发起请求
接下来,kubectl 通过 / apis / group 版本组发现端点向 API 服务器询问所有现有 API 组中的资源
然后,kubectl 将给定类型 ats 转换为三重类型
Group ( cnat.programming-kubernetes.info
)
Version ( v1alpha1
)
Resource (ats
)
在最后一步中,发现端点提供了进行转化所需的所有信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 $ http localhost:8080/apis/ { "groups" : [{ "name" : "at.cnat.programming-kubernetes.info" , "preferredVersion" : { "groupVersion" : "cnat.programming-kubernetes.info/v1" , "version" : "v1alpha1“ }, " versions": [{ " groupVersion": " cnat.programming-kubernetes.info/v1alpha1", " version": " v1alpha1" }] }, ...] } $ http localhost:8080/apis/cnat.programming-kubernetes.info/v1alpha1 { " apiVersion": " v1", " groupVersion": " cnat.programming-kubernetes.info/v1alpha1", " kind": " APIResourceList", " resources": [{ " kind": " At", " name": " ats", " namespaced": true, " verbs": [" create", " delete", " deletecollection", " get", " list", " patch", " update", " watch" ] }, ...] }
这些都是通过发现 RESTMapper 实现的
kubectl会在 ~/.kubectl目录下把自己发现的所有资源类型缓存下来 , 每过10分钟失效一次,重新 开始开始发现, 所有我们自定义的资源会在10分钟后出现在此缓存中
自定义资源的编写 自定义资源就是CRD 格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: name spec: group: group name version: version name names: kind: uppercase name plural: lowercase plural name singular: lowercase singular name shortNames: list of strings as short names listKind: uppercase list kind categories: list of category membership like "all" validation: openAPIV3Schema: OpenAPI schema subresources: status: {} scale: specReplicasPath: JSON path for the replica number in the spec of the custom resource statusReplicasPath: JSON path for the replica number in the status of the custom resource labelSelectorPath: JSON path of the Scale.Status.Selector field in the scale resource versions: - name: version name served: boolean whether the version is served by the API server storage: boolean whether this version is the version used to store object - ...
定义之后提交, API server就开始可以进行验证了, API server验证这些字段是采用OpenAPI v3 schema 模式完成的, OpenAPI schema是基于JSON Schema standard , 使用JSON/YAML来表达一个schema , 下面是个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type: object properties: apiVersion: type: string kind: type: string metadata: type: object spec: type: object properties: schedule: type: string pattern: "^\d{4}-([0]\d|1[0-2])-([0-2]\d|3[01])..." command: type: string required: - schedule - command status: type: object properties: phase: type: string required: - metadata - apiVersion - kind - spec
上述schema其实是个JSON object , 每个JSON object中的spec 必须要求有2个字段:schedule
and command
, schedule匹配一个ISO date(这里是用正则表达式匹配)
Short Names and Categories 和本地资源一样, 自定义资源也可以用自己的短名称 , 比如namespace的简称就是ns , 我们可以用kubectl get ns
来获取
查看所有可用的短名称 , 我们可以使用以下命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ kubectl api-resources NAME SHORTNAMES APIGROUP NAMESPACED KIND bindings true Binding componentstatuses cs false ComponentStatus configmaps cm true ConfigMap endpoints ep true Endpoints events ev true Event limitranges limits true LimitRange namespaces ns false Namespace nodes no false Node persistentvolumeclaims pvc true PersistentVolumeClaim persistentvolumes pv false PersistentVolume pods po true Pod statefulsets sts apps true StatefulSet ...
如何自定义短名称呢
1 2 3 4 5 6 7 8 apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: ats.cnat.programming-kubernetes.info spec: ... shortNames: - at
当然还可以把他们加入一个categories
1 2 3 4 5 6 7 8 apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: ats.cnat.programming-kubernetes.info spec: ... categories: - all
Printer Columns Kubectl CLI 工具使用服务器端打印来呈现 kubectl get 的输出。 这意味着它会向 API 服务器查询要显示的列和每一行中的值
通过 additionalPrinterColumns,自定义资源也支持服务器端打印列,它们被称为“ additional”
1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: ats.cnat.programming-kubernetes.info spec: additionalPrinterColumns: (optional) - name: kubectl column name type: OpenAPI type for the column format: OpenAPI format for the column (optional) description: human-readable description of the column (optional) priority: integer, always zero supported by kubectl JSONPath: JSON path inside the CR for the displayed value
比如我们在自定义资源中添加如下字段
1 2 3 4 5 6 7 8 9 10 additionalPrinterColumns: - name: schedule type: string JSONPath: .spec.schedule - name: command type: string JSONPath: .spec.command - name: phase type: string JSONPath: .status.phase
之后我们使用kubectl来获取
1 2 3 $ kubectl get ats NAME SCHEDULER COMMAND PHASE foo 2019-07-03T02:00:00Z echo "hello world" Pending
子资源状态 status状态子资源启用 例子:
1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition spec: ... versions: - name: v1alpha1 served: true storage: true - name: v1beta1 served: true subresources: status: {}
其中status中的字段要为空, 这是因为这些字段的填写是系统填写的
scale子资源 它允许我们查看和修改副本值
比如我们想用kubectl scale来扩展副本数量
1 2 3 $ kubectl scale --replicas=3 your-custom-resource -v=7 I0429 21:17:53.138353 66743 round_trippers.go:383] PUT https://host/apis/group/v1/your-custom-resource/scale
在进行自定义资源定义的时候 , 我们可以
1 2 3 4 5 6 7 8 9 apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition spec: subresources: scale: specReplicasPath: .spec.replicas statusReplicasPath: .status.replicas labelSelectorPath: .status.labelSelector ...
这样replica的值就由从GET返回的值填充到spec.replicas中去了
使用golang访问Custom resources 我们可以使用多种client来访问Custom resources
使用dynamic client
使用typed client
目前有2种方式生成类型化客户端
Dynamic Client Dynamic Client不使用RESTMapper , 得手动添加类型
1 2 3 4 5 schema.GroupVersionResource{ Group: "apps" , Version: "v1" , Resource: "deployments" , }
如果 REST 客户端配置可用 ,动态客户端可以在一行中创建
1 client, err := NewForConfig(cfg)
对给定的 GVR 的 REST 访问同样简单
1 2 client.Resource(gvr). Namespace(namespace).Get("foo" , metav1.GetOptions{})
Typed Clients 常见的一个做法是把它放进一个约定的包里
然后在types.go 里面定义类型
每一个与 GVK 相对应的 Golang 类型都嵌入了来自包装 k8s.io/apimachinery/pkg/apis/meta/v1的 TypeMeta 结构。 只包含 Kind 和 ApiVersion 字段
1 2 3 4 5 6 type TypeMeta struct { // +optional APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` // +optional Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` }
此外还需要一个名称以及名称空间等等字段, 他们存储在一个名为 ObjectMeta 的结构体中,该结构体位在k8s.io/apimachinery/pkg/apis/meta/v1文件包中
1 2 3 4 5 6 7 8 9 10 11 type ObjectMeta struct { Name string `json:"name,omitempty"` Namespace string `json:"namespace,omitempty"` UID types.UID `json:"uid,omitempty"` ResourceVersion string `json:"resourceVersion,omitempty"` CreationTimestamp Time `json:"creationTimestamp,omitempty"` DeletionTimestamp *Time `json:"deletionTimestamp,omitempty"` Labels map [string ]string `json:"labels,omitempty"` Annotations map [string ]string `json:"annotations,omitempty"` ... }
Kubernetes 顶级类型(即那些具有嵌入式 TypeMeta 和嵌入式 ObjectMeta 的类型,并且在本例中被持久化到 etcd 中)看起来非常相似,因为它们通常具有 spec 和status。 看看这个来自 k8s.io/kubernetes/apps/v1/types.go 的部署示例
1 2 3 4 5 6 7 type Deployment struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec DeploymentSpec `json:"spec,omitempty"` Status DeploymentStatus `json:"status,omitempty"` }
golang包的组织 Golang 类型通常放在一个名为 types.go 的文件中,放在包pkg/apis/group/version 中 , 除了这个文件还有其他文件
doc.go文件描述了 API 的用途,并包含了一些 package-global 代码生成标签
register.go 文件将custom resource Golang types注册到scheme中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package versionimport ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" group "repo/pkg/apis/group" ) var SchemeGroupVersion = schema.GroupVersion{ Group: group.GroupName, Version: "version" , } func Kind (kind string ) schema .GroupKind { return SchemeGroupVersion.WithKind(kind).GroupKind() } func Resource (resource string ) schema .GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } var ( SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) AddToScheme = SchemeBuilder.AddToScheme ) func addKnownTypes (scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &SomeKind{}, &SomeKindList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil }
之后zz_generated.deepcopy.go 文件自动生成 , 它定义了custom resource Golang top-level types的deep-copy method , 关于如何用go的注释标签自动生成代码, 我们下一讲再说
通过client-gen生成typed client 在pkg/apis/group/version 这个地方 , client generator client-gen
在pkg/generated/clientset/versioned (老版本是/client/clientset/versioned里面生成)生成了一个typed client
它长的是这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package versionedimport ( discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" flowcontrol "k8s.io/client-go/util/flowcontrol" cnatv1alpha1 ".../cnat/cnat-client-go/pkg/generated/clientset/versioned/ ) type Interface interface { Discovery() discovery.DiscoveryInterface CnatV1alpha1() cnatv1alpha1.CnatV1alpha1Interface } // Clientset contains the clients for groups. Each group has exactly one // version included in a Clientset. type Clientset struct { *discovery.DiscoveryClient cnatV1alpha1 *cnatv1alpha1.CnatV1alpha1Client } // CnatV1alpha1 retrieves the CnatV1alpha1Client func (c *Clientset) CnatV1alpha1() cnatv1alpha1.CnatV1alpha1Interface { return c.cnatV1alpha1 } // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { ... } // NewForConfig creates a new Clientset for the given config. func NewForConfig(c *rest.Config) (*Clientset, error) { ... }
这个clinet set由interface Interface
表示,并为每个版本提供对 API 组客户端接口的访问接口ー例如,下面的示例代码中的 cnatv1alpha1接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 type CnatV1alpha1Interface interface { RESTClient() rest.Interface AtsGetter } type AtsGetter interface { Ats(namespace string ) AtInterface } type AtInterface interface { Create(*v1alpha1.At) (*v1alpha1.At, error) Update(*v1alpha1.At) (*v1alpha1.At, error) UpdateStatus(*v1alpha1.At) (*v1alpha1.At, error) Delete(name string , options *v1.DeleteOptions) error DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error Get(name string , options v1.GetOptions) (*v1alpha1.At, error) List(opts v1.ListOptions) (*v1alpha1.AtList, error) Watch(opts v1.ListOptions) (watch.Interface, error) Patch(name string , pt types.PatchType, data []byte , subresources ...string ) (result *v1alpha1.At, err error) AtExpansion }
可以使用 NewForConfig 函数创建client set的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/clientcmd" client "github.com/.../cnat/cnat-client-go/pkg/generated/clientset/versioned" ) kubeconfig = flag.String("kubeconfig" , "~/.kube/config" , "kubeconfig file" ) flag.Parse() config, err := clientcmd.BuildConfigFromFlags("" , *kubeconfig) clientset, err := client.NewForConfig(config) ats := clientset.CnatV1alpha1Interface().Ats("default" ) book, err := ats.Get("kubernetes-programming" , metav1.GetOptions{})
Operator SDK and Kubebuilder的controller-runtime Client 下面是个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import ( "flag" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/clientcmd" runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" ) kubeconfig = flag.String("kubeconfig" , "~/.kube/config" , "kubeconfig file path" ) flag.Parse() config, err := clientcmd.BuildConfigFromFlags("" , *kubeconfig) cl, _ := runtimeclient.New(config, client.Options{ Scheme: scheme.Scheme, }) podList := &corev1.PodList{} err := cl.List(context.TODO(), client.InNamespace("default" ), podList)
在上述中 client object的List()方法接受任何一个给定scheme中的runtime.Object , 将corev1.PodList这个go类型映射到GVK , 之后由GVR获取pod , 即通过 api/v1/namespace/default/pods*来获取schema.GroupVersionResource{“”, “v1”, “pods”}