近期的一个工作项是通过使用openkruise中的daemonset来替代原生daemonset,目标是能够借用kruise daemonset支持的热升级能力(Surging)以及灰度控制能力(selector/partition)来提升daemonset使用的稳定性,满足业务需求。
接管的操作流程暂且不表,这里说一下遇到的一个问题:如何让kustomize支持自定义CR的SMP(strategic merge patch)。这个问题的解决还走了一些弯路,做一下记录。

原始玩法

我们通过git来维护daemonset定义,基于kustomize的base/overlays的玩法来实现基础配置的共享+集群个性化配置的支持。
配置的示例如下:

  1. # tree daemonset_demo
  2. daemonset_demo
  3. ├── base
  4. ├── daemonset.yaml
  5. └── kustomization.yaml
  6. └── overlays
  7. └── cluster1
  8. ├── daemonset_site.yaml
  9. └── kustomization.yaml

文件内容如下:

# cat daemonset_demo/base/daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: base-image-ds
  namespace: default
  labels:
    agent-name: base-image-ds
spec:
  minReadySeconds: 300
  selector:
    matchLabels:
      component: base-image-ds
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1%
  template:
    metadata:
      labels:
        component: base-image-ds
    spec:
      hostNetwork: true
      tolerations:
      - operator: Exists
      restartPolicy: Always
      initContainers:
      - name: base-image-ds-1
        image: image1
        command:
        - /bin/true
      containers:
      - name: main
        image: image1
        command:
        - cat

# cat daemonset_demo/base/kustomization.yaml
resources:
- daemonset.yaml

# cat daemonset_demo/overlays/cluster1/daemonset_site.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: base-image-ds
  namespace: default
spec:
  template:
    metadata:
      labels:
        site:
    spec:
      initContainers:
      - name: base-image-ds-new
        image: image-new
        command:
        - /bin/true

# cat daemonset_demo/overlays/cluster1/kustomization.yaml
bases:
- ../../base
patches:
- daemonset_site.yaml

# kustomize build daemonset_demo/overlays/cluster1/ > ori.yaml

基于上述玩法,通过kruise daemonset接管cluster1之后,配置修改如下

# cat daemonset_demo/base-kruise/daemonset.yaml
apiVersion: apps.kruise.io/v1alpha1  # 修改
kind: DaemonSet
metadata:
  name: base-image-ds
  namespace: default
  annotations:
    daemonset.kruise.io/ignore-unscheduable: "true" # 修改
    daemonset.kruise.io/ignore-not-ready: "true" # 修改
  labels:
    daemonset.kruise.io/is-first-deployed: "false" # 修改
    agent-name: base-image-ds
spec:
  minReadySeconds: 300
  selector:
    matchLabels:
      component: base-image-ds
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1%
  template:
    metadata:
      labels:
        component: base-image-ds
    spec:
      hostNetwork: true
      tolerations:
      - operator: Exists
      restartPolicy: Always
      initContainers:
      - name: base-image-ds-1
        image: image1
        command:
        - /bin/true
      containers:
      - name: main
        image: image1
        command:
        - cat

# cat daemonset_demo/base-kruise/kustomization.yaml
resources:
- daemonset.yaml

#cat daemonset_demo/overlays/cluster1/daemonset_site.yaml
apiVersion: apps.kruise.io/v1alpha1 # 修改
kind: DaemonSet
metadata:
  name: base-image-ds
  namespace: default
spec:
  template:
    metadata:
      labels:
        site:
    spec:
      initContainers:
      - name: base-image-ds-new
        image: image-new
        command:
        - /bin/true

# cat daemonset_demo/overlays/cluster1/kustomization.yaml
bases:
- ../../base-kruise # 修改
patches:
- daemonset_site.yaml

# kustomize build daemonset_demo/overlays/cluster1/ > new.yaml

# tree daemonset_demo
daemonset_demo
├── base
│   ├── daemonset.yaml
│   └── kustomization.yaml
├── base-kruise
│   ├── daemonset.yaml
│   └── kustomization.yaml
└── overlays
    └── cluster1
        ├── daemonset_site.yaml
        └── kustomization.yaml

接管之后,对比ori.yaml和new.yaml,会发现两者并不一致
image.png

问题根因

根因在于kustomize自带了原始K8S核心Api对象的types信息,原生对象支持SMP,kustomize就能识别。而自定义类型CRD,kustomize并不能感知到其真实的type详情,只能使用JsonPatch来做patch。JsonPatch的显著的缺点,就是无法在一个list(例如上图中的initContainers list)中,根据定义的key来进行识别,针对性的进行patch。
原生DaemonSet支持name作为key来进行SMP
image.png

解决方案

考虑到用户已经习惯了kustomize的玩法,大幅修改原来的玩法对用户不太友好,也对之前的配套建设造成冲击。所以最小化的解决方案肯定还是基于kustomize来进行开发、改造。
由于对kustomize的架构不熟悉,先后摸索了三个方案,最终采用第三个方案:

方案1)【放弃】使用JsonPatch来替代StrategicMergePatch

这条路对用户并不友好,以patch修改list为例,JsonPatch需要指定修改list中指定对象的index,这个非常不人性且容易出错,并且对现有的overlays修改较大,最终放弃!

方案2)【不可行】尝试通过config+transformer

kustomize提供了generator和transformer机制,可以允许我们指定告知CR中某些部分是label,哪些是annotation,哪些字段是引用了Pod之类的,类似于argo-rollouts的做法。
后来发现,如果要识别kruise daemonset的CR,我们需要能告知例如spec/template对应的是k8s中的PodTemplate结构,而这种kustomize并没有原生识别,它能识别的如Pod,Deployment等,并不适用于本CR的场景,所以无法描述清楚我们CR的结构,kustomize也就无法做到对其支持SMP,不可行!

方案3)【最终方案】使用transformer + kustomize go plugin

这个方案来自于已有的issue,通过这种变通的方式,通过plugin的方式让kustomize直接感知到了我们指定的GroupVersionKind的真实结构,并由于结构本身支持SMP,完美的实现了对CR的SMP支持。选中此方案
具体操作步骤记录如下

a) 开发go plugin用于注册CRD的GVK

基于kustomize源码库,在sigs.k8s.io/kustomize/pkg/plugin/下增加apps.kruise.io/v1alpha1/daemonsetregister/DaemonSetRegister.go

//go:generate go run sigs.k8s.io/kustomize/v3/cmd/pluginator
package main

import (
    "k8s.io/client-go/kubernetes/scheme"

    "sigs.k8s.io/kustomize/v3/pkg/ifc"
    "sigs.k8s.io/kustomize/v3/pkg/resmap"
    "sigs.k8s.io/kustomize/v3/pkg/types"

    v1alpha1 "sigs.k8s.io/kustomize/v3/pkg/apis/kruiseapps/v1alpha1"
)

type plugin struct {
    rf               *resmap.Factory
    types.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
}

//noinspection GoUnusedGlobalVariable
var KustomizePlugin plugin

func (p *plugin) Config(ldr ifc.Loader, rf *resmap.Factory, config []byte) error {
    return v1alpha1.SchemeBuilder.AddToScheme(scheme.Scheme)
}

func (p *plugin) Transform(m resmap.ResMap) error {
    return nil
}

如上,这里把kruise DaemonSet的types信息迁移到了sigs.k8s.io/kustomize/v3/pkg/apis/kruiseapps/,以便识别(直接引用github.com/openkruise/kruise对应路径时遇到了依赖相关的问题,这里简化处理)

b) 生成golang plugin so文件

基于上述代码,生成kusomize plugin的文件

go build -buildmode=plugin -o DaemonSetRegister.so src/sigs.k8s.io/kustomize/plugin/apps.kruise.io/v1alpha1/daemonsetregister/DaemonSetRegister.go

拷贝至kustomize的plugin搜索路径即可。
搜索路径有如下两种:

  • 默认$HOME/.config/kustomize/plugin
  • 如果指定了环境变量XDG_CONFIG_HOME,则路径为$XDG_CONFIG_HOME/kustomize/plugin

这里我直接拷贝DaemonSetRegister.so到$HOME/.config/kustomize/plugin下(路径不存在则创建)。
注意:kustomize启用插件需要指定``--enable_alpha_plugins选项

c) 修改kustomization相关配置

将原来的base/overlays配置做如下修改

  • base-kruise下新增transformer.yaml,用来引入plugin

    apiVersion: apps.kruise.io/v1alpha1
    kind: DaemonSetRegister
    metadata:
    name: daemonsetregister
    
  • base-kruise的kustomization.yaml中引入transformer.yaml ```yaml resources:

  • daemonset.yaml

transformers:

  • transformer.yaml 修改完成后查看文件和差异如下yaml

    tree daemonset_demo

    daemonset_demo ├── base │ ├── daemonset.yaml │ └── kustomization.yaml ├── base-kruise │ ├── daemonset.yaml │ ├── kustomization.yaml │ └── transformer.yaml └── overlays └── cluster1
      ├── daemonset_site.yaml
      └── kustomization.yaml
    

kustomize build —enable_alpha_plugins daemonset_demo/overlays/cluster1/ > final.yaml

``` 比较差异后发现符合预期
image.png

d) 平台支持且开启—enable_alpha_plugins

上面的plugin还仅仅是放在本地,验证可行。真正管理DaemonSet时需要将对应版本的kusomize和plugin部署到平台镜像中。(如果kustmoize和plugin镜像不一致,还可能出错。。)
另外,需要将原始的kustomize build xxx命令,改成kustomize build --enable_alpha_plugins xxx

参考: