前言

根据Istioctl的思路(https://segmentfault.com/a/1190000043440836),Dubboctl目前实现了manifest generate、manifest install、manifest uninstall和manifest diff这几个重要的根底命令,接下来会在补足测试用例的同时持续加强。为了让其他同学能疾速地了解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组织命令行,实现命令的加载,如下图所示:

其中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,每个文件对应一个子命令的实现。

每个子命令文件可分为以下几局部:

  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的流程如下图所示:

总的来说,利用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 optionstype 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的构造如下所示:

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.yamlapiVersion: dubbo.apache.org/v1alpha1kind: DubboOperatormetadata:  namespace: dubbo-systemspec:  componentsMeta:    zookeeper:      enabled: false  components:    admin:      replicas: 3
# default profileapiVersion: dubbo.apache.org/v1alpha1kind: DubboOperatormetadata:  namespace: dubbo-systemspec:  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 yamlapiVersion: dubbo.apache.org/v1alpha1kind: DubboOperatormetadata:  namespace: dubbo-systemspec:  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: truereplicas: 1

联合以上代码正文,咱们能够失去以下论断:

  1. yaml文件面向用户,在default profile的根底上,用户通过ComponentsMeta开启敞开组件,设置近程chart的仓库地址和版本;通过Components配置各组件,且各个组件的配置最终都会映射到各个chart的values.yaml。
  2. profile,DubboConfig,values.yaml三者强相干,假如dubbo-admin的values.yaml产生了扭转,那么DubboConfig,profile都须要进行批改。