创建CRD
初始化
直接在GOPATH
下初始化,这样init时就不用加--repo
。
mkdir -p $GOPATH/src/kubebuilder-demo
cd $GOPATH/src/kubebuilder-demo
kubebuilder init --domain demo.kubebuilder.io
创建API
创建Resource
和Controller
。
kubebuilder create api --group myapp --version v1 --kind Redis
install
make install
查看CRD
k get crd | grep redis
开发CRD
需要实现的功能有:
- 创建自定义资源CR:Redis
- 创建POD,支持滚动更新
将kubebuilder-demo
文件夹导入到本地,配置SFTP自动上传到$GOPATH/src/kubebuilder-demo
。
修改redis_types.go
修改RedisSpec
,修改Foo
为Port
,添加端口范围的验证tag。
修改myapp_v1_redis.yaml
添加port字段。
修改完成后上传到服务器。
服务器上重新install。再次查看crd中已经包含了端口验证。
k get crd redis.myapp.demo.kubebuilder.io -o yaml
创建CR
k apply -f config/samples/myapp_v1_redis.yaml
修改Redis的端口为6399,可以看到无法执行,报错为端口应该在范围内。
创建POD
创建controller/redis_helper.go
文件,该文件的作用是:
- 为自定义资源
vRedis
配置对象创建对应的Pod对象。 - 它将
vRedis
的元数据如名称、命名空间等复制到新的Pod对象中。 - 设置Pod容器使用的redis镜像,镜像拉取策略,暴露端口。
- 使用controller-runtime的client向kubernetes API服务器创建这个Pod对象。
package controller
import (
"context"
coreV1 "k8s.io/api/core/v1"
v1 "kubebuilder-demo/api/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func CreateRedis(client client.Client, redisConfig *v1.Redis) error {
newPod := &coreV1.Pod{}
newPod.Name = redisConfig.Name
newPod.Namespace = redisConfig.Namespace
newPod.Spec.Containers = []coreV1.Container{
{
Name: redisConfig.Name,
Image: "redis:5-alpine",
ImagePullPolicy: coreV1.PullIfNotPresent,
Ports: []coreV1.ContainerPort{
{
ContainerPort: int32(redisConfig.Spec.Port),
},
},
},
}
return client.Create(context.Background(), newPod)
}
修改controller/redis_controller.go
文件中的Reconcile回调逻辑。
- 首先获取Redis自定义资源对象。
- 调用
CreateRedis
函数创建对应的Pod对象。 - 如果Pod创建成功,则
Reconcile
函数直接返回,等待下一个调用循环。 - 如果Pod创建失败,则
Reconcile
函数会返回错误信息。
func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
redis := &myappv1.Redis{}
err := r.Get(ctx, req.NamespacedName, redis)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("redis Obj:", redis)
err := CreateRedis(r.Client, redis)
fmt.Println("create pod failue,", err)
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
先删除上一步创建的CR。
k delete -f config/samples/myapp_v1_redis.yaml
再开一个窗口启动控制器。
make run
再次创建CR。
k apply -f config/samples/myapp_v1_redis.yaml
控制器输出如下:
查看CR和pod,pod已创建。
这里有个问题,删除CR,pod依然存在。
k delete -f config/samples/myapp_v1_redis.yaml
k get po
创建多副本的pod
修改api/v1/redis_types.go
文件,添加数量num字段。
type RedisSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// Foo is an example field of Redis. Edit redis_types.go to remove/update
// +kubebuilder:validation:Maximum:=6380
// +kubebuilder:validation:Minimum:=6370
Port int `json:"port,omitempty"`
Num int `json:"num,omitempty"`
}
修改controller/redis_helper.go
文件:
package controller
import (
"context"
"fmt"
coreV1 "k8s.io/api/core/v1"
v1 "kubebuilder-demo/api/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// 判断pod在finalizer中是否存在
func isExist(podName string, redis *v1.Redis) bool {
for _, finalizer := range redis.Finalizers {
if finalizer != "" && podName == finalizer {
return true
}
}
return false
}
// 判断pod是否存在,不存在则创建pod
func CreateRedis(podName string, client client.Client, redisConfig *v1.Redis) (string, error) {
if isExist(podName, redisConfig) {
return "", nil
}
newPod := &coreV1.Pod{}
// 注意这里需要改为podName
newPod.Name = podName
newPod.Namespace = redisConfig.Namespace
newPod.Spec.Containers = []coreV1.Container{
{
Name: redisConfig.Name,
Image: "redis:5-alpine",
ImagePullPolicy: coreV1.PullIfNotPresent,
Ports: []coreV1.ContainerPort{
{
ContainerPort: int32(redisConfig.Spec.Port),
},
},
},
}
return podName, client.Create(context.Background(), newPod)
}
// 生成多个Redis副本的pod名称,格式为:<Redis Name>-<index>
func SetRedisPodName(redis *v1.Redis) []string {
podNames := make([]string, redis.Spec.Num)
for i := 0; i < redis.Spec.Num; i++ {
podNames[i] = fmt.Sprintf("%s-%d", redis.Name, i)
}
return podNames
}
修改controller/redis_controller.go
文件:
func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
redis := &myappv1.Redis{}
// 获取Redis对象
err := r.Get(ctx, req.NamespacedName, redis)
if err != nil {
fmt.Println("error:", err)
}
fmt.Println("redis Obj:", redis)
// 生成podName
podNames := SetRedisPodName(redis)
fmt.Println("podNames:", podNames)
updateFlag := false
// 判断Redis是否被删除,如果已被标记为删除,则调用r.deleteRedis函数来处理删除逻辑
if !redis.DeletionTimestamp.IsZero() {
return ctrl.Result{}, r.deleteRedis(ctx, redis)
}
// 创建Redis pod
for _, podName := range podNames {
pod, err := CreateRedis(podName, r.Client, redis)
if err != nil {
fmt.Println("create pod failed:", err)
return ctrl.Result{}, err
}
if pod == "" {
continue
}
// 如果pod创建成功,则将其名称添加到Redis对象的Finalizers列表中
redis.Finalizers = append(redis.Finalizers, pod)
// 标记更新,表示需要更新redis对象
updateFlag = true
}
// 更新Redis对象
if updateFlag {
err = r.Client.Update(ctx, redis)
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// 在删除Redis对象时,删除与之关联的所有pod
func (r *RedisReconciler) deleteRedis(ctx context.Context, redis *myappv1.Redis) error {
for _, finalizer := range redis.Finalizers {
err := r.Client.Delete(ctx, &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: finalizer,
Namespace: redis.Namespace,
},
})
if err != nil {
return err
}
}
// 清空Redis对象的Finalizers列表
redis.Finalizers = []string{}
return r.Client.Update(ctx, redis)
}
修改config/samples/myapp_v1_redis.yaml
,设置副本数为3。
spec:
# TODO(user): Add fields here
port: 6379
num: 3
重新创建CRD,启动controller。
make install
make run
另开一个窗口,创建Redis CR。
k apply -f config/samples/myapp_v1_redis.yaml
k get Redis
k get po
controller日志输出podName为:podNames: [redis-sample-0 redis-sample-1 redis-sample-2]
三个Redis pod已创建。
查看Redis CR的yaml中已包含了podName的finalizers。
删除Redis CR,对应的pod也自动被删除了。
k delete -f config/samples/myapp_v1_redis.yaml
再次创建后,修改副本数为2,发现pod没有减少。
k apply -f config/samples/myapp_v1_redis.yaml
vim config/samples/myapp_v1_redis.yaml
k apply -f config/samples/myapp_v1_redis.yaml
k get po
Redis对象中仍然为3个副本,只是podName变为了2个。
副本收缩
修改controller/redis_controller.go
文件,修改删除逻辑:
func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
redis := &myappv1.Redis{}
// 获取Redis对象
err := r.Get(ctx, req.NamespacedName, redis)
if err != nil {
return ctrl.Result{}, err
}
fmt.Println("redis Obj:", redis)
// 判断Redis是否被删除,如果已被标记为删除。
// 删除Redis对象时删除创建的pod,或者副本收缩时删除pod
// 则调用r.deleteRedis函数来处理删除逻辑
if !redis.DeletionTimestamp.IsZero() || len(redis.Finalizers) > redis.Spec.Num {
return ctrl.Result{}, r.deleteRedis(ctx, redis)
}
// 生成podName
podNames := SetRedisPodName(redis)
fmt.Println("podNames:", podNames)
updateFlag := false
// 创建Redis pod
for _, podName := range podNames {
pod, err := CreateRedis(podName, r.Client, redis)
if err != nil {
fmt.Println("create pod failed:", err)
return ctrl.Result{}, err
}
if pod == "" {
continue
}
// 如果pod创建成功,则将其名称添加到Redis对象的Finalizers列表中
redis.Finalizers = append(redis.Finalizers, pod)
// 标记更新,表示需要更新redis对象
updateFlag = true
}
// 更新Redis对象
if updateFlag {
err = r.Client.Update(ctx, redis)
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
func (r *RedisReconciler) deleteRedis(ctx context.Context, redis *myappv1.Redis) error {
// finalizers列表和副本数有三种情况:
// 1.finalizers=num,删除时全部删除
// 2.finalizers>num,删除时只删除多余的pod
// 3.finalizers<num,pod创建失败无法将其添加到finalizers列表中,需要先检测有没有缺失的pod,如果有则重新创建再次删除。
// 但是在上面的Reconcile函数中是创建成功后才添加到finalizers中,所以这种情形不存在。
// 创建需要删除的pod名称切片
var deletePods []string
// 删除多余的pod,即删除finalizers切片position即Redis副本数位置后面的元素。
position := redis.Spec.Num
if (len(redis.Finalizers) - redis.Spec.Num) > 0 {
deletePods = redis.Finalizers[position:]
redis.Finalizers = redis.Finalizers[:position]
} else {
// 创建切片为了确保在后续的删除操作中,只修改deletePods列表,而不会影响到原始的redis.Finalizers列表。
deletePods = redis.Finalizers[:]
// 清空Redis对象的Finalizers列表
redis.Finalizers = []string{}
}
fmt.Println("deletepPods:", deletePods)
fmt.Println("redis.Finalizers:", redis.Finalizers)
for _, finalizer := range deletePods {
err := r.Client.Delete(ctx, &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: finalizer,
Namespace: redis.Namespace,
},
})
if err != nil {
return err
}
}
return r.Client.Update(ctx, redis)
}
先手动编辑Redis CR,删除finalizers,再删除CR。
k edit Redis redis-sample
k delete -f config/samples/myapp_v1_redis.yaml
重新启动controller。
make run
另开一个窗口,还原副本数为3,创建Redis CR。
vim config/samples/myapp_v1_redis.yaml
k apply -f config/samples/myapp_v1_redis.yaml
k get Redis
k get po
修改副本数为2,发现pod数量已变为2了。
vim config/samples/myapp_v1_redis.yaml
k apply -f config/samples/myapp_v1_redis.yaml
k get po
删除Redis CR,pod自动删除。
删除和重建
上面pod删除时不会自动重建,因为是通过Redis CR创建的,而不是Deployment,要想实现类似的效果可以使用Owner references
。
相关介绍参考官方文档:
https://kubernetes.io/zh-cn/docs/concepts/overview/working-with-objects/owners-dependents/
https://kubernetes.io/blog/2021/05/14/using-finalizers-to-control-deletion/
ownerReferences
包含以下几个字段:
其中blockOwnerDeletion
设置为true可以确保在删除所有者对象时,子资源对象也会被正确地删除。将controller
设置为true可以让Kubernetes管理器来负责删除子资源对象,实现级联删除的功能。
如果某个资源对象的所有者对象已经被删除,但是该资源对象本身仍然存在,Kubernetes会将其视为垃圾资源。在这种情况下Kubernetes会根据ownerReferences
信息,在适当的时候自动删除这些孤立的资源对象,实现垃圾删除的功能
通过监听资源对象的创建、更新和删除事件来跟踪资源的生命周期变化。在事件处理逻辑中,可以根据ownerReferences
信息来确定资源对象的依赖关系,并做出相应的处理。例如,当某个资源对象被删除时,可以根据ownerReferences
信息来决定是否需要同时删除其子资源对象。下面就用这个功能来实现pod的自动重建。
那么监听对象的生命周期可以用以下几种方法:
- client-go库:编写一个控制器或者Operator,使用client-go库来监听感兴趣的资源对象的创建、更新和删除事件。
- Informer机制:
Informer
会缓存资源对象的状态信息,并在对象发生变化时通知控制器。我们可以自定义Informer
来监听特定类型的资源对象,并在事件回调函数中处理生命周期变化。 - Operator SDK:是Kubernetes社区提供的一个开发框架,它在client-go的基础上封装了更高级的抽象。它内置了事件监听和资源管理的功能,可以帮助我们更方便地监控资源对象的生命周期变化。
- 利用APIServer的Watch机制:Watch机制允许客户端订阅资源对象的变化事件。我们可以直接使用 Kubernetes API来实现事件监听,而无需依赖于client-go或Operator SDK。
Owns函数
这里采用第四种方法来实现。controller-runtime包中有一个Owns
函数,它允许我们在定义控制器时,指定控制器需要管理的其他资源对象。
ControllerManagedBy
用于生成一个控制器,它返回一个ControllerBuilder
对象,该对象可以使用Owns
函数,它用于告诉控制器除了主资源对象之外,还需要监听和管理哪些子资源对象。当主资源对象发生变化时,控制器会自动触发对子资源对象的调谐操作。
默认情况下,这相当于调用Watches(object, handler.EnqueueRequestForOwner([...], ownerType, OnlyControllerOwner()))
。也就是说也可以使用Watches
函数,但是要单独写一个eventHandler
实现pod删除时的回调。
func (blder *Builder) Owns(object client.Object, opts ...OwnsOption) *Builder {
input := OwnsInput{object: object}
for _, opt := range opts {
opt.ApplyToOwns(&input)
}
blder.ownsInput = append(blder.ownsInput, input)
return blder
}
func (blder *Builder) Watches(object client.Object, eventHandler handler.EventHandler, opts ...WatchesOption) *Builder {
src := source.Kind(blder.mgr.GetCache(), object)
return blder.WatchesRawSource(src, eventHandler, opts...)
}
修改controller/redis_controller.go
文件,修改回调函数:
func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
redis := &myappv1.Redis{}
// 获取Redis对象
err := r.Get(ctx, req.NamespacedName, redis)
if err != nil {
return ctrl.Result{}, err
}
fmt.Println("redis Obj:", redis)
// 判断Redis是否被删除,如果已被标记为删除。
// 删除Redis对象时删除创建的pod,或者副本收缩时删除pod
// 则调用r.deleteRedis函数来处理删除逻辑
if !redis.DeletionTimestamp.IsZero() || len(redis.Finalizers) > redis.Spec.Num {
return ctrl.Result{}, r.deleteRedis(ctx, redis)
}
// 生成podName
podNames := SetRedisPodName(redis)
fmt.Println("podNames:", podNames)
updateFlag := false
// 创建Redis pod
for _, podName := range podNames {
// 检查Finalizers列表中是否已经包含该pod名称
if containsString(redis.Finalizers, podName) {
// 如果包含,说明该pod已经被创建过,但可能被删除了,需要重新创建
_, err := CreateRedis(podName, r.Client, redis, r.Scheme)
if err != nil {
fmt.Println("create pod failed:", err)
return ctrl.Result{}, err
}
} else {
// 如果不包含,则创建pod并添加Finalizer
pod, err := CreateRedis(podName, r.Client, redis, r.Scheme)
if err != nil {
fmt.Println("create pod failed:", err)
return ctrl.Result{}, err
}
if pod == "" {
continue
}
redis.Finalizers = append(redis.Finalizers, podName)
updateFlag = true
}
}
// 更新Redis对象
if updateFlag {
err = r.Client.Update(ctx, redis)
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// 检查podName是否在Finalizer列表中
func containsString(slice []string, s string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}
修改SetupWithManager
函数,添加Owns
告诉控制器监听和管理myappv1.Redis
主对象的pod对象。
func (r *RedisReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&myappv1.Redis{}).
Owns(&v1.Pod{}).
Complete(r)
}
修改controller/redis_helper.go
文件,
// 使用client.Get方法判断指定的pod是否存在,可以更可靠地确定pod的实际状态
func isExist(podName string, redis *v1.Redis, client client.Client) bool {
err := client.Get(context.Background(), types.NamespacedName{
Namespace: redis.Namespace,
Name: podName,
}, &coreV1.Pod{})
if err != nil {
return false
}
return true
}
// 判断pod是否存在,不存在则创建pod
func CreateRedis(podName string, client client.Client, redisConfig *v1.Redis, scheme *runtime.Scheme) (string, error) {
if isExist(podName, redisConfig, client) {
return podName, nil
}
newPod := &coreV1.Pod{}
newPod.Name = podName
newPod.Namespace = redisConfig.Namespace
newPod.Spec.Containers = []coreV1.Container{
{
Name: redisConfig.Name,
Image: "redis:5-alpine",
ImagePullPolicy: coreV1.PullIfNotPresent,
Ports: []coreV1.ContainerPort{
{
ContainerPort: int32(redisConfig.Spec.Port),
},
},
},
}
// 设置pod的OwnerReference属性。
// func SetControllerReference(owner v1.Object, controlled v1.Object, scheme *runtime.Scheme) error 父对象,子对象,scheme
err := controllerutil.SetControllerReference(redisConfig, newPod, scheme)
if err != nil {
return podName, err
}
return podName, client.Create(context.Background(), newPod)
}
重新启动controller,创建CR,查看ownerReferences
:
查看Redis的uid:
说明ownerReferences
属性设置成功。
删除redis-sample-2
pod,确认pod自动恢复。
添加events
当前CR是没有events的,要想添加对于增加和删除POD的事件,需要修改controller/redis_controller.go
文件,增加事件记录。
type RedisReconciler struct {
client.Client
Scheme *runtime.Scheme
// 添加事件记录
EventRecorder record.EventRecorder
}
func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
...
if updateFlag {
// 记录创建pod事件
r.EventRecorder.Event(redis, v1.EventTypeNormal, "CreatePod", "create pod success")
err = r.Client.Update(ctx, redis)
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
func (r *RedisReconciler) deleteRedis(ctx context.Context, redis *myappv1.Redis) error {
...
// 记录删除pod事件
r.EventRecorder.Event(redis, v1.EventTypeNormal, "DeletePod", "delete pod success")
return r.Client.Update(ctx, redis)
}
修改cmd/main.go
,创建RedisReconciler
时,为EventRecorder
属性赋值。
func main() {
...
if err = (&controllers.RedisReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
EventRecorder: mgr.GetEventRecorderFor("redis-controller"), //名称任意
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Redis")
os.Exit(1)
}
...
}
重新启动controller,创建CR,查看创建事件。
k describe Redis redis-sample
修改副本数为2,查看删除事件。
sed -i 's/num: 3/num: 2/g' config/samples/myapp_v1_redis.yaml
k apply -f config/samples/myapp_v1_redis.yaml
k describe Redis redis-sample | tail -n 5
因为上面代码中没有添加删除Finalizers
的逻辑,所以只能修改副本数,不能直接删除pod。
添加额外信息输出
添加Redis副本数的输出,要想实现这个功能,需要先添加kubebuilder
的Mark。参考:https://book.kubebuilder.io/reference/markers/crd.html ,
// +kubebuilder:printcolumn
JSONPathstringdescriptionstringformatstringnamestringpriorityinttypestring
adds a column to "kubectl get" output for this CRD.
修改api/v1/redis_types.go
文件:
type RedisStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
Replicas int `json:"replicas,omitempty"`
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".status.replicas"
修改controller/redis_controller.go
文件:
func (r *RedisReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
...
if updateFlag {
r.EventRecorder.Event(redis, v1.EventTypeNormal, "CreatePod", "create pod success")
err = r.Client.Update(ctx, redis)
if err != nil {
return ctrl.Result{}, err
}
// 更新Redis状态
redis.Status.Replicas = len(redis.Finalizers)
err := r.Status().Update(ctx, redis)
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
func (r *RedisReconciler) deleteRedis(ctx context.Context, redis *myappv1.Redis) error {
...
r.EventRecorder.Event(redis, v1.EventTypeNormal, "DeletePod", "delete pod success")
redis.Status.Replicas = len(redis.Finalizers)
err := r.Status().Update(ctx, redis)
if err != nil {
return err
}
return r.Client.Update(ctx, redis)
}
重新创建CRD,启动controller,创建CR。
make install
make run
查看输出:
修改副本数为1,查看输出:
请问一下有源代码可以分享吗?
等我上传一下github