mac 上学习k8s系列(20)CRD (part II)

2022-08-02 19:32:58 浏览数 (2)

Kubernetes目前常使用CRD Controller的方式扩展API,官方提供了CRD代码的自动生成器code-generator。

最新的工具kubebuilder ,已经非常方便我们完成 CRD/Controller,甚至 Operator 的开发(当然 Operator 的开发也有专用的 operator-sdk开源框架)

和k8s.io/code-generator类似,是一个码生成工具,用于为你的CRD生成kubernetes-style API实现。区别在于:

  • Kubebuilder不会生成informers、listers、clientsets,而code-generator会。
  • Kubebuilder会生成Controller、Admission Webhooks,而code-generator不会。
  • Kubebuilder会生成manifests yaml,而code-generator不会。
  • Kubebuilder还带有一些其他便利性设施。

使用步骤如下:

代码语言:javascript复制
go mod init my.domain

1, 初始化

代码语言:javascript复制
kubebuilder init --domain example.com --license apache2 --owner "The Kubernetes authors"

2, 创建CRD api

代码语言:javascript复制
kubebuilder create api --group webapp --version v1 --kind Frigate

3, 安装CRD

代码语言:javascript复制
make install

4, 启动controller(本地)

代码语言:javascript复制
make run

思路基本一致,本文还是从古老的官方code-generator进行介绍,官方工具地址:

代码语言:javascript复制
k8s.io/code-generator/generate-groups.sh

code-generator提供了以下工具为kubernetes中的资源生成代码:

1,deepcopy-gen: 生成深度拷贝方法,避免性能开销

deepcopy-gen是用于自动生成DeepCopy函数的工具,使用方法:

代码语言:javascript复制
在文件中添加注释 //  k8s:deepcopy-gen=package
为单个类型添加自动生成 //  k8s:deepcopy-gen=true
为单个类型关闭自动生成 //  k8s:deepcopy-gen=false

2,client-gen:为资源生成标准的操作方法

(get,list,create,update,patch,delete,deleteCollection,watch),在 pkg/apis/GROUP/{VERSION}/types.go中使用,使用

// genclient标记对应类型生成的客户端, 如果与该类型相关联的资源不是命名空间范围的(例如PersistentVolume), 则还需要附加

// genclient:nonNamespaced标记,

代码语言:javascript复制
//  genclient - 生成默认的客户端动作函数(create, update, delete, get, list, update, patch, watch以及 是否生成updateStatus取决于.Status字段是否存在)。
//  genclient:nonNamespaced - 所有动作函数都是在没有名称空间的情况下生成
//  genclient:onlyVerbs=create,get - 指定的动作函数被生成.
//  genclient:skipVerbs=watch - 生成watch以外所有的动作函数.
//  genclient:noStatus - 即使 .Status字段存在也不生成updateStatus动作函数

3,informer-gen: 生成informer,提供事件机制来相应kubernetes的event

4,lister-gen: 为get和list方法提供只读缓存层

5,conversion-gen是用于自动生成在内部和外部类型之间转换的函数的工具。一般的转换代码生成任务涉及三套程序包:

一套包含内部类型的程序包,

一套包含外部类型的程序包,

单个目标程序包(即,生成的转换函数所在的位置,以及开发人员授权的转换功能所在的位置)。包含内部类型的包在Kubernetes的常规代码生成框架中扮演着称为 peerpackage的角色。

使用方法

代码语言:javascript复制
标记转换内部软件包 //  k8s:conversion-gen=<import-path-of-internal-package>
标记转换外部软件包 //  k8s:conversion-gen-external-types=<import-path-of-external-package>
标记不转换对应注释或结构 //  k8s:conversion-gen=false

6,defaulter-gen:用于生成Defaulter函数

代码语言:javascript复制
为包含字段的所有类型创建defaulters, //  k8s:defaulter-gen=<field-name-to-flag>
所有都生成 //  k8s:defaulter-gen=true|false

7,go-to-protobuf:通过go struct生成pb idl

8,import-boss:在给定存储库中强制执行导入限制

9,openapi-gen:生成openAPI定义,使用方法:

代码语言:javascript复制
 k8s:openapi-gen=true 为指定包或方法开启
 k8s:openapi-gen=false 指定包关闭

10,register-gen:生成register

11,set-gen

其中informer和listers是构建controller的基础,kubebuilder也是基于informer的机制生成的代码。code-generator还专门整合了这些gen,形成了generate-groups.sh和generate-internal-groups.sh这两个脚本.

默认的生成脚本在code-generator下的generate-groups.sh,如果想生成自定义的crd,运行下面的命令:

代码语言:javascript复制
./generate-groups.sh all github.com/nevermore/project/pkg/client github.com/nevermore/project/pkg/apis "foo:v1 bar:v1beta1

在我们的源代码中出现了很多,类似go generator的编译tag:

代码语言:javascript复制
doc.go
//  k8s:deepcopy-gen=package,register
//  groupName=samplecontroller.k8s.io
代码语言:javascript复制
types.go
//  genclient
//  k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

出现了这样的tag,这些tag到底是什么意思呢,有什么作用呢?

1,分类,其实code-generator将tag分为了两种:

Global tags: 全局的tag,放在具体版本的doc.go文件中

Local tags: 本地的tag,放在types.go文件中的具体的struct上.

tag的使用语法为:

代码语言:javascript复制
//  tag-name
或
//  tag-name=value

并且 这些注释块必须分开,这也是源代码中 注释存在分割的原因.

A,Global

全局的tag是写在doc.go中的,典型的内容如下:

代码语言:javascript复制
//  k8s:deepcopy-gen=package

// Package v1 is the v1 version of the API.
//  groupName=example.com
package v1

注意: 空行不能省

k8s:deepcopy-gen=: 它告诉deepcopy默认为该包中的每一个类型创建deepcopy方法,如果不需要深度复制,可以选择关闭此功能

代码语言:javascript复制
//  k8s:deepcopy-gen=false

如果不启用包级别的深度复制,那么就需要在每个类型上加入深度复制

代码语言:javascript复制
//  k8s:deepcopy-gen=true

groupName: 定义group的名称,注意别弄错了.

注意 这里是 k8s:deepcopy-gen=,最后是 = ,和local中的区别开来.

B,local

本地的tag直接写在类型上,典型的内容如下:

代码语言:javascript复制
//  genclient
//  k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Foo is a specification for a Foo resource
type Foo struct {
metav1.TypeMeta   `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec   FooSpec   `json:"spec"`
Status FooStatus `json:"status"`
}

可以看到local支持两种tag

genclient: 此标签是告诉client-gen,为此类型创建clientset,但也有以下几种用法.

1,对于集群范围内的资源(没有namespace限制的),需要使用

代码语言:javascript复制
//  genclient:nonNamespaced

,生成的clientset中的namespace()方法就不再需要传入参数

2,使用子资源分离的,例如/status分离的,则需要使用 genclient:noStatus,来避免更新到status资源(当然代码的struct中也没有status)

3,对于其他的值,这里不做过多的解释,请参照

代码语言:javascript复制
//  genclient:noVerbs
//  genclient:onlyVerbs=create,delete
//  genclient:skipVerbs=get,list,create,update,patch,delete,deleteCollection,watch
//  genclient:method=Create,verb=create,result=k8s.io/apimachinery/pkg/apis/meta/v1.Status

k8s:deepcopy-gen:interfaces=: 为struct生成实现 tag值的DeepCopyXxx方法,例如:

代码语言:javascript复制
//  k8s:deepcopy-gen:interfaces=example.com/pkg/apis/example.SomeInterface

将生成 DeepCopySomeInterface() SomeInterface方法

代码生成实践

1,在$GOPATH/src/新建好相应的路径(go mod 同理)

代码语言:javascript复制
mkdir -p $GOPATH/src/github.com/nevermore/project/pkg/client
mkdir -p $GOPATH/src/github.com/nevermore/project/pkg/apis/foo/v1

进入到v1路径下,新建三个文件

代码语言:javascript复制
touch doc.go types.go regsiter.go
代码语言:javascript复制
pkg/apis/ip/v1/types.go

该文件包含了资源的数据结构,对应yaml,types不能有interface{} ,否则自动生成的时候会生成出错,因此其实也不建议通过代码自动生成,还是手动去编写会更好。

修改每个文件开头为package v1;同理配置apis/bar/v1beta1相应的文件。

最终生成相应的

代码语言:javascript复制
clientset、listers、informers

准备好上述3个文件后,我们就可以用generator进行代码生成,生成完毕后,我们分析下源码结构:

生成的deepcopy.文件,里面定义每个结构体的深拷贝函数:

代码语言:javascript复制
pkg/apis/ip/v1/zz_generated.deepcopy.go 
      func (in *Ip) DeepCopyInto(out *Ip) 
      func (in *Ip) DeepCopy() *Ip
      func (in *Ip) DeepCopyObject() runtime.Object

生成的client文件,目录层级如下:

代码语言:javascript复制
pkg/client/
      clientset/versioned
             clientset.go
          doc.go
          fake
             clientset_generated.go
             doc.go
             register.go
          scheme
              doc.go
              register.go
          typed/ip/v1
              doc.go
              fake
                doc.go
                fake_ip.go
                fake_ip_client.go
              generated_expansion.go
              ip.go
              ip_client.go
      informers/externalversions
                  factory.go
                  generic.go
                  internalinterfaces                     
                    factory_interfaces.go 
                  ip
                    interface.go
                    v1/                  
                     interface.go
                     ip.go
       listers/ip/v1 
          expansion_generated.go
          ip.go

在cllientset.go中定义了clientset结构体

代码语言:javascript复制
func New(c rest.Interface) *Clientset 

type Clientset struct {
  *discovery.DiscoveryClient
  rocduV1 *rocduv1.RocduV1Client
}

clientset_generated.go定义了测试用的clientset

代码语言:javascript复制
type Clientset struct {
  testing.Fake
  discovery *fakediscovery.FakeDiscovery
  tracker   testing.ObjectTracker
}

register.go对schema进行了注册:

代码语言:javascript复制
var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)

var localSchemeBuilder = runtime.SchemeBuilder{
  rocduv1.AddToScheme,
}

scheme/register.go是真正完成schema注册的地方

代码语言:javascript复制
var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
  rocduv1.AddToScheme,
}

Scheme定义了序列化和反序列化API对象的方法,用于将group、版本和类型信息转换为Go模式和从Go模式转换为Go模式的类型注册表,以及不同版本的Go模式之间的映射。当和API Server通信的时候能够处理新的types类型的话就需要先让client能够知道有这个新的types类型存在。

AddToScheme 会利用到反射,因此新定义的types类型的结构体的命名必须要和自定义的Kind的命名(如VirtualService)保持一致,否则会找不到对应的kind,

接着看下typed/ip/v1/fake/fake_ip.go

代码语言:javascript复制
type FakeIps struct {
  Fake *FakeRocduV1
  ns   string
}

typed/ip/v1/fake/fake_ip_client.go

代码语言:javascript复制
type FakeRocduV1 struct {
  *testing.Fake
}

generated_expansion.go

代码语言:javascript复制
type IpExpansion interface{}

ip.go

代码语言:javascript复制
type ips struct {
  client rest.Interface
  ns     string
}
代码语言:javascript复制
type IpInterface interface {
  Create(ctx context.Context, ip *v1.Ip, opts metav1.CreateOptions) (*v1.Ip, error)
  Update(ctx context.Context, ip *v1.Ip, opts metav1.UpdateOptions) (*v1.Ip, error)
  Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
  DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
  Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Ip, error)
  List(ctx context.Context, opts metav1.ListOptions) (*v1.IpList, error)
  Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
  Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Ip, err error)
  IpExpansion
}

ip_client.go

代码语言:javascript复制
type RocduV1Client struct {
  restClient rest.Interface
}
func (c *RocduV1Client) Ips(namespace string) IpInterface

informers/externalversions/factory.go

代码语言:javascript复制
type sharedInformerFactory struct {
  client           versioned.Interface
  namespace        string
  tweakListOptions internalinterfaces.TweakListOptionsFunc
  lock             sync.Mutex
  defaultResync    time.Duration
  customResync     map[reflect.Type]time.Duration


  informers map[reflect.Type]cache.SharedIndexInformer
  // startedInformers is used for tracking which informers have been started.
  // This allows Start() to be called multiple times safely.
  startedInformers map[reflect.Type]bool
}

informers/externalversions/generic.go

代码语言:javascript复制
type genericInformer struct {
  informer cache.SharedIndexInformer
  resource schema.GroupResource
}

informers/externalversions/ip/interface.go

代码语言:javascript复制
type Interface interface {
  // V1 provides access to shared informers for resources in V1.
  V1() v1.Interface
}

type group struct {
  factory          internalinterfaces.SharedInformerFactory
  namespace        string
  tweakListOptions internalinterfaces.TweakListOptionsFunc
}

v1/ interface.go

代码语言:javascript复制
type version struct {
  factory          internalinterfaces.SharedInformerFactory
  namespace        string
  tweakListOptions internalinterfaces.TweakListOptionsFunc
}

以上就是informers的源码内容,下面看看listener

listers/ip/v1 /expansion_generated.go

代码语言:javascript复制
type IpListerExpansion interface{}

ip.go

代码语言:javascript复制
type ipLister struct {
  indexer cache.Indexer
}
代码语言:javascript复制
type ipNamespaceLister struct {

  indexer   cache.Indexer
  namespace string
}

生成完上述代码,我们就可以编写对应controller来管理上述资源,并通过kubectl操作对应的yaml文件。

0 人点赞