一个服务肯定会有多个版本,版本直接的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 “创建控制器”。
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
设置存储版本
因为同时存在了v1
和v2
两个版本,因此我们需要标记存储版本。这是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 {
...
}
版本转换
由于我们现在有两个不同的版本,并且用户可以请求任何一个版本,因此我们必须定义一种在版本之间转换的方法。对于CRD,这是通过Webhook完成的。所以需要修改controller的逻辑。在修改代码之前
需要了解下转换模型。
转换模型
定义转换的一种简单方法可能是定义转换函数如何可以在我们的每个版本之间进行转换。然后,每当我们需要转换时,调用相应的函数运行转换。当我们只有两个版本时,这可以正常工作,但是如果我们有4个版本或者更多的时候,就需要创建多个转换函数。
控制器运行时会根据hub和spoke
模型进行转换:我们将一个版本标记为hub
,而所有其他版本只需定义为与hub
之间的转换即可:
然后如果我们必须在两个非中心版本之间进行转换,我们首先转换为中心版本,然后转换为所需的版本:
这样就减少了转换函数的数量。
当客户端请求特定的版本资源时,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.go
。v2
版本需要实现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
的设置:只要我们的类型实现了Hub
和Convertible
接口,就会注册转换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.yaml
和replacements
。
测试
重新创建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
先创建一个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
可以看到即使控制器是根据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路径。