go, k8s

开发Redis CRD

创建CRD

初始化

直接在GOPATH下初始化,这样init时就不用加--repo

mkdir -p $GOPATH/src/kubebuilder-demo
cd $GOPATH/src/kubebuilder-demo
kubebuilder init --domain demo.kubebuilder.io

file

创建API

创建ResourceController

kubebuilder create api --group myapp --version v1 --kind Redis

file

install

make install

file

查看CRD

k get crd | grep redis

file

开发CRD

需要实现的功能有:

  • 创建自定义资源CR:Redis
  • 创建POD,支持滚动更新

kubebuilder-demo文件夹导入到本地,配置SFTP自动上传到$GOPATH/src/kubebuilder-demo

file

修改redis_types.go

修改RedisSpec,修改FooPort,添加端口范围的验证tag。

file

修改myapp_v1_redis.yaml

添加port字段。

file

修改完成后上传到服务器。

file

服务器上重新install。再次查看crd中已经包含了端口验证。

k get crd redis.myapp.demo.kubebuilder.io -o yaml

file

创建CR

k apply -f config/samples/myapp_v1_redis.yaml

file

修改Redis的端口为6399,可以看到无法执行,报错为端口应该在范围内。

file

创建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

file

再次创建CR。

k apply -f config/samples/myapp_v1_redis.yaml

控制器输出如下:

file

查看CR和pod,pod已创建。

file

这里有个问题,删除CR,pod依然存在。

k delete -f config/samples/myapp_v1_redis.yaml
k get po

file

创建多副本的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

file

另开一个窗口,创建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]

file

三个Redis pod已创建。

file

查看Redis CR的yaml中已包含了podName的finalizers。

file

删除Redis CR,对应的pod也自动被删除了。

k delete -f config/samples/myapp_v1_redis.yaml

file

file

再次创建后,修改副本数为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

file

Redis对象中仍然为3个副本,只是podName变为了2个。

file

file

副本收缩

修改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

file

file

删除Redis CR,pod自动删除。

file

file

删除和重建

上面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

file

查看Redis的uid:

file

说明ownerReferences属性设置成功。

删除redis-sample-2pod,确认pod自动恢复。

file

添加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

file

修改副本数为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

file

因为上面代码中没有添加删除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

file

查看输出:

file

修改副本数为1,查看输出:

file

0 0 投票数
文章评分
订阅评论
提醒
guest

2 评论
最旧
最新 最多投票
内联反馈
查看所有评论
xiao
xiao
5 月 前

请问一下有源代码可以分享吗?

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部
0
希望看到您的想法,请您发表评论x