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文件。