go, k8s

创建CronJob多版本API

一个服务肯定会有多个版本,版本直接的APi会有差异,下面将对CronJob的spec做一些更改,并确保它可以支持多版本。

创建新版本v2

Kubernetes API中一个相当常见的改变是获取一些非结构化的或者存储在一些特殊的字符串格式的数据,并将其修改为结构化的数据。比如spec中的schedule字段:

schedule: "*/1 * * * *"

在Kubernetes中,所有版本都必须可以安全地相互往返。这意味着,如果我们从版本1转换为版本2,然后再转换回版本1,我们一定不能丢失信息。因此,我们对API所做的任何更改都必须与我们在v1中支持的任何内容兼容,并且还需要确保我们在v2中添加的任何内容在v1中都受支持。在某些情况下,这意味着我们需要向v1添加新字段,但在CronJob中,我们不必这样做,因为我们没有添加新功能。

把schedule字段改为如下格式:

schedule:
  minute: */1

使用下面的命令创建v2版本:

kubebuilder create api --group batch --version v2 --kind CronJob

按 y “创建资源”和 n “创建控制器”。

file

cronjob_types.go

目录是project/api/v2/cronjob_types.go
首先修改导入的包:

import (
    batchv1 "k8s.io/api/batch/v1"
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

修改CronSpec struct,只修改了Schedule字段为新类型CronSchedule,其余和v1保持不变。

type CronJobSpec struct {
    // The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron.
    Schedule CronSchedule `json:"schedule"`
    ...
}

然后定义CronSchedule的结构:

// describes a Cron schedule.
type CronSchedule struct {
    // specifies the minute during which the job executes.
    // +optional
    Minute *CronField `json:"minute,omitempty"`
    // specifies the hour during which the job executes.
    // +optional
    Hour *CronField `json:"hour,omitempty"`
    // specifies the day of the month during which the job executes.
    // +optional
    DayOfMonth *CronField `json:"dayOfMonth,omitempty"`
    // specifies the month during which the job executes.
    // +optional
    Month *CronField `json:"month,omitempty"`
    // specifies the day of the week during which the job executes.
    // +optional
    DayOfWeek *CronField `json:"dayOfWeek,omitempty"`
}

最后,我们定义一个封装器类型来表示Cron的一个字段。

// represents a Cron field specifier.
type CronField string

设置存储版本

因为同时存在了v1v2两个版本,因此我们需要标记存储版本。这是Kubernetes API服务器用于存储数据的版本。我们选择v1版本。

注意如果在存储版本改变之前它们已经被写入那么在仓库中可能存在多个版本。改变存储版本仅仅影响在改变之后对象的创建/更新。

目录是project/api/v1/cronjob_types.go,使用+kubebuilder:storageversion标记来标识存储版本。

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:storageversion

// CronJob is the Schema for the cronjobs API
type CronJob struct {
    ...
}

file

版本转换

由于我们现在有两个不同的版本,并且用户可以请求任何一个版本,因此我们必须定义一种在版本之间转换的方法。对于CRD,这是通过Webhook完成的。所以需要修改controller的逻辑。在修改代码之前
需要了解下转换模型。

转换模型

定义转换的一种简单方法可能是定义转换函数如何可以在我们的每个版本之间进行转换。然后,每当我们需要转换时,调用相应的函数运行转换。当我们只有两个版本时,这可以正常工作,但是如果我们有4个版本或者更多的时候,就需要创建多个转换函数。

控制器运行时会根据hub和spoke模型进行转换:我们将一个版本标记为hub,而所有其他版本只需定义为与hub之间的转换即可:

file

然后如果我们必须在两个非中心版本之间进行转换,我们首先转换为中心版本,然后转换为所需的版本:

file

这样就减少了转换函数的数量。

当客户端请求特定的版本资源时,Kubernetes API服务器需要返回该版本的结果。但是,该版本可能与API服务器存储的版本不匹配。在这种情况下,API服务器需要知道如何在所需版本和存储版本之间进行转换。由于CRD没有内置转换,因此Kubernetes API服务器会调用Webhook来执行转换。对于Kubebuilder,Webhook通过controller-runtime来执行hub-and-spoke的转换。

实现转换

v1版本当做hub,文件目录project/api/v1/cronjob_conversion.go。只需要添加一个名为 Hub() 用作标记的空方法。

/*
Copyright 2024.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1

// Hub marks this type as a conversion hub.
func (*CronJob) Hub() {}

v2版本当做spoke,文件目录project/api/v1/cronjob_conversion.gov2版本需要实现Convertible接口。顾名思义,它需要实现ConvertTo从(其它版本)向hub版本转换,ConvertFrom实现从hub版本转换到(其他版本)。

  • ConvertTo应修改其参数以包含转换后的对象。大部分转换都是直接赋值,除了那些发生变化的field。
  • ConvertFrom应修改其接收者以包含转换后的对象。。大部分转换都是直接赋值,除了那些发生变化的field。
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v2

/*
For imports, we'll need the controller-runtime
[`conversion`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc)
package, plus the API version for our hub type (v1), and finally some of the
standard packages.
*/
import (
    "fmt"
    "strings"
    "sigs.k8s.io/controller-runtime/pkg/conversion"
    "wgh.io/project/api/v1"
)

// ConvertTo converts this CronJob to the Hub version (v1).
func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error {
    dst := dstRaw.(*v1.CronJob)

    sched := src.Spec.Schedule
    scheduleParts := []string{"*", "*", "*", "*", "*"}
    if sched.Minute != nil {
        scheduleParts[0] = string(*sched.Minute)
    }
    if sched.Hour != nil {
        scheduleParts[1] = string(*sched.Hour)
    }
    if sched.DayOfMonth != nil {
        scheduleParts[2] = string(*sched.DayOfMonth)
    }
    if sched.Month != nil {
        scheduleParts[3] = string(*sched.Month)
    }
    if sched.DayOfWeek != nil {
        scheduleParts[4] = string(*sched.DayOfWeek)
    }
    dst.Spec.Schedule = strings.Join(scheduleParts, " ")

    // ObjectMeta
    dst.ObjectMeta = src.ObjectMeta

    // Spec
    dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds
    dst.Spec.ConcurrencyPolicy = v1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy)
    dst.Spec.Suspend = src.Spec.Suspend
    dst.Spec.JobTemplate = src.Spec.JobTemplate
    dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit
    dst.Spec.FailedJobsHistoryLimit = src.Spec.FailedJobsHistoryLimit

    // Status
    dst.Status.Active = src.Status.Active
    dst.Status.LastScheduleTime = src.Status.LastScheduleTime

    return nil
}

// ConvertFrom converts from the Hub version (v1) to this version.
func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error {
    src := srcRaw.(*v1.CronJob)

    schedParts := strings.Split(src.Spec.Schedule, " ")
    if len(schedParts) != 5 {
        return fmt.Errorf("invalid schedule: not a standard 5-field schedule")
    }
    partIfNeeded := func(raw string) *CronField {
        if raw == "*" {
            return nil
        }
        part := CronField(raw)
        return &part
    }
    dst.Spec.Schedule.Minute = partIfNeeded(schedParts[0])
    dst.Spec.Schedule.Hour = partIfNeeded(schedParts[1])
    dst.Spec.Schedule.DayOfMonth = partIfNeeded(schedParts[2])
    dst.Spec.Schedule.Month = partIfNeeded(schedParts[3])
    dst.Spec.Schedule.DayOfWeek = partIfNeeded(schedParts[4])

    // ObjectMeta
    dst.ObjectMeta = src.ObjectMeta

    // Spec
    dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds
    dst.Spec.ConcurrencyPolicy = ConcurrencyPolicy(src.Spec.ConcurrencyPolicy)
    dst.Spec.Suspend = src.Spec.Suspend
    dst.Spec.JobTemplate = src.Spec.JobTemplate
    dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit
    dst.Spec.FailedJobsHistoryLimit = src.Spec.FailedJobsHistoryLimit

    // Status
    dst.Status.Active = src.Status.Active
    dst.Status.LastScheduleTime = src.Status.LastScheduleTime

    return nil
}

设置webhook

通常是通过下面的命令来设置webhook,但是我们上一篇文章中已经设置过了webhook,所以这里就不用再执行了。

kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion

project/api/v1/cronjob_webhook.go文件中:

// log is for logging in this package.
var cronjoblog = logf.Log.WithName("cronjob-resource")

// SetupWebhookWithManager will setup the manager to manage the webhooks
func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error {
    return ctrl.NewWebhookManagedBy(mgr).
        For(r).
        Complete()
}

此设置兼作转换Webhook的设置:只要我们的类型实现了HubConvertible接口,就会注册转换Webhook

main.go

project/cmd/main.go中我们对SetupWebhookWithManager的现有调用也会向管理器注册我们的转换Webhook

if os.Getenv("ENABLE_WEBHOOKS") != "false" {
        if err = (&batchv1.CronJob{}).SetupWebhookWithManager(mgr); err != nil {
            setupLog.Error(err, "unable to create webhook", "webhook", "CronJob")
            os.Exit(1)
        }
        if err = (&batchv2.CronJob{}).SetupWebhookWithManager(mgr); err != nil {
            setupLog.Error(err, "unable to create webhook", "webhook", "CronJob")
            os.Exit(1)
        }
    }

启用转换

  • config/crd/kustomization.yaml文件启用- path: patches/webhook_in_cronjobs.yaml - path: patches/cainjection_in_cronjobs.yaml
  • config/default/kustomization.yaml文件的resources部分下启用../certmanager../webhook
  • config/default/kustomization.yaml文件的patches部分下启用 - path: manager_webhook_patch.yaml
  • config/default/kustomization.yaml文件的CERTMANAGER部分下启用所有变量。- path: webhookcainjection_patch.yamlreplacements

测试

重新创建crd,controller。

make manifests
make docker-build docker-push IMG=registry-1.docker.io/wgh9626/cronjob-mutiversion:v1
make install
make deploy IMG=registry-1.docker.io/wgh9626/cronjob-mutiversion:v1

file

先创建一个v2版本:

vim config/samples/v2.yaml

apiVersion: batch.tutorial.kubebuilder.io/v2
kind: CronJob
metadata:
  labels:
    app.kubernetes.io/name: cronjob
    app.kubernetes.io/instance: cronjob-sample
    app.kubernetes.io/part-of: project
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/created-by: project
  name: cronjob-sample
spec:
  schedule:
    minute: "*/1"
  startingDeadlineSeconds: 60
  concurrencyPolicy: Allow # explicitly specify, but Allow is also default.
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

kubectl get cronjobs.v1.batch.wgh.io
kubectl get cronjobs.v2.batch.wgh.io

file

可以看到即使控制器是根据v1版本编写的,v2版本的CronJob依旧可以正常工作。

开启多组模式

kubebuilder edit --multigroup=true
mkdir api/batch
mv api/* api/batch
mkdir internal/controller/batch
mv internal/controller/* internal/controller/batch/

使用上面的命令开启,KubeBuilder将会在PROJECT中新增一行,标记该项目是一个Multi-group项目。然后迁移之前的API和控制器目录到新目录中。

multigroup: true

具体的变化是:

  • CRD对象将包含多个APIGroup资源,比如:"foo.example.com", "bar.example.com"等。
  • 类型定义结构体会添加新的字段:
    • groupVersionKind – 记录API组、版本和Kind
    • group – 记录API组
  • 各API类型将注册到对应的APIGroupVersoin对象中。
  • 客户端生成的Scheme会区分加载不同API组的对象。
  • 客户端将根据API组与版本解析URL路径。
0 0 投票数
文章评分
订阅评论
提醒
guest

0 评论
内联反馈
查看所有评论

相关文章

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

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