operator/pkg/translate/translate_value.go (599 lines of code) (raw):

// Copyright Istio Authors // // 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 translate import ( "fmt" "sort" "strings" ) import ( "istio.io/api/operator/v1alpha1" "sigs.k8s.io/yaml" ) import ( "github.com/apache/dubbo-go-pixiu/operator/pkg/metrics" "github.com/apache/dubbo-go-pixiu/operator/pkg/name" "github.com/apache/dubbo-go-pixiu/operator/pkg/tpath" "github.com/apache/dubbo-go-pixiu/operator/pkg/util" "github.com/apache/dubbo-go-pixiu/operator/pkg/version" oversion "github.com/apache/dubbo-go-pixiu/operator/version" ) // ReverseTranslator is a set of mappings to translate between values.yaml and API paths, charts, k8s paths. type ReverseTranslator struct { Version version.MinorVersion // APIMapping is Values.yaml path to API path mapping using longest prefix match. If the path is a non-leaf node, // the output path is the matching portion of the path, plus any remaining output path. APIMapping map[string]*Translation `yaml:"apiMapping,omitempty"` // KubernetesPatternMapping defines mapping patterns from k8s resource paths to IstioOperator API paths. KubernetesPatternMapping map[string]string `yaml:"kubernetesPatternMapping,omitempty"` // KubernetesMapping defines actual k8s mappings generated from KubernetesPatternMapping before each translation. KubernetesMapping map[string]*Translation `yaml:"kubernetesMapping,omitempty"` // GatewayKubernetesMapping defines actual k8s mappings for gateway components generated from KubernetesPatternMapping before each translation. GatewayKubernetesMapping gatewayKubernetesMapping `yaml:"GatewayKubernetesMapping,omitempty"` // ValuesToComponentName defines mapping from value path to component name in API paths. ValuesToComponentName map[string]name.ComponentName `yaml:"valuesToComponentName,omitempty"` } type gatewayKubernetesMapping struct { IngressMapping map[string]*Translation EgressMapping map[string]*Translation } var ( // Component enablement mapping. Ex "{{.ValueComponent}}.enabled": Components.{{.ComponentName}}.enabled}", nil}, componentEnablementPattern = "Components.{{.ComponentName}}.Enabled" // specialComponentPath lists cases of component path of values.yaml we need to have special treatment. specialComponentPath = map[string]bool{ "gateways": true, "gateways.istio-ingressgateway": true, "gateways.istio-egressgateway": true, } skipTranslate = map[name.ComponentName]bool{ name.IstioBaseComponentName: true, name.IstioOperatorComponentName: true, name.IstioOperatorCustomResourceName: true, name.CNIComponentName: true, name.IstiodRemoteComponentName: true, } gatewayPathMapping = map[string]name.ComponentName{ "gateways.istio-ingressgateway": name.IngressComponentName, "gateways.istio-egressgateway": name.EgressComponentName, } ) // initAPIMapping generate the reverse mapping from original translator apiMapping. func (t *ReverseTranslator) initAPIAndComponentMapping() { ts := NewTranslator() t.APIMapping = make(map[string]*Translation) t.KubernetesMapping = make(map[string]*Translation) t.ValuesToComponentName = make(map[string]name.ComponentName) for valKey, outVal := range ts.APIMapping { t.APIMapping[outVal.OutPath] = &Translation{valKey, nil} } for cn, cm := range ts.ComponentMaps { // we use dedicated translateGateway for gateway instead if !skipTranslate[cn] && !cm.SkipReverseTranslate && !cn.IsGateway() { t.ValuesToComponentName[cm.ToHelmValuesTreeRoot] = cn } } } // initK8SMapping generates the k8s settings mapping for components that are enabled based on templates. func (t *ReverseTranslator) initK8SMapping() error { outputMapping := make(map[string]*Translation) for valKey, componentName := range t.ValuesToComponentName { for K8SValKey, outPathTmpl := range t.KubernetesPatternMapping { newKey, err := renderComponentName(K8SValKey, valKey) if err != nil { return err } newVal, err := renderFeatureComponentPathTemplate(outPathTmpl, componentName) if err != nil { return err } outputMapping[newKey] = &Translation{newVal, nil} } } t.KubernetesMapping = outputMapping igwOutputMapping := make(map[string]*Translation) egwOutputMapping := make(map[string]*Translation) for valKey, componentName := range gatewayPathMapping { mapping := igwOutputMapping if componentName == name.EgressComponentName { mapping = egwOutputMapping } for K8SValKey, outPathTmpl := range t.KubernetesPatternMapping { newKey, err := renderComponentName(K8SValKey, valKey) if err != nil { return err } newP := util.PathFromString(outPathTmpl) mapping[newKey] = &Translation{newP[len(newP)-2:].String(), nil} } } t.GatewayKubernetesMapping = gatewayKubernetesMapping{IngressMapping: igwOutputMapping, EgressMapping: egwOutputMapping} return nil } // NewReverseTranslator creates a new ReverseTranslator for minorVersion and returns a ptr to it. func NewReverseTranslator() *ReverseTranslator { rt := &ReverseTranslator{ KubernetesPatternMapping: map[string]string{ "{{.ValueComponentName}}.env": "Components.{{.ComponentName}}.K8s.Env", "{{.ValueComponentName}}.autoscaleEnabled": "Components.{{.ComponentName}}.K8s.HpaSpec", "{{.ValueComponentName}}.imagePullPolicy": "Components.{{.ComponentName}}.K8s.ImagePullPolicy", "{{.ValueComponentName}}.nodeSelector": "Components.{{.ComponentName}}.K8s.NodeSelector", "{{.ValueComponentName}}.tolerations": "Components.{{.ComponentName}}.K8s.Tolerations", "{{.ValueComponentName}}.podDisruptionBudget": "Components.{{.ComponentName}}.K8s.PodDisruptionBudget", "{{.ValueComponentName}}.podAnnotations": "Components.{{.ComponentName}}.K8s.PodAnnotations", "{{.ValueComponentName}}.priorityClassName": "Components.{{.ComponentName}}.K8s.PriorityClassName", "{{.ValueComponentName}}.readinessProbe": "Components.{{.ComponentName}}.K8s.ReadinessProbe", "{{.ValueComponentName}}.replicaCount": "Components.{{.ComponentName}}.K8s.ReplicaCount", "{{.ValueComponentName}}.resources": "Components.{{.ComponentName}}.K8s.Resources", "{{.ValueComponentName}}.rollingMaxSurge": "Components.{{.ComponentName}}.K8s.Strategy", "{{.ValueComponentName}}.rollingMaxUnavailable": "Components.{{.ComponentName}}.K8s.Strategy", "{{.ValueComponentName}}.serviceAnnotations": "Components.{{.ComponentName}}.K8s.ServiceAnnotations", }, } rt.initAPIAndComponentMapping() rt.Version = oversion.OperatorBinaryVersion.MinorVersion return rt } // TranslateFromValueToSpec translates from values.yaml value to IstioOperatorSpec. func (t *ReverseTranslator) TranslateFromValueToSpec(values []byte, force bool) (controlPlaneSpec *v1alpha1.IstioOperatorSpec, err error) { yamlTree := make(map[string]interface{}) err = yaml.Unmarshal(values, &yamlTree) if err != nil { return nil, fmt.Errorf("error when unmarshalling into untype tree %v", err) } outputTree := make(map[string]interface{}) err = t.TranslateTree(yamlTree, outputTree, nil) if err != nil { return nil, err } outputVal, err := yaml.Marshal(outputTree) if err != nil { return nil, err } cpSpec := &v1alpha1.IstioOperatorSpec{} err = util.UnmarshalWithJSONPB(string(outputVal), cpSpec, force) if err != nil { return nil, fmt.Errorf("error when unmarshalling into control plane spec %v, \nyaml:\n %s", err, outputVal) } return cpSpec, nil } // TranslateTree translates input value.yaml Tree to ControlPlaneSpec Tree. func (t *ReverseTranslator) TranslateTree(valueTree map[string]interface{}, cpSpecTree map[string]interface{}, path util.Path) error { // translate enablement and namespace err := t.setEnablementFromValue(valueTree, cpSpecTree) if err != nil { return fmt.Errorf("error when translating enablement and namespace from value.yaml tree: %v", err) } // translate with api mapping err = t.translateAPI(valueTree, cpSpecTree) if err != nil { return fmt.Errorf("error when translating value.yaml tree with global mapping: %v", err) } // translate with k8s mapping if err := t.TranslateK8S(valueTree, cpSpecTree); err != nil { return err } if err := t.translateGateway(valueTree, cpSpecTree); err != nil { return fmt.Errorf("error when translating gateway with kubernetes mapping: %v", err.Error()) } // translate remaining untranslated paths into component values err = t.translateRemainingPaths(valueTree, cpSpecTree, nil) if err != nil { return fmt.Errorf("error when translating remaining path: %v", err) } return nil } // TranslateK8S is a helper function to translate k8s settings from values.yaml to IstioOperator, except for gateways. func (t *ReverseTranslator) TranslateK8S(valueTree map[string]interface{}, cpSpecTree map[string]interface{}) error { // translate with k8s mapping if err := t.initK8SMapping(); err != nil { return fmt.Errorf("error when initiating k8s mapping: %v", err) } if err := t.translateK8sTree(valueTree, cpSpecTree, t.KubernetesMapping); err != nil { return fmt.Errorf("error when translating value.yaml tree with kubernetes mapping: %v", err) } return nil } // setEnablementFromValue translates the enablement value of components in the values.yaml // tree, based on feature/component inheritance relationship. func (t *ReverseTranslator) setEnablementFromValue(valueSpec map[string]interface{}, root map[string]interface{}) error { for _, cni := range t.ValuesToComponentName { enabled, pathExist, err := IsComponentEnabledFromValue(cni, valueSpec) if err != nil { return err } if !pathExist { continue } tmpl := componentEnablementPattern ceVal, err := renderFeatureComponentPathTemplate(tmpl, cni) if err != nil { return err } outCP := util.ToYAMLPath(ceVal) // set component enablement if err := tpath.WriteNode(root, outCP, enabled); err != nil { return err } } return nil } // WarningForGatewayK8SSettings creates deprecated warning messages // when user try to set kubernetes settings for gateways via values api. func (t *ReverseTranslator) WarningForGatewayK8SSettings(valuesOverlay string) (string, error) { gwOverlay, err := tpath.GetConfigSubtree(valuesOverlay, "gateways") if err != nil { return "", fmt.Errorf("error getting gateways overlay from valuesOverlayYaml %v", err) } if gwOverlay == "" { return "", nil } var deprecatedFields []string for inPath := range t.GatewayKubernetesMapping.IngressMapping { _, found, err := tpath.GetPathContext(valuesOverlay, util.ToYAMLPath(inPath), false) if err != nil { scope.Debug(err.Error()) continue } if found { deprecatedFields = append(deprecatedFields, inPath) } } for inPath := range t.GatewayKubernetesMapping.EgressMapping { _, found, err := tpath.GetPathContext(valuesOverlay, util.ToYAMLPath(inPath), false) if err != nil { scope.Debug(err.Error()) continue } if found { deprecatedFields = append(deprecatedFields, inPath) } } if len(deprecatedFields) == 0 { return "", nil } warningMessage := fmt.Sprintf("using deprecated values api paths: %s.\n"+ " please use k8s spec of gateway components instead\n", strings.Join(deprecatedFields, ",")) return warningMessage, nil } // translateGateway handles translation for gateways specific configuration func (t *ReverseTranslator) translateGateway(valueSpec map[string]interface{}, root map[string]interface{}) error { for inPath, outPath := range gatewayPathMapping { enabled, pathExist, err := IsComponentEnabledFromValue(outPath, valueSpec) if err != nil { return err } if !pathExist && !enabled { continue } gwSpecs := make([]map[string]interface{}, 1) gwSpec := make(map[string]interface{}) gwSpecs[0] = gwSpec gwSpec["enabled"] = enabled gwSpec["name"] = util.ToYAMLPath(inPath)[1] outCP := util.ToYAMLPath("Components." + string(outPath)) if enabled { mapping := t.GatewayKubernetesMapping.IngressMapping if outPath == name.EgressComponentName { mapping = t.GatewayKubernetesMapping.EgressMapping } err = t.translateK8sTree(valueSpec, gwSpec, mapping) if err != nil { return err } } err = tpath.WriteNode(root, outCP, gwSpecs) if err != nil { return err } } return nil } // TranslateK8SfromValueToIOP use reverse translation to convert k8s settings defined in values API to IOP API. // this ensures that user overlays that set k8s through spec.values // are not overridden by spec.components.X.k8s settings in the base profiles func (t *ReverseTranslator) TranslateK8SfromValueToIOP(userOverlayYaml string) (string, error) { valuesOverlay, err := tpath.GetConfigSubtree(userOverlayYaml, "spec.values") if err != nil { scope.Debugf("no spec.values section from userOverlayYaml %v", err) return "", nil } valuesOverlayTree := make(map[string]interface{}) err = yaml.Unmarshal([]byte(valuesOverlay), &valuesOverlayTree) if err != nil { return "", fmt.Errorf("error unmarshalling values overlay yaml into untype tree %v", err) } iopSpecTree := make(map[string]interface{}) iopSpecOverlay, err := tpath.GetConfigSubtree(userOverlayYaml, "spec") if err != nil { return "", fmt.Errorf("error getting iop spec subtree from overlay yaml %v", err) } err = yaml.Unmarshal([]byte(iopSpecOverlay), &iopSpecTree) if err != nil { return "", fmt.Errorf("error unmarshalling spec overlay yaml into tree %v", err) } if err = t.TranslateK8S(valuesOverlayTree, iopSpecTree); err != nil { return "", err } warning, err := t.WarningForGatewayK8SSettings(valuesOverlay) if err != nil { return "", fmt.Errorf("error handling values gateway k8s settings: %v", err) } if warning != "" { return "", fmt.Errorf(warning) } iopSpecTreeYAML, err := yaml.Marshal(iopSpecTree) if err != nil { return "", fmt.Errorf("error marshaling reverse translated tree %v", err) } iopTreeYAML, err := tpath.AddSpecRoot(string(iopSpecTreeYAML)) if err != nil { return "", fmt.Errorf("error adding spec root: %v", err) } // overlay the reverse translated iopTreeYAML back to userOverlayYaml finalYAML, err := util.OverlayYAML(userOverlayYaml, iopTreeYAML) if err != nil { return "", fmt.Errorf("failed to overlay the reverse translated iopTreeYAML: %v", err) } return finalYAML, err } // translateStrategy translates Deployment Strategy related configurations from helm values.yaml tree. func translateStrategy(fieldName string, outPath string, value interface{}, cpSpecTree map[string]interface{}) error { fieldMap := map[string]string{ "rollingMaxSurge": "maxSurge", "rollingMaxUnavailable": "maxUnavailable", } newFieldName, ok := fieldMap[fieldName] if !ok { return fmt.Errorf("expected field name found in values.yaml: %s", fieldName) } outPath += ".rollingUpdate." + newFieldName scope.Debugf("path has value in helm Value.yaml tree, mapping to output path %s", outPath) if err := tpath.WriteNode(cpSpecTree, util.ToYAMLPath(outPath), value); err != nil { return err } return nil } // translateHPASpec translates HPA related configurations from helm values.yaml tree. // do not translate if autoscaleEnabled is explicitly set to false func translateHPASpec(inPath string, outPath string, valueTree map[string]interface{}, cpSpecTree map[string]interface{}) error { m, found, err := tpath.Find(valueTree, util.ToYAMLPath(inPath)) if err != nil { return err } if found { asEnabled, ok := m.(bool) if !ok { return fmt.Errorf("expect autoscaleEnabled node type to be bool but got: %T", m) } if !asEnabled { return nil } } newP := util.PathFromString(inPath) // last path element is k8s setting name newPS := newP[:len(newP)-1].String() valMap := map[string]string{ ".autoscaleMin": ".minReplicas", ".autoscaleMax": ".maxReplicas", } for key, newVal := range valMap { valPath := newPS + key asVal, found, err := tpath.Find(valueTree, util.ToYAMLPath(valPath)) if found && err == nil { if err := setOutputAndClean(valPath, outPath+newVal, asVal, valueTree, cpSpecTree, true); err != nil { return err } } } valPath := newPS + ".cpu.targetAverageUtilization" asVal, found, err := tpath.Find(valueTree, util.ToYAMLPath(valPath)) if found && err == nil { rs := make([]interface{}, 1) rsVal := ` - type: Resource resource: name: cpu target: type: Utilization averageUtilization: %f` rsString := fmt.Sprintf(rsVal, asVal) if err = yaml.Unmarshal([]byte(rsString), &rs); err != nil { return err } if err := setOutputAndClean(valPath, outPath+".metrics", rs, valueTree, cpSpecTree, true); err != nil { return err } } // There is no direct source from value.yaml for scaleTargetRef value, we need to construct from component name if found { revision := "" rev, ok := cpSpecTree["revision"] if ok { revision = rev.(string) } st := make(map[string]interface{}) stVal := ` apiVersion: apps/v1 kind: Deployment name: %s` // need to do special handling for gateways if specialComponentPath[newPS] && len(newP) > 2 { newPS = newP[1 : len(newP)-1].String() } // convert from values component name to correct deployment target if newPS == "pilot" { newPS = "istiod" if revision != "" { newPS = newPS + "-" + revision } } stString := fmt.Sprintf(stVal, newPS) if err := yaml.Unmarshal([]byte(stString), &st); err != nil { return err } if err := setOutputAndClean(valPath, outPath+".scaleTargetRef", st, valueTree, cpSpecTree, false); err != nil { return err } } return nil } // setOutputAndClean is helper function to set value of iscp tree and clean the original value from value.yaml tree. func setOutputAndClean(valPath, outPath string, outVal interface{}, valueTree, cpSpecTree map[string]interface{}, clean bool) error { scope.Debugf("path has value in helm Value.yaml tree, mapping to output path %s", outPath) if err := tpath.WriteNode(cpSpecTree, util.ToYAMLPath(outPath), outVal); err != nil { return err } if !clean { return nil } if _, err := tpath.Delete(valueTree, util.ToYAMLPath(valPath)); err != nil { return err } return nil } // translateEnv translates env value from helm values.yaml tree. func translateEnv(outPath string, value interface{}, cpSpecTree map[string]interface{}) error { envMap, ok := value.(map[string]interface{}) if !ok { return fmt.Errorf("expect env node type to be map[string]interface{} but got: %T", value) } if len(envMap) == 0 { return nil } scope.Debugf("path has value in helm Value.yaml tree, mapping to output path %s", outPath) nc, found, _ := tpath.GetPathContext(cpSpecTree, util.ToYAMLPath(outPath), false) var envValStr []byte if nc != nil { envValStr, _ = yaml.Marshal(nc.Node) } if !found || strings.TrimSpace(string(envValStr)) == "{}" { scope.Debugf("path doesn't have value in k8s setting with output path %s, override with helm Value.yaml tree", outPath) outEnv := make([]map[string]interface{}, len(envMap)) keys := make([]string, 0, len(envMap)) for k := range envMap { keys = append(keys, k) } sort.Strings(keys) for i, k := range keys { outEnv[i] = make(map[string]interface{}) outEnv[i]["name"] = k outEnv[i]["value"] = fmt.Sprintf("%v", envMap[k]) } if err := tpath.WriteNode(cpSpecTree, util.ToYAMLPath(outPath), outEnv); err != nil { return err } } else { scope.Debugf("path has value in k8s setting with output path %s, merge it with helm Value.yaml tree", outPath) keys := make([]string, 0, len(envMap)) for k := range envMap { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { outEnv := make(map[string]interface{}) outEnv["name"] = k outEnv["value"] = fmt.Sprintf("%v", envMap[k]) if err := tpath.MergeNode(cpSpecTree, util.ToYAMLPath(outPath), outEnv); err != nil { return err } } } return nil } // translateK8sTree is internal method for translating K8s configurations from value.yaml tree. func (t *ReverseTranslator) translateK8sTree(valueTree map[string]interface{}, cpSpecTree map[string]interface{}, mapping map[string]*Translation) error { for inPath, v := range mapping { scope.Debugf("Checking for k8s path %s in helm Value.yaml tree", inPath) path := util.PathFromString(inPath) k8sSettingName := "" if len(path) != 0 { k8sSettingName = path[len(path)-1] } if k8sSettingName == "autoscaleEnabled" { if err := translateHPASpec(inPath, v.OutPath, valueTree, cpSpecTree); err != nil { return fmt.Errorf("error in translating K8s HPA spec: %s", err) } metrics.LegacyPathTranslationTotal.Increment() continue } m, found, err := tpath.Find(valueTree, util.ToYAMLPath(inPath)) if err != nil { return err } if !found { scope.Debugf("path %s not found in helm Value.yaml tree, skip mapping.", inPath) continue } if mstr, ok := m.(string); ok && mstr == "" { scope.Debugf("path %s is empty string, skip mapping.", inPath) continue } // Zero int values are due to proto3 compiling to scalars rather than ptrs. Skip these because values of 0 are // the default in destination fields and need not be set explicitly. if mint, ok := util.ToIntValue(m); ok && mint == 0 { scope.Debugf("path %s is int 0, skip mapping.", inPath) continue } switch k8sSettingName { case "env": err := translateEnv(v.OutPath, m, cpSpecTree) if err != nil { return fmt.Errorf("error in translating k8s Env: %s", err) } case "rollingMaxSurge", "rollingMaxUnavailable": err := translateStrategy(k8sSettingName, v.OutPath, m, cpSpecTree) if err != nil { return fmt.Errorf("error in translating k8s Strategy: %s", err) } default: if util.IsValueNilOrDefault(m) { continue } output := util.ToYAMLPath(v.OutPath) scope.Debugf("path has value in helm Value.yaml tree, mapping to output path %s", output) if err := tpath.WriteNode(cpSpecTree, output, m); err != nil { return err } } metrics.LegacyPathTranslationTotal.Increment() if _, err := tpath.Delete(valueTree, util.ToYAMLPath(inPath)); err != nil { return err } } return nil } // translateRemainingPaths translates remaining paths that are not available in existing mappings. func (t *ReverseTranslator) translateRemainingPaths(valueTree map[string]interface{}, cpSpecTree map[string]interface{}, path util.Path) error { for key, val := range valueTree { newPath := append(path, key) // value set to nil means no translation needed or being translated already. if val == nil { continue } switch node := val.(type) { case map[string]interface{}: err := t.translateRemainingPaths(node, cpSpecTree, newPath) if err != nil { return err } case []interface{}: if err := tpath.WriteNode(cpSpecTree, util.ToYAMLPath("Values."+newPath.String()), node); err != nil { return err } // remaining leaf need to be put into root.values default: if t.isEnablementPath(newPath) { continue } if err := tpath.WriteNode(cpSpecTree, util.ToYAMLPath("Values."+newPath.String()), val); err != nil { return err } } } return nil } // translateAPI is internal method for translating value.yaml tree based on API mapping. func (t *ReverseTranslator) translateAPI(valueTree map[string]interface{}, cpSpecTree map[string]interface{}) error { for inPath, v := range t.APIMapping { scope.Debugf("Checking for path %s in helm Value.yaml tree", inPath) m, found, err := tpath.Find(valueTree, util.ToYAMLPath(inPath)) if err != nil { return err } if !found { scope.Debugf("path %s not found in helm Value.yaml tree, skip mapping.", inPath) continue } if mstr, ok := m.(string); ok && mstr == "" { scope.Debugf("path %s is empty string, skip mapping.", inPath) continue } // Zero int values are due to proto3 compiling to scalars rather than ptrs. Skip these because values of 0 are // the default in destination fields and need not be set explicitly. if mint, ok := util.ToIntValue(m); ok && mint == 0 { scope.Debugf("path %s is int 0, skip mapping.", inPath) continue } path := util.ToYAMLPath(v.OutPath) scope.Debugf("path has value in helm Value.yaml tree, mapping to output path %s", path) metrics.LegacyPathTranslationTotal. With(metrics.ResourceKindLabel.Value(inPath)).Increment() if err := tpath.WriteNode(cpSpecTree, path, m); err != nil { return err } if _, err := tpath.Delete(valueTree, util.ToYAMLPath(inPath)); err != nil { return err } } return nil } // isEnablementPath is helper function to check whether paths represent enablement of components in values.yaml func (t *ReverseTranslator) isEnablementPath(path util.Path) bool { if len(path) < 2 || path[len(path)-1] != "enabled" { return false } pf := path[:len(path)-1].String() if specialComponentPath[pf] { return true } _, exist := t.ValuesToComponentName[pf] return exist } // renderComponentName renders a template of the form <path>{{.ComponentName}}<path> with // the supplied parameters. func renderComponentName(tmpl string, componentName string) (string, error) { type temp struct { ValueComponentName string } return util.RenderTemplate(tmpl, temp{componentName}) }