一般情况下,开发一个新项目,它的API是会经常变更的,不管一开始考虑得多么详细,都避免不了迭代的过程中去修改API定义。过了一段时间后,API会趋于稳定,在达到稳定版本之后,可能才正式发布V1.0版本。当然,这个稳定版的API就不应该再变化了,到了下一个版本想增强一下这个API,可能需要发布V2.0版本,这时V1.0版本还是要能够继续正常工作。我们来看看Operator中是如何支持多版本API的。
实现V2版本API
先通过命令添加一个V2版本API:
# operator-sdk create api --group apps --version v2 --kind Atom
Create Resource [y/n]
y
Create Controller [y/n]
n
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
api/v2/atom_types.go
Update dependencies:
$ go mod tidy
Running make:
$ make generate
test -s /Users/8lablw/Documents/atom-operator/bin/controller-gen && /Users/8lablw/Documents/atom-operator/bin/controller-gen --version | grep -q v0.11.1 || \
GOBIN=/Users/8lablw/Documents/atom-operator/bin go install sigs.k8s.io/controller-tools/cmd/[email protected]
/Users/8lablw/Documents/atom-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests
需要注意的是这里不需要创建Controller
新增的文件有api/v2:
atom_types.go
groupversion_info.go
zz_generated.deepcopy.go
发生变化的文件有:
main.go
appsv2 "github.com/xiaowei6688/atom-operator/api/v2"
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(appsv1.AddToScheme(scheme))
utilruntime.Must(appsv2.AddToScheme(scheme))
//+kubebuilder:scaffold:scheme
}
PROJECT
- api:
crdVersion: v1
namespaced: true
domain: atom.com
group: apps
kind: Atom
path: github.com/xiaowei6688/atom-operator/api/v2
version: v2
V2版本只用于演示多版本API,所以这里不添加多余功能。我们在 api/v2 目录下的atom_types.go文件中实现和V1版本完全一样的代码逻辑(除了package v2这一行有差异), 然后修改为如下:
type AtomSpec 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 Atom. Edit atom_types.go to remove/update
Deployment Deployment `json:"deployment,omitempty"`
Service ServiceSpec `json:"service,omitempty"`
Upgradeable bool `json:"upgradeable,omitempty"` // 这是V2新增的
}
创建V2的webhook
kubebuilder create webhook --group apps --version v2 --kind Atom --conversion
在V1版本内新增atom_conversion.go文件
package v1
func (*Atom) Hub() {}
在V2版本内新增atom_conversion.go转换文件
package v2
import (
v1 "github.com/xiaowei6688/atom-operator/api/v1"
"sigs.k8s.io/controller-runtime/pkg/conversion"
)
// ConvertTo converts this Atom v2 to the Hub version (v1).
func (src *Atom) ConvertTo(dstRaw conversion.Hub) error {
dst := dstRaw.(*v1.Atom)
// Convert ObjectMeta
dst.ObjectMeta = src.ObjectMeta
// Convert Deployment
dst.Spec.Deployment.Replicas = src.Spec.Deployment.Replicas
dst.Spec.Deployment.Selector = convertSelectorToV1(src.Spec.Deployment.Selector)
dst.Spec.Deployment.Template.Metadata = src.Spec.Deployment.Template.Metadata
dst.Spec.Deployment.Template.Spec.Containers = convertContainersToV1(src.Spec.Deployment.Template.Spec.Containers)
// Convert Service
dst.Spec.Service = convertServiceSpecToV1(src.Spec.Service)
// Convert Status
dst.Status = convertAtomStatusToV1(src.Status)
return nil
}
// ConvertFrom converts from the Hub version (v1) to this Atom v2.
func (dst *Atom) ConvertFrom(srcRaw conversion.Hub) error {
src := srcRaw.(*v1.Atom)
// Convert ObjectMeta
dst.ObjectMeta = src.ObjectMeta
// Convert Deployment
dst.Spec.Deployment.Replicas = src.Spec.Deployment.Replicas
dst.Spec.Deployment.Selector = convertSelectorFromV1(src.Spec.Deployment.Selector)
dst.Spec.Deployment.Template.Metadata = src.Spec.Deployment.Template.Metadata
dst.Spec.Deployment.Template.Spec.Containers = convertContainersFromV1(src.Spec.Deployment.Template.Spec.Containers)
// Convert Service
dst.Spec.Service = convertServiceSpecFromV1(src.Spec.Service)
// Convert Status
dst.Status = convertAtomStatusFromV1(src.Status)
dst.Spec.Upgradeable = false // Default value as it's not in v1
return nil
}
// Helper function to convert AtomStatus from v2 to v1
func convertAtomStatusToV1(status AtomStatus) v1.AtomStatus {
return v1.AtomStatus{
Workflow: status.Workflow,
Network: status.Network,
}
}
// Helper function to convert AtomStatus from v1 to v2
func convertAtomStatusFromV1(status v1.AtomStatus) AtomStatus {
return AtomStatus{
Workflow: status.Workflow,
Network: status.Network,
}
}
// Helper function to convert ServiceSpec from v2 to v1
func convertServiceSpecToV1(service ServiceSpec) v1.ServiceSpec {
return v1.ServiceSpec{
Type: service.Type,
Ports: service.Ports, // 注意,如果 Ports 内部的字段在 v1 和 v2 之间有差异,可能需要为 Ports 也编写转换函数
}
}
// Helper function to convert ServiceSpec from v1 to v2
func convertServiceSpecFromV1(service v1.ServiceSpec) ServiceSpec {
return ServiceSpec{
Type: service.Type,
Ports: service.Ports, // 同上,注意检查 Ports 的字段
}
}
// Helper function to convert containers from v2 to v1
func convertContainersToV1(containers []PodCondition) []v1.PodCondition {
v1Containers := make([]v1.PodCondition, len(containers))
for i, container := range containers {
v1Containers[i].Name = container.Name
v1Containers[i].Image = container.Image
v1Containers[i].Ports = container.Ports
}
return v1Containers
}
// Helper function to convert containers from v1 to v2
func convertContainersFromV1(containers []v1.PodCondition) []PodCondition {
v2Containers := make([]PodCondition, len(containers))
for i, container := range containers {
v2Containers[i].Name = container.Name
v2Containers[i].Image = container.Image
v2Containers[i].Ports = container.Ports
}
return v2Containers
}
// Helper function to convert Selector from v2 to v1
func convertSelectorToV1(selector Selector) v1.Selector {
return v1.Selector{
MatchLabels: selector.MatchLabels,
}
}
// Helper function to convert Selector from v1 to v2
func convertSelectorFromV1(selector v1.Selector) Selector {
return Selector{
MatchLabels: selector.MatchLabels,
}
}
修改main.go文件
//if err = (&appsv1.Atom{}).SetupWebhookWithManager(mgr); err != nil {
//setupLog.Error(err, "unable to create webhook", "webhook", "Atom")
//os.Exit(1)
//}
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err = (&appsv1.Atom{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "Atom")
os.Exit(1)
}
if err = (&appsv2.Atom{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "Atom")
os.Exit(1)
}
}
部署V2版本API
有了多个版本的API之后,我们在API Server中却只能持久化一个版本,这里依然选择持久化V1版本,但是需要在V1版本的Atom上增加一行注解(持久化哪个版本就在哪个版本加注解):
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:path=atoms,singular=atom,scope=Namespaced,shortName=at
//+kubebuilder:storageversion
// Atom is the Schema for the atoms API
type Atom struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec AtomSpec `json:"spec,omitempty"`
Status AtomStatus `json:"status,omitempty"`
}
至此,新版本就算完成了。这时应该添加一个Webhook用来接收API Server的conversion回调请求,之前已经配置过Webhook了,所以这里参考Operator 开发实践 四 (WebHook),配置好之后就可以开始部署测试多版本API了
打包镜像
make generate
make manifests
make docker-build IMG=atom-operator:v0.2
kind load docker-image atom-operator:v0.2 --name dev
部署CRD
# make install
test -s /Users/8lablw/Documents/atom-operator/bin/controller-gen && /Users/8lablw/Documents/atom-operator/bin/controller-gen --version | grep -q v0.11.1 || \
GOBIN=/Users/8lablw/Documents/atom-operator/bin go install sigs.k8s.io/controller-tools/cmd/[email protected]
/Users/8lablw/Documents/atom-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /Users/8lablw/Documents/atom-operator/bin/kustomize || { curl -Ss "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 4.3.0 /Users/8lablw/Documents/atom-operator/bin; }
/Users/8lablw/Documents/atom-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/atoms.apps.atom.com created
部署Operator
# make deploy IMG=atom-operator:v0.2
test -s /Users/8lablw/Documents/atom-operator/bin/controller-gen && /Users/8lablw/Documents/atom-operator/bin/controller-gen --version | grep -q v0.11.1 || \
GOBIN=/Users/8lablw/Documents/atom-operator/bin go install sigs.k8s.io/controller-tools/cmd/[email protected]
/Users/8lablw/Documents/atom-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
test -s /Users/8lablw/Documents/atom-operator/bin/kustomize || { curl -Ss "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash -s -- 4.3.0 /Users/8lablw/Documents/atom-operator/bin; }
cd config/manager && /Users/8lablw/Documents/atom-operator/bin/kustomize edit set image controller=atom-operator:v0.2
/Users/8lablw/Documents/atom-operator/bin/kustomize build config/default | kubectl apply -f -
namespace/atom-operator-system created
customresourcedefinition.apiextensions.k8s.io/atoms.apps.atom.com configured
serviceaccount/atom-operator-controller-manager created
检查部署
# kubectl get pod -n atom-operator-system
NAME READY STATUS RESTARTS AGE
atom-operator-controller-manager-79b9dcb475-gqqj2 2/2 Running 0 3m26s
部署V2版本资源
准备v2 YAML资源文件
apiVersion: apps.atom.com/v2
kind: Atom
metadata:
name: nginx-sample
namespace: default
labels:
app: nginx
spec:
deployment:
replicas: 12
selector:
matchLabels:
app: nginx
template:
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
service:
type: NodePort
ports:
- name: nginx-http
port: 80
targetPort: 80
nodePort: 30080
upgradeable: true
kubectl apply -f apps_v2_atom.yaml
此时replicas超过10,创建出现错误:admission webhook “vatom.kb.io” denied the request: replicas too many error。说明检测成功
将replicas改为2, 查看结果正常
default nginx-sample-cbdccf466-mx9qg 1/1 Running 0 31s
default nginx-sample-cbdccf466-tmh9c 1/1 Running 0 31s