前言

依据Istioctl的思路(https://segmentfault.com/a/1190000043440836),Dubboctl目前实现了manifest generate、manifest install、manifest uninstall和manifest diff这几个重要的基础命令,接下来会在补足测试用例的同时继续增强。为了让其他同学能快速地理解Dubboctl,参与后续的开发工作,编写这个文档解释dubboctl的结构与关键流程。

结构

Dubboctl目前结构如下所示:
Dubboctl结构

概述

cmd:
负责解析命令,并利用operator与其他功能包实现命令。目前利用manifest包和 operator实现了manifest子命令。
controller:
负责响应DubboOperator CR的更新,利用operator与其他功能包实现相应的响应式功能。目前未实现该模块。
operator:
主要功能模块,提供RenderManifest、ApplyManifest、RemoveManifest等核心功能。cmd、controller等入口模块无需关注实现细节,只需要配置并调用功能即可。
components:
目前支持的组件有Admin、Grafana、Nacos、Prometheus、Skywalking、Zipkin、Zookeeper,各组件借助renderer实现RenderManifest,完成各自的渲染工作。
kube:
负责与k8s进行交互,主要使用controller-runtime库。

代码走读

Dubboctl的总体入口在/cmd/dubboctl/main.go,/pkg/dubboctl/cmd处使用cobra组织命令行,完成命令的加载,如下图所示:
image.png
其中manifest.go完成manifest子命令的加载:

func addManifest(rootCmd *cobra.Command) {
    manifestCmd := &cobra.Command{
        Use:   "manifest",
        Short: "Commands related to manifest",
        Long:  "Commands help user to generate manifest and install manifest",
    }
         // manifest generate 
    cmd.ConfigManifestGenerateCmd(manifestCmd)
         // manifest install
    cmd.ConfigManifestInstallCmd(manifestCmd)
         // manifest uninstall
    cmd.ConfigManifestUninstallCmd(manifestCmd)
         // manifest diff
    cmd.ConfigManifestDiffCmd(manifestCmd)
    rootCmd.AddCommand(manifestCmd)
}

命令的具体实现处存在于/pkg/dubboctl/internal/cmd,每个文件对应一个子命令的实现。
image.png
每个子命令文件可分为以下几部分:

  1. 该子命令接受的参数对应的结构体
  2. 配置cobra入口
  3. 子命令的实现逻辑

接下来按这三部分对manifest generate进行代码走读。

manifest generate

ManifestGenerateArgs
type ManifestGenerateArgs struct {
    // 用户的自定义配置,可配置多个,按照顺序从右往左依次覆盖
    FileNames    []string
    // 存放Helm chart的目录,默认使用/deploy/charts
    ChartsPath   string
    // 存放profile的目录,默认使用/deploy/profiles
    ProfilesPath string
    // manifest输出路径,若不设置,将默认向控制台输出
    OutputPath   string
    // 多个SetFlag,--set key=val
    SetFlags     []string
}
ConfigManifestGenerateCmd

将manifest generate接入cobra,重点关注其中的RunE字段,其中定义了子命令的执行逻辑:

RunE: func(cmd *cobra.Command, args []string) error {
            // 初始化日志模块
            logger.InitCmdSugar(zapcore.AddSync(cmd.OutOrStdout()))
            // 设置命令行参数的默认值
            mgArgs.setDefault()
            // Overlay用户自定义配置,profile,SetFlags,最终生成配置对应的DubboConfig结构体
            cfg, _, err := generateValues(mgArgs)
            if err != nil {
                return err
            }
            // 根据DubboConfig与命令行参数产生最终的manifest
            if err := generateManifests(mgArgs, cfg); err != nil {
                return err
            }
            return nil
        },
实现逻辑
generateValues

参照Istioctl(https://segmentfault.com/a/1190000043440836),generateValues的流程如下图所示:
generateValues
总的来说,利用Overlay机制将所有相关yaml映射到DubboConfig,为接下来使用DubboOperator做好准备。
结合代码进行分析:

func generateValues(mgArgs *ManifestGenerateArgs) (*v1alpha1.DubboConfig, string, error) {
    // 从mgArgs.FileNames读取用户自定义配置
    // 按照从左至右的顺序overlay用户自定义配置,并获取profile
    // profile优先级为setFlag>用户自定义配置,若未设置,则默认为default profile
    mergedYaml, profile, err := manifest.ReadYamlAndProfile(mgArgs.FileNames, mgArgs.SetFlags)
    if err != nil {
        return nil, "", fmt.Errorf("generateValues err: %v", err)
    }
    // 从mgArgs.ProfilesPath指定的路径中读取制定的profile
    // 将该profile Overlay至default profile上得到profileYaml
    profileYaml, err := manifest.ReadProfileYaml(mgArgs.ProfilesPath, profile)
    if err != nil {
        return nil, "", err
    }
    // 将mergedYaml Overlay至profileYaml得到finalYaml
    finalYaml, err := util.OverlayYAML(profileYaml, mergedYaml)
    if err != nil {
        return nil, "", err
    }
    // 将mgArgs.SetFlags设置到finalYaml
    // 由此可得,设置优先级上,SetFlags>用户自定义>select profile>default profile
    finalYaml, err = manifest.OverlaySetFlags(finalYaml, mgArgs.SetFlags)
    if err != nil {
        return nil, "", err
    }
    cfg := &v1alpha1.DubboConfig{}
    // 反序列化成DubboConfig,用于配置之后的Operator
    if err := yaml.Unmarshal([]byte(finalYaml), cfg); err != nil {
        return nil, "", err
    }
    // 设置字段
    if cfg.Spec.Components == nil {
        cfg.Spec.Components = &v1alpha1.DubboComponentsSpec{}
    }
    cfg.Spec.ProfilePath = mgArgs.ProfilesPath
    cfg.Spec.ChartPath = mgArgs.ChartsPath
    return cfg, finalYaml, nil
}

其中/pkg/dubboctl/internal/manifest包内对manifest进行处理的函数都很好理解,这里重点关注/pkg/dubboctl/internal/util/yaml.go中的OverlayYAML以及/pkg/dubboctl/internal/manifest/common.go中的OverlaySetFlags,它们是实现Overlay机制的核心,代码如下所示:

func OverlayYAML(base, overlay string) (string, error) {
    if strings.TrimSpace(base) == "" {
        return overlay, nil
    }
    if strings.TrimSpace(overlay) == "" {
        return base, nil
    }
    // 与k8s yaml相关的处理都使用sigs.k8s.io/yaml包
    bj, err := yaml.YAMLToJSON([]byte(base))
    if err != nil {
        return "", fmt.Errorf("yamlToJSON error in base: %s\n%s", err, bj)
    }
    oj, err := yaml.YAMLToJSON([]byte(overlay))
    if err != nil {
        return "", fmt.Errorf("yamlToJSON error in overlay: %s\n%s", err, oj)
    }
    if base == "" {
        bj = []byte("{}")
    }
    if overlay == "" {
        oj = []byte("{}")
    }
    
    // 使用json merge patch将overlay string覆盖于base string
    merged, err := jsonpatch.MergePatch(bj, oj)
    if err != nil {
        return "", fmt.Errorf("json merge error (%s) for base object: \n%s\n override object: \n%s", err, bj, oj)
    }
    my, err := yaml.JSONToYAML(merged)
    if err != nil {
        return "", fmt.Errorf("jsonToYAML error (%s) for merged object: \n%s", err, merged)
    }

    return string(my), nil
}

OverlayYAML的关键在于使用json merge patch将overlay覆盖于base之上,因为目前DubboConfig的字段及字段含义并不稳定,所以不考虑使用strategic merge patch。patch的相关知识可以参考https://kubernetes.io/zh-cn/docs/tasks/manage-kubernetes-obje...或其他官方文档与博客。

func OverlaySetFlags(base string, setFlags []string) (string, error) {
    baseMap := make(map[string]interface{})
    if err := yaml.Unmarshal([]byte(base), &baseMap); err != nil {
        return "", err
    }
    for _, setFlag := range setFlags {
        // 拆分--set key=val 
        key, val := SplitSetFlag(setFlag)
        // 为了能够以类似spec.components.nacos.replicas的路径访问和修改baseMap
        // 将map组织成树形结构,以pathContext进行表示
        // 第三个参数为true代表若路径上的节点不存在则直接创建
        pathCtx, _, err := GetPathContext(baseMap, util.PathFromString(key), true)
        if err != nil {
            return "", err
        }
        // 将val写入路径
        if err := WritePathContext(pathCtx, ParseValue(val), false); err != nil {
            return "", err
        }
    }
    finalYaml, err := yaml.Marshal(baseMap)
    if err != nil {
        return "", err
    }
    return string(finalYaml), nil
}

/pkg/dubboctl/internal/manifest/tree.go的PathContext通过将map组织成树,提供了以类似spec.components.nacos.replicas的路径访问和修改yaml的机制,具体细节可直接参考这块代码。
最后解释一下为什么这个函数命名为generateValues,因为最后渲染manifest要利用Helm chart,DubboConfig最终要映射成各个组件对应chart的values.yaml,所以命名为generateValues。DubboConfig的内容在之后进行解释。

generateManifests

generateManifests利用genenrateValues生成的DubboConfig配置
DubboOperator,并调用RenderManifest功能,最后根据是否指定了输出路径来对manifest进行处理,代码如下所示:

func generateManifests(mgArgs *ManifestGenerateArgs, cfg *v1alpha1.DubboConfig) error {
    // 配置Operator,第二个参数代表KubeCli,manifest generate目前不需要和k8s交互
    op, err := operator.NewDubboOperator(cfg.Spec, nil)
    if err != nil {
        return err
    }
    // 在调用功能前,必须执行Run完成初始化
    if err := op.Run(); err != nil {
        return err
    }
    // manifestMap的key为组件名,如Admin,Grafana,value为对应的manifest
    manifestMap, err := op.RenderManifest()
    if err != nil {
        return err
    }
    if mgArgs.OutputPath == "" {
        // 为了在相同的命令行参数输入下,manifest输出都能一致,对manifest进行排序
        res, err := sortManifests(manifestMap)
        if err != nil {
            return err
        }
        // 不带日志格式直接输出到控制台,方便执行dubboctl manifest generate | kubectl apply -f -
        logger.CmdSugar().Print(res)
    } else {
        // 将manifest写入指定路径,每个组件对应一个manifest文件,如admin.yaml,grafana.yaml
        if err := writeManifests(manifestMap, mgArgs.OutputPath); err != nil {
            return err
        }
    }
    return nil
}
DubboOperator

深入/pkg/dubboctl/internal/operator包,这个包包含了operator和component的相关逻辑。回顾之前的结构图,DubboOperator将渲染功能委托给各个Component,代码如下所示:

type DubboOperator struct {
    // generateValues生成的DubboConfig
    spec       *v1alpha1.DubboConfigSpec
    // 标志是否提前执行了Run
    started    bool
    // 具体的工作都委派给各组件
    components map[ComponentName]Component
    // 负责与k8s交互
    kubeCli    *kube.CtlClient
}

// 在执行其他功能前,必须先执行
func (do *DubboOperator) Run() error {
    for name, component := range do.components {
        // 每个组件都需要完成初始化
        if err := component.Run(); err != nil {
            return fmt.Errorf("component %s run failed, err: %s", name, err)
        }
    }
    do.started = true
    return nil
}

func (do *DubboOperator) RenderManifest() (map[ComponentName]string, error) {
    if !do.started {
        return nil, errors.New("DubboOperator is not running")
    }
    res := make(map[ComponentName]string)
    for name, component := range do.components {
        // 渲染功能委派给各组件
        manifest, err := component.RenderManifest()
        if err != nil {
            return nil, fmt.Errorf("component %s RenderManifest err: %v", name, err)
        }
        res[name] = manifest
    }
    return res, nil
}
Component

Component负责承接DubboOperator委派的任务,考虑到诸如Admin、Grafana的组件很多,未来会新增其他组件,因此将Component抽象成接口,并采用functional options创建具体的Component,代码如下所示:

type Component interface {
    // 初始化
    Run() error
    // 渲染各组件的manifest
    RenderManifest() (string, error)
}

type ComponentOptions struct {
    // 使用Helm chart渲染时Release的namespace
    // 该组件所有k8s资源的namespace
    Namespace string

    // 指定chart的目录路径,用于本地组件,目前Admin和Nacos的chart由我们自己维护
    // 默认为/deploy/charts
    ChartPath string

     // 用于远程组件,对于grafana、prometheus、zookeeper等比较成熟的组件,直接使用官方提供的chart可以避免不必要的成本
    // 仓库地址
    RepoURL string
    // chart版本
    Version string
}

// functional options
type ComponentOption func(*ComponentOptions)

func WithNamespace(namespace string) ComponentOption {
    return func(opts *ComponentOptions) {
        opts.Namespace = namespace
    }
}

func WithChartPath(path string) ComponentOption {
    return func(opts *ComponentOptions) {
        opts.ChartPath = path
    }
}

func WithRepoURL(url string) ComponentOption {
    return func(opts *ComponentOptions) {
        opts.RepoURL = url
    }
}

func WithVersion(version string) ComponentOption {
    return func(opts *ComponentOptions) {
        opts.Version = version
    }
}

Admin和Nacos的chart由我们自己维护,因此需要指定chart的路径。而grafana、zookeeper、prometheus、skywalking、zipkin则直接使用官方提供的chart,因此需要指定仓库地址和版本。

各组件都实现Component接口以及生成函数,因此/pkg/dubboctl/internal/operator/component.go的结构如下所示:
image.png

Renderer

Renderer的本质在于接收values.yaml并利用Helm渲染Chart,根据Chart来自于本地还是远程仓库,Renderer分为LocalRenderer和RemoteRenderer。在代码实现上,主要使用helm提供的库,可参考https://pkg.go.dev/helm.sh/helm/v3,这里不再赘述。

DubboConfig

讲到这里,可以具体分析用户自定义配置,profile,DubboConfig以及它们最终映射到各组件的values.yaml。

# user-customization.yaml
apiVersion: dubbo.apache.org/v1alpha1
kind: DubboOperator
metadata:
  namespace: dubbo-system
spec:
  componentsMeta:
    zookeeper:
      enabled: false
  components:
    admin:
      replicas: 3
# default profile
apiVersion: dubbo.apache.org/v1alpha1
kind: DubboOperator
metadata:
  namespace: dubbo-system
spec:
  profile: default
  namespace: dubbo-system
  componentsMeta:
    admin:
      enabled: true
    grafana:
      enabled: true
      repoURL: https://grafana.github.io/helm-charts
      version: 6.52.4
    nacos:
      enabled: true
    zookeeper:
      enabled: true
      repoURL: https://charts.bitnami.com/bitnami
      version: 11.1.6
    prometheus:
      enabled: true
      repoURL: https://prometheus-community.github.io/helm-charts
      version: 20.0.2
    skywalking:
      enabled: true
      repoURL: https://apache.jfrog.io/artifactory/skywalking-helm
      version: 4.3.0
    zipkin:
      enabled: true
      repoURL: https://openzipkin.github.io/zipkin
      version: 0.3.0
# final yaml
apiVersion: dubbo.apache.org/v1alpha1
kind: DubboOperator
metadata:
  namespace: dubbo-system
spec:
  profile: default
  namespace: dubbo-system
  componentsMeta:
    admin:
      enabled: true
    grafana:
      enabled: true
      repoURL: https://grafana.github.io/helm-charts
      version: 6.52.4
    nacos:
      enabled: true
    zookeeper:
      enabled: false
      repoURL: https://charts.bitnami.com/bitnami
      version: 11.1.6
    prometheus:
      enabled: true
      repoURL: https://prometheus-community.github.io/helm-charts
      version: 20.0.2
    skywalking:
      enabled: true
      repoURL: https://apache.jfrog.io/artifactory/skywalking-helm
      version: 4.3.0
    zipkin:
      enabled: true
      repoURL: https://openzipkin.github.io/zipkin
      version: 0.3.0
  components:
    admin:
      replicas: 3
//组织成CRD对应的go struct,为之后实现controller做好准备
type DubboConfig struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   *DubboConfigSpec   `json:"spec,omitempty"`
    // 暂未使用
    Status *DubboConfigStatus `json:"status,omitempty"`
}

type DubboConfigSpec struct {
    // 存放profile yaml的路径
    ProfilePath    string               `json:"profilePath,omitempty"`
    // 指定profile,默认为default
    Profile        string               `json:"profile,omitempty"`
    // 存放本地chart的路径,默认为/deploy/charts,目前该路径下有dubbo-admin和nacos
    ChartPath      string               `json:"chartPath,omitempty"`
    // dubbo control plane的所有相关资源所在的namespace
    Namespace      string               `json:"namespace,omitempty"`
    // 各组件的元数据,用于开启和关闭组件,以及确定远程chart的仓库地址和版本
    ComponentsMeta *DubboComponentsMeta `json:"componentsMeta,omitempty"`
    // 各组件chart的values.yaml所对应的go struct
    // admin和nacos由我们维护
    // grafana等成熟组件统一设置为map[string]any
    Components     *DubboComponentsSpec `json:"components,omitempty"`
}

type DubboComponentsMeta struct {
    Admin      *AdminMeta      `json:"admin,omitempty"`
    Grafana    *GrafanaMeta    `json:"grafana,omitempty"`
    Nacos      *NacosMeta      `json:"nacos,omitempty"`
    Zookeeper  *ZookeeperMeta  `json:"zookeeper,omitempty"`
    Prometheus *PrometheusMeta `json:"prometheus,omitempty"`
    Skywalking *SkywalkingMeta `json:"skywalking,omitempty"`
    Zipkin     *ZipkinMeta     `json:"zipkin,omitempty"`
}

type BaseMeta struct {
    Enabled bool `json:"enabled,omitempty"`
}

type RemoteMeta struct {
    RepoURL string `json:"repoURL,omitempty"`
    Version string `json:"version,omitempty"`
}

type AdminMeta struct {
    BaseMeta
}

type GrafanaMeta struct {
    BaseMeta
    RemoteMeta
}

type DubboComponentsSpec struct {
    Admin      *AdminSpec      `json:"admin,omitempty"`
    Grafana    *GrafanaSpec    `json:"grafana,omitempty"`
    Nacos      *NacosSpec      `json:"nacos,omitempty"`
    Zookeeper  *ZookeeperSpec  `json:"zookeeper,omitempty"`
    Prometheus *PrometheusSpec `json:"prometheus,omitempty"`
    Skywalking *SkywalkingSpec `json:"skywalking,omitempty"`
    Zipkin     *ZipkinSpec     `json:"zipkin,omitempty"`
}

type AdminSpec struct {
    Image              *Image              `json:"image,omitempty"`
    Replicas           uint32              `json:"replicas"`
    Global             *AdminGlobal        `json:"global,omitempty"`
    Rbac               *Rbac               `json:"rbac,omitempty"`
    ServiceAccount     *ServiceAccount     `json:"serviceAccount,omitempty"`
    ImagePullSecrets   []string            `json:"imagePullSecrets,omitempty"`
    Autoscaling        *Autoscaling        `json:"autoscaling,omitempty"`
    DeploymentStrategy *DeploymentStrategy `json:"deploymentStrategy,omitempty"`
    corev1.ContainerImage
}

type GrafanaSpec map[string]any
#dubbo-admin values.yaml
#未显示完整内容,请注意replicas字段
global:
  imageRegistry: ""
  ## E.g.
  ## imagePullSecrets:
  ##   - myRegistryKeySecretName
  ##
  imagePullSecrets: []


rbac:
  # Use an existing ClusterRole/Role (depending on rbac.namespaced false/true)
  enabled: true
  pspEnabled: true

replicas: 1

结合以上代码注释,我们可以得到以下结论:

  1. yaml文件面向用户,在default profile的基础上,用户通过ComponentsMeta开启关闭组件,设置远程chart的仓库地址和版本;通过Components配置各组件,且各个组件的配置最终都会映射到各个chart的values.yaml。
  2. profile,DubboConfig,values.yaml三者强相关,假设dubbo-admin的values.yaml发生了改变,那么DubboConfig,profile都需要进行修改。

DMwangnima
4 声望6 粉丝