operator/pkg/translate/translate.go (841 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 defines translations from installer proto to values.yaml. package translate import ( "encoding/json" "fmt" "reflect" "sort" "strings" ) import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "istio.io/api/operator/v1alpha1" "istio.io/pkg/log" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/yaml" ) import ( "github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio" iopv1alpha1 "github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio/v1alpha1" "github.com/apache/dubbo-go-pixiu/operator/pkg/name" "github.com/apache/dubbo-go-pixiu/operator/pkg/object" "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" ) const ( // HelmValuesEnabledSubpath is the subpath from the component root to the enabled parameter. HelmValuesEnabledSubpath = "enabled" // HelmValuesNamespaceSubpath is the subpath from the component root to the namespace parameter. HelmValuesNamespaceSubpath = "namespace" // HelmValuesHubSubpath is the subpath from the component root to the hub parameter. HelmValuesHubSubpath = "hub" // HelmValuesTagSubpath is the subpath from the component root to the tag parameter. HelmValuesTagSubpath = "tag" // default ingress gateway name defaultIngressGWName = "istio-ingressgateway" // default egress gateway name defaultEgressGWName = "istio-egressgateway" ) var scope = log.RegisterScope("translator", "API translator", 0) // Translator is a set of mappings to translate between API paths, charts, values.yaml and k8s paths. type Translator struct { // Translations remain the same within a minor version. Version version.MinorVersion // APIMapping is a mapping between an API path and the corresponding values.yaml path 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"` // KubernetesMapping defines mappings from an IstioOperator API paths to k8s resource paths. KubernetesMapping map[string]*Translation `yaml:"kubernetesMapping"` // GlobalNamespaces maps feature namespaces to Helm global namespace definitions. GlobalNamespaces map[name.ComponentName]string `yaml:"globalNamespaces"` // ComponentMaps is a set of mappings for each Istio component. ComponentMaps map[name.ComponentName]*ComponentMaps `yaml:"componentMaps"` // checkedDeprecatedAutoscalingFields represents whether the translator already checked the deprecated fields already. // Different components do not need to rerun the translation logic checkedDeprecatedAutoscalingFields bool } // ComponentMaps is a set of mappings for an Istio component. type ComponentMaps struct { // ResourceType maps a ComponentName to the type of the rendered k8s resource. ResourceType string // ResourceName maps a ComponentName to the name of the rendered k8s resource. ResourceName string // ContainerName maps a ComponentName to the name of the container in a Deployment. ContainerName string // HelmSubdir is a mapping between a component name and the subdirectory of the component Chart. HelmSubdir string // ToHelmValuesTreeRoot is the tree root in values YAML files for the component. ToHelmValuesTreeRoot string // SkipReverseTranslate defines whether reverse translate of this component need to be skipped. SkipReverseTranslate bool } // TranslationFunc maps a yamlStr API path into a YAML values tree. type TranslationFunc func(t *Translation, root map[string]interface{}, valuesPath string, value interface{}) error // Translation is a mapping to an output path using a translation function. type Translation struct { // OutPath defines the position in the yaml file OutPath string `yaml:"outPath"` translationFunc TranslationFunc } // NewTranslator creates a new translator for minorVersion and returns a ptr to it. func NewTranslator() *Translator { t := &Translator{ Version: oversion.OperatorBinaryVersion.MinorVersion, APIMapping: map[string]*Translation{ "hub": {OutPath: "global.hub"}, "tag": {OutPath: "global.tag"}, "revision": {OutPath: "revision"}, "meshConfig": {OutPath: "meshConfig"}, }, GlobalNamespaces: map[name.ComponentName]string{ name.PilotComponentName: "istioNamespace", }, ComponentMaps: map[name.ComponentName]*ComponentMaps{ name.IstioBaseComponentName: { HelmSubdir: "base", ToHelmValuesTreeRoot: "global", SkipReverseTranslate: true, }, name.PilotComponentName: { ResourceType: "Deployment", ResourceName: "istiod", ContainerName: "discovery", HelmSubdir: "istio-control/istio-discovery", ToHelmValuesTreeRoot: "pilot", }, name.IngressComponentName: { ResourceType: "Deployment", ResourceName: "istio-ingressgateway", ContainerName: "istio-proxy", HelmSubdir: "gateways/istio-ingress", ToHelmValuesTreeRoot: "gateways.istio-ingressgateway", }, name.EgressComponentName: { ResourceType: "Deployment", ResourceName: "istio-egressgateway", ContainerName: "istio-proxy", HelmSubdir: "gateways/istio-egress", ToHelmValuesTreeRoot: "gateways.istio-egressgateway", }, name.CNIComponentName: { ResourceType: "DaemonSet", ResourceName: "istio-cni-node", ContainerName: "install-cni", HelmSubdir: "istio-cni", ToHelmValuesTreeRoot: "cni", }, name.IstiodRemoteComponentName: { HelmSubdir: "istiod-remote", ToHelmValuesTreeRoot: "global", SkipReverseTranslate: true, }, }, // nolint: lll KubernetesMapping: map[string]*Translation{ "Components.{{.ComponentName}}.K8S.Affinity": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.affinity"}, "Components.{{.ComponentName}}.K8S.Env": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.containers.[name:{{.ContainerName}}].env"}, "Components.{{.ComponentName}}.K8S.HpaSpec": {OutPath: "[HorizontalPodAutoscaler:{{.ResourceName}}].spec"}, "Components.{{.ComponentName}}.K8S.ImagePullPolicy": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.containers.[name:{{.ContainerName}}].imagePullPolicy"}, "Components.{{.ComponentName}}.K8S.NodeSelector": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.nodeSelector"}, "Components.{{.ComponentName}}.K8S.PodDisruptionBudget": {OutPath: "[PodDisruptionBudget:{{.ResourceName}}].spec"}, "Components.{{.ComponentName}}.K8S.PodAnnotations": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.metadata.annotations"}, "Components.{{.ComponentName}}.K8S.PriorityClassName": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.priorityClassName."}, "Components.{{.ComponentName}}.K8S.ReadinessProbe": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.containers.[name:{{.ContainerName}}].readinessProbe"}, "Components.{{.ComponentName}}.K8S.ReplicaCount": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.replicas"}, "Components.{{.ComponentName}}.K8S.Resources": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.containers.[name:{{.ContainerName}}].resources"}, "Components.{{.ComponentName}}.K8S.Strategy": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.strategy"}, "Components.{{.ComponentName}}.K8S.Tolerations": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.tolerations"}, "Components.{{.ComponentName}}.K8S.ServiceAnnotations": {OutPath: "[Service:{{.ResourceName}}].metadata.annotations"}, "Components.{{.ComponentName}}.K8S.Service": {OutPath: "[Service:{{.ResourceName}}].spec"}, "Components.{{.ComponentName}}.K8S.SecurityContext": {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.securityContext"}, }, } return t } // OverlayK8sSettings overlays k8s settings from iop over the manifest objects, based on t's translation mappings. func (t *Translator) OverlayK8sSettings(yml string, iop *v1alpha1.IstioOperatorSpec, componentName name.ComponentName, resourceName string, index int) (string, error, ) { // om is a map of kind:name string to Object ptr. // This is lazy loaded to avoid parsing when there are no overlays var om map[string]*object.K8sObject var objects object.K8sObjects for inPath, v := range t.KubernetesMapping { inPath, err := renderFeatureComponentPathTemplate(inPath, componentName) if err != nil { return "", err } renderedInPath := strings.Replace(inPath, "gressGateways.", "gressGateways."+fmt.Sprint(index)+".", 1) scope.Debugf("Checking for path %s in IstioOperatorSpec", renderedInPath) m, found, err := tpath.GetFromStructPath(iop, renderedInPath) if err != nil { return "", err } if !found { scope.Debugf("path %s not found in IstioOperatorSpec, skip mapping.", renderedInPath) continue } if mstr, ok := m.(string); ok && mstr == "" { scope.Debugf("path %s is empty string, skip mapping.", renderedInPath) 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.", renderedInPath) continue } if componentName == name.IstioBaseComponentName { return "", fmt.Errorf("base component can only have k8s.overlays, not other K8s settings") } inPathParts := strings.Split(inPath, ".") outPath, err := t.renderResourceComponentPathTemplate(v.OutPath, componentName, resourceName, iop.Revision) if err != nil { return "", err } scope.Debugf("path has value in IstioOperatorSpec, mapping to output path %s", outPath) path := util.PathFromString(outPath) pe := path[0] // Output path must start with [kind:name], which is used to map to the object to overlay. if !util.IsKVPathElement(pe) { return "", fmt.Errorf("path %s has an unexpected first element %s in OverlayK8sSettings", path, pe) } // We need to apply overlay, lazy load om if om == nil { objects, err = object.ParseK8sObjectsFromYAMLManifest(yml) if err != nil { return "", err } if scope.DebugEnabled() { scope.Debugf("Manifest contains the following objects:") for _, o := range objects { scope.Debugf("%s", o.HashNameKind()) } } om = objects.ToNameKindMap() } // After brackets are removed, the remaining "kind:name" is the same format as the keys in om. pe, _ = util.RemoveBrackets(pe) oo, ok := om[pe] if !ok { // skip to overlay the K8s settings if the corresponding resource doesn't exist. scope.Infof("resource Kind:name %s doesn't exist in the output manifest, skip overlay.", pe) continue } // When autoscale is enabled we should not overwrite replica count, consider following scenario: // 0. Set values.pilot.autoscaleEnabled=true, components.pilot.k8s.replicaCount=1 // 1. In istio operator it "caches" the generated manifests (with istiod.replicas=1) // 2. HPA autoscales our pilot replicas to 3 // 3. Set values.pilot.autoscaleEnabled=false // 4. The generated manifests (with istiod.replicas=1) is same as istio operator "cache", // the deployment will not get updated unless istio operator is restarted. if inPathParts[len(inPathParts)-1] == "ReplicaCount" { if skipReplicaCountWithAutoscaleEnabled(iop, componentName) { continue } } // strategic merge overlay m to the base object oo mergedObj, err := MergeK8sObject(oo, m, path[1:]) if err != nil { return "", err } // Apply the workaround for merging service ports with (port,protocol) composite // keys instead of just the merging by port. if inPathParts[len(inPathParts)-1] == "Service" { if msvc, ok := m.(*v1alpha1.ServiceSpec); ok { mergedObj, err = t.fixMergedObjectWithCustomServicePortOverlay(oo, msvc, mergedObj) if err != nil { return "", err } } } // Update the original object in objects slice, since the output should be ordered. *(om[pe]) = *mergedObj } if objects != nil { return objects.YAMLManifest() } return yml, nil } var componentToAutoScaleEnabledPath = map[name.ComponentName]string{ name.PilotComponentName: "pilot.autoscaleEnabled", name.IngressComponentName: "gateways.istio-ingressgateway.autoscaleEnabled", name.EgressComponentName: "gateways.istio-egressgateway.autoscaleEnabled", } // checkDeprecatedHPAFields is a helper function to check for the deprecated fields usage in HorizontalPodAutoscalerSpec func checkDeprecatedHPAFields(iop *v1alpha1.IstioOperatorSpec) bool { hpaSpecs := []*v1alpha1.HorizontalPodAutoscalerSpec{} if iop.GetComponents().GetPilot().GetK8S().GetHpaSpec() != nil { hpaSpecs = append(hpaSpecs, iop.GetComponents().GetPilot().GetK8S().GetHpaSpec()) } for _, gwSpec := range iop.GetComponents().GetIngressGateways() { if gwSpec.Name == defaultIngressGWName && gwSpec.GetK8S().GetHpaSpec() != nil { hpaSpecs = append(hpaSpecs, gwSpec.GetK8S().GetHpaSpec()) } } for _, gwSpec := range iop.GetComponents().GetEgressGateways() { if gwSpec.Name == defaultEgressGWName && gwSpec.GetK8S().GetHpaSpec() != nil { hpaSpecs = append(hpaSpecs, gwSpec.GetK8S().GetHpaSpec()) } } for _, hpaSpec := range hpaSpecs { if hpaSpec.GetMetrics() != nil { for _, me := range hpaSpec.GetMetrics() { // nolint: staticcheck if me.GetObject().GetMetricName() != "" || me.GetObject().GetAverageValue() != nil || // nolint: staticcheck me.GetObject().GetSelector() != nil || me.GetObject().GetTargetValue() != nil { return true } // nolint: staticcheck if me.GetPods().GetMetricName() != "" || me.GetPods().GetSelector() != nil || // nolint: staticcheck me.GetPods().GetTargetAverageValue() != nil { return true } // nolint: staticcheck if me.GetResource().GetTargetAverageValue() != nil || me.GetResource().GetTargetAverageUtilization() != 0 { return true } // nolint: staticcheck if me.GetExternal().GetTargetAverageValue() != nil || me.GetExternal().GetTargetValue() != nil || // nolint: staticcheck me.GetExternal().GetMetricName() != "" || me.GetExternal().GetMetricSelector() != nil { return true } } } } return false } // translateDeprecatedAutoscalingFields checks for existence of deprecated HPA fields, if found, set values.global.autoscalingv2API to false // It only needs to run the logic for the first component because we are setting the values.global field instead of per component ones. // we do not set per component values because we may want to avoid mixture of v2 and v2beta1 autoscaling templates usage func (t *Translator) translateDeprecatedAutoscalingFields(values map[string]interface{}, iop *v1alpha1.IstioOperatorSpec) error { if t.checkedDeprecatedAutoscalingFields || checkDeprecatedHPAFields(iop) { path := util.PathFromString("global.autoscalingv2API") if err := tpath.WriteNode(values, path, false); err != nil { return fmt.Errorf("failed to set autoscalingv2API path: %v", err) } t.checkedDeprecatedAutoscalingFields = true } return nil } func skipReplicaCountWithAutoscaleEnabled(iop *v1alpha1.IstioOperatorSpec, componentName name.ComponentName) bool { values := iop.GetValues().AsMap() path, ok := componentToAutoScaleEnabledPath[componentName] if !ok { return false } enabledVal, found, err := tpath.GetFromStructPath(values, path) if err != nil || !found { return false } enabled, ok := enabledVal.(bool) return ok && enabled } func (t *Translator) fixMergedObjectWithCustomServicePortOverlay(oo *object.K8sObject, msvc *v1alpha1.ServiceSpec, mergedObj *object.K8sObject) (*object.K8sObject, error) { var basePorts []*v1.ServicePort bps, _, err := unstructured.NestedSlice(oo.Unstructured(), "spec", "ports") if err != nil { return nil, err } bby, err := json.Marshal(bps) if err != nil { return nil, err } if err = json.Unmarshal(bby, &basePorts); err != nil { return nil, err } overlayPorts := make([]*v1.ServicePort, 0, len(msvc.GetPorts())) for _, p := range msvc.GetPorts() { var pr v1.Protocol switch strings.ToLower(p.GetProtocol()) { case "udp": pr = v1.ProtocolUDP default: pr = v1.ProtocolTCP } port := &v1.ServicePort{ Name: p.GetName(), Protocol: pr, Port: p.GetPort(), NodePort: p.GetNodePort(), } if p.TargetPort != nil { port.TargetPort = p.TargetPort.ToKubernetes() } overlayPorts = append(overlayPorts, port) } mergedPorts := strategicMergePorts(basePorts, overlayPorts) mpby, err := json.Marshal(mergedPorts) if err != nil { return nil, err } var mergedPortSlice []interface{} if err = json.Unmarshal(mpby, &mergedPortSlice); err != nil { return nil, err } if err = unstructured.SetNestedSlice(mergedObj.Unstructured(), mergedPortSlice, "spec", "ports"); err != nil { return nil, err } // Now fix the merged object mjsonby, err := json.Marshal(mergedObj.Unstructured()) if err != nil { return nil, err } if mergedObj, err = object.ParseJSONToK8sObject(mjsonby); err != nil { return nil, err } return mergedObj, nil } type portWithProtocol struct { port int32 protocol v1.Protocol } func portIndexOf(element portWithProtocol, data []portWithProtocol) int { for k, v := range data { if element == v { return k } } return len(data) } // strategicMergePorts merges the base with the given overlay considering both // port and the protocol as the merge keys. This is a workaround for the strategic // merge patch in Kubernetes which only uses port number as the key. This causes // an issue when we have to expose the same port with different protocols. // See - https://github.com/kubernetes/kubernetes/issues/103544 // TODO(su225): Remove this once the above issue is addressed in Kubernetes func strategicMergePorts(base, overlay []*v1.ServicePort) []*v1.ServicePort { // We want to keep the original port order with base first and then the newly // added ports through the overlay. This is because there are some cases where // port order actually matters. For instance, some cloud load balancers use the // first port for health-checking (in Istio it is 15021). So we must keep maintain // it in order not to break the users // See - https://github.com/istio/istio/issues/12503 for more information // // Or changing port order might generate weird diffs while upgrading or changing // IstioOperator spec. It is annoying. So better maintain original order while // appending newly added ports through overlay. portPriority := make([]portWithProtocol, 0, len(base)+len(overlay)) for _, p := range base { if p.Protocol == "" { p.Protocol = v1.ProtocolTCP } portPriority = append(portPriority, portWithProtocol{port: p.Port, protocol: p.Protocol}) } for _, p := range overlay { if p.Protocol == "" { p.Protocol = v1.ProtocolTCP } portPriority = append(portPriority, portWithProtocol{port: p.Port, protocol: p.Protocol}) } sortFn := func(ps []*v1.ServicePort) func(int, int) bool { return func(i, j int) bool { pi := portIndexOf(portWithProtocol{port: ps[i].Port, protocol: ps[i].Protocol}, portPriority) pj := portIndexOf(portWithProtocol{port: ps[j].Port, protocol: ps[j].Protocol}, portPriority) return pi < pj } } if overlay == nil { sort.Slice(base, sortFn(base)) return base } if base == nil { sort.Slice(overlay, sortFn(overlay)) return overlay } // first add the base and then replace appropriate // keys with the items in the overlay list merged := make(map[portWithProtocol]*v1.ServicePort) for _, p := range base { key := portWithProtocol{port: p.Port, protocol: p.Protocol} merged[key] = p } for _, p := range overlay { key := portWithProtocol{port: p.Port, protocol: p.Protocol} merged[key] = p } res := make([]*v1.ServicePort, 0, len(merged)) for _, pv := range merged { res = append(res, pv) } sort.Slice(res, sortFn(res)) return res } // ProtoToValues traverses the supplied IstioOperatorSpec and returns a values.yaml translation from it. func (t *Translator) ProtoToValues(ii *v1alpha1.IstioOperatorSpec) (string, error) { root, err := t.ProtoToHelmValues2(ii) if err != nil { return "", err } // Special additional handling not covered by simple translation rules. if err := t.setComponentProperties(root, ii); err != nil { return "", err } // Special handling of the settings of legacy fields in autoscaling/v2beta1 if err := t.translateDeprecatedAutoscalingFields(root, ii); err != nil { return "", err } // Return blank string for empty case. if len(root) == 0 { return "", nil } y, err := yaml.Marshal(root) if err != nil { return "", err } return string(y), nil } // TranslateHelmValues creates a Helm values.yaml config data tree from iop using the given translator. func (t *Translator) TranslateHelmValues(iop *v1alpha1.IstioOperatorSpec, componentsSpec interface{}, componentName name.ComponentName) (string, error) { apiVals := make(map[string]interface{}) // First, translate the IstioOperator API to helm Values. apiValsStr, err := t.ProtoToValues(iop) if err != nil { return "", err } err = yaml.Unmarshal([]byte(apiValsStr), &apiVals) if err != nil { return "", err } scope.Debugf("Values translated from IstioOperator API:\n%s", apiValsStr) // Add global overlay from IstioOperatorSpec.Values/UnvalidatedValues. globalVals := iop.GetValues().AsMap() globalUnvalidatedVals := iop.GetUnvalidatedValues().AsMap() if scope.DebugEnabled() { scope.Debugf("Values from IstioOperatorSpec.Values:\n%s", util.ToYAML(globalVals)) scope.Debugf("Values from IstioOperatorSpec.UnvalidatedValues:\n%s", util.ToYAML(globalUnvalidatedVals)) } mergedVals, err := util.OverlayTrees(apiVals, globalVals) if err != nil { return "", err } mergedVals, err = util.OverlayTrees(mergedVals, globalUnvalidatedVals) if err != nil { return "", err } mergedYAML, err := yaml.Marshal(mergedVals) if err != nil { return "", err } mergedYAML, err = applyGatewayTranslations(mergedYAML, componentName, componentsSpec) if err != nil { return "", err } return string(mergedYAML), err } // applyGatewayTranslations writes gateway name gwName at the appropriate values path in iop and maps k8s.service.ports // to values. It returns the resulting YAML tree. func applyGatewayTranslations(iop []byte, componentName name.ComponentName, componentSpec interface{}) ([]byte, error) { if !componentName.IsGateway() { return iop, nil } iopt := make(map[string]interface{}) if err := yaml.Unmarshal(iop, &iopt); err != nil { return nil, err } gwSpec := componentSpec.(*v1alpha1.GatewaySpec) k8s := gwSpec.K8S switch componentName { case name.IngressComponentName: setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-ingressgateway.name"), gwSpec.Name) if len(gwSpec.Label) != 0 { setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-ingressgateway.labels"), gwSpec.Label) } if k8s != nil && k8s.Service != nil && k8s.Service.Ports != nil { setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-ingressgateway.ports"), k8s.Service.Ports) } case name.EgressComponentName: setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-egressgateway.name"), gwSpec.Name) if len(gwSpec.Label) != 0 { setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-egressgateway.labels"), gwSpec.Label) } if k8s != nil && k8s.Service != nil && k8s.Service.Ports != nil { setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-egressgateway.ports"), k8s.Service.Ports) } } return yaml.Marshal(iopt) } // setYAMLNodeByMapPath sets the value at the given path to val in treeNode. The path cannot traverse lists and // treeNode must be a YAML tree unmarshaled into a plain map data structure. func setYAMLNodeByMapPath(treeNode interface{}, path util.Path, val interface{}) { if len(path) == 0 || treeNode == nil { return } pe := path[0] switch nt := treeNode.(type) { case map[interface{}]interface{}: if len(path) == 1 { nt[pe] = val return } if nt[pe] == nil { return } setYAMLNodeByMapPath(nt[pe], path[1:], val) case map[string]interface{}: if len(path) == 1 { nt[pe] = val return } if nt[pe] == nil { return } setYAMLNodeByMapPath(nt[pe], path[1:], val) } } // ComponentMap returns a ComponentMaps struct ptr for the given component name if one exists. // If the name of the component is lower case, the function will use the capitalized version // of the name. func (t *Translator) ComponentMap(cns string) *ComponentMaps { cn := name.TitleCase(name.ComponentName(cns)) return t.ComponentMaps[cn] } func (t *Translator) ProtoToHelmValues2(ii *v1alpha1.IstioOperatorSpec) (map[string]interface{}, error) { by, err := json.Marshal(ii) if err != nil { return nil, err } res := map[string]interface{}{} err = json.Unmarshal(by, &res) if err != nil { return nil, err } r2 := map[string]interface{}{} errs := t.ProtoToHelmValues(res, r2, nil) return r2, errs.ToError() } // ProtoToHelmValues function below is used by third party for integrations and has to be public // ProtoToHelmValues takes an interface which must be a struct ptr and recursively iterates through all its fields. // For each leaf, if looks for a mapping from the struct data path to the corresponding YAML path and if one is // found, it calls the associated mapping function if one is defined to populate the values YAML path. // If no mapping function is defined, it uses the default mapping function. func (t *Translator) ProtoToHelmValues(node interface{}, root map[string]interface{}, path util.Path) (errs util.Errors) { scope.Debugf("ProtoToHelmValues with path %s, %v (%T)", path, node, node) if util.IsValueNil(node) { return nil } vv := reflect.ValueOf(node) vt := reflect.TypeOf(node) switch vt.Kind() { case reflect.Ptr: if !util.IsNilOrInvalidValue(vv.Elem()) { errs = util.AppendErrs(errs, t.ProtoToHelmValues(vv.Elem().Interface(), root, path)) } case reflect.Struct: scope.Debug("Struct") for i := 0; i < vv.NumField(); i++ { fieldName := vv.Type().Field(i).Name fieldValue := vv.Field(i) scope.Debugf("Checking field %s", fieldName) if a, ok := vv.Type().Field(i).Tag.Lookup("json"); ok && a == "-" { continue } if !fieldValue.CanInterface() { continue } errs = util.AppendErrs(errs, t.ProtoToHelmValues(fieldValue.Interface(), root, append(path, fieldName))) } case reflect.Map: scope.Debug("Map") for _, key := range vv.MapKeys() { nnp := append(path, key.String()) errs = util.AppendErrs(errs, t.insertLeaf(root, nnp, vv.MapIndex(key))) } case reflect.Slice: scope.Debug("Slice") for i := 0; i < vv.Len(); i++ { errs = util.AppendErrs(errs, t.ProtoToHelmValues(vv.Index(i).Interface(), root, path)) } default: // Must be a leaf scope.Debugf("field has kind %s", vt.Kind()) if vv.CanInterface() { errs = util.AppendErrs(errs, t.insertLeaf(root, path, vv)) } } return errs } // setComponentProperties translates properties (e.g., enablement and namespace) of each component // in the baseYAML values tree, based on feature/component inheritance relationship. func (t *Translator) setComponentProperties(root map[string]interface{}, iop *v1alpha1.IstioOperatorSpec) error { var keys []string for k := range t.ComponentMaps { if k != name.IngressComponentName && k != name.EgressComponentName { keys = append(keys, string(k)) } } sort.Strings(keys) l := len(keys) for i := l - 1; i >= 0; i-- { cn := name.ComponentName(keys[i]) c := t.ComponentMaps[cn] e, err := t.IsComponentEnabled(cn, iop) if err != nil { return err } enablementPath := c.ToHelmValuesTreeRoot // CNI calls itself "cni" in the chart but "istio_cni" for enablement outside of the chart. if cn == name.CNIComponentName { enablementPath = "istio_cni" } if err := tpath.WriteNode(root, util.PathFromString(enablementPath+"."+HelmValuesEnabledSubpath), e); err != nil { return err } ns, err := name.Namespace(cn, iop) if err != nil { return err } if err := tpath.WriteNode(root, util.PathFromString(c.ToHelmValuesTreeRoot+"."+HelmValuesNamespaceSubpath), ns); err != nil { return err } hub, found, _ := tpath.GetFromStructPath(iop, "Components."+string(cn)+".Hub") // Unmarshal unfortunately creates struct fields with "" for unset values. Skip these cases to avoid // overwriting current value with an empty string. hubStr, ok := hub.(string) if found && !(ok && hubStr == "") { if err := tpath.WriteNode(root, util.PathFromString(c.ToHelmValuesTreeRoot+"."+HelmValuesHubSubpath), hub); err != nil { return err } } tag, found, _ := tpath.GetFromStructPath(iop, "Components."+string(cn)+".Tag") tagv, ok := tag.(*structpb.Value) if found && !(ok && util.ValueString(tagv) == "") { if err := tpath.WriteNode(root, util.PathFromString(c.ToHelmValuesTreeRoot+"."+HelmValuesTagSubpath), util.ValueString(tagv)); err != nil { return err } } } for cn, gns := range t.GlobalNamespaces { ns, err := name.Namespace(cn, iop) if err != nil { return err } if err := tpath.WriteNode(root, util.PathFromString("global."+gns), ns); err != nil { return err } } return nil } // IsComponentEnabled reports whether the component with name cn is enabled, according to the translations in t, // and the contents of ocp. func (t *Translator) IsComponentEnabled(cn name.ComponentName, iop *v1alpha1.IstioOperatorSpec) (bool, error) { if t.ComponentMaps[cn] == nil { return false, nil } return IsComponentEnabledInSpec(cn, iop) } // insertLeaf inserts a leaf with value into root at path, which is first mapped using t.APIMapping. func (t *Translator) insertLeaf(root map[string]interface{}, path util.Path, value reflect.Value) (errs util.Errors) { // Must be a scalar leaf. See if we have a mapping. valuesPath, m := getValuesPathMapping(t.APIMapping, path) var v interface{} if value.Kind() == reflect.Ptr { v = value.Elem().Interface() } else { v = value.Interface() } switch { case m == nil: break case m.translationFunc == nil: // Use default translation which just maps to a different part of the tree. errs = util.AppendErr(errs, defaultTranslationFunc(m, root, valuesPath, v)) default: // Use a custom translation function. errs = util.AppendErr(errs, m.translationFunc(m, root, valuesPath, v)) } return errs } // getValuesPathMapping tries to map path against the passed in mappings with a longest prefix match. If a matching prefix // is found, it returns the translated YAML path and the corresponding translation. // e.g. for mapping "a.b" -> "1.2", the input path "a.b.c.d" would yield "1.2.c.d". func getValuesPathMapping(mappings map[string]*Translation, path util.Path) (string, *Translation) { p := path var m *Translation for ; len(p) > 0; p = p[0 : len(p)-1] { m = mappings[p.String()] if m != nil { break } } if m == nil { return "", nil } if m.OutPath == "" { return "", m } out := m.OutPath + "." + path[len(p):].String() scope.Debugf("translating %s to %s", path, out) return out, m } // renderFeatureComponentPathTemplate renders a template of the form <path>{{.ComponentName}}<path> with // the supplied parameters. func renderFeatureComponentPathTemplate(tmpl string, componentName name.ComponentName) (string, error) { type Temp struct { ComponentName name.ComponentName } ts := Temp{ ComponentName: componentName, } return util.RenderTemplate(tmpl, ts) } // renderResourceComponentPathTemplate renders a template of the form <path>{{.ResourceName}}<path>{{.ContainerName}}<path> with // the supplied parameters. func (t *Translator) renderResourceComponentPathTemplate(tmpl string, componentName name.ComponentName, resourceName, revision string) (string, error) { cn := string(componentName) cmp := t.ComponentMap(cn) if cmp == nil { return "", fmt.Errorf("component: %s does not exist in the componentMap", cn) } if resourceName == "" { resourceName = cmp.ResourceName } // The istiod resource will be istiod-<REVISION>, so we need to append the revision suffix if revision != "" && resourceName == "istiod" { resourceName += "-" + revision } ts := struct { ResourceType string ResourceName string ContainerName string }{ ResourceType: cmp.ResourceType, ResourceName: resourceName, ContainerName: cmp.ContainerName, } return util.RenderTemplate(tmpl, ts) } // defaultTranslationFunc is the default translation to values. It maps a Go data path into a YAML path. func defaultTranslationFunc(m *Translation, root map[string]interface{}, valuesPath string, value interface{}) error { var path []string if util.IsEmptyString(value) { scope.Debugf("Skip empty string value for path %s", m.OutPath) return nil } if valuesPath == "" { scope.Debugf("Not mapping to values, resources path is %s", m.OutPath) return nil } for _, p := range util.PathFromString(valuesPath) { path = append(path, firstCharToLower(p)) } return tpath.WriteNode(root, path, value) } func firstCharToLower(s string) string { return strings.ToLower(s[0:1]) + s[1:] } // MergeK8sObject function below is used by third party for integrations and has to be public // MergeK8sObject does strategic merge for overlayNode on the base object. func MergeK8sObject(base *object.K8sObject, overlayNode interface{}, path util.Path) (*object.K8sObject, error) { overlay, err := createPatchObjectFromPath(overlayNode, path) if err != nil { return nil, err } overlayYAML, err := yaml.Marshal(overlay) if err != nil { return nil, err } overlayJSON, err := yaml.YAMLToJSON(overlayYAML) if err != nil { return nil, fmt.Errorf("yamlToJSON error in overlayYAML: %s\n%s", err, overlayYAML) } baseJSON, err := base.JSON() if err != nil { return nil, err } // get a versioned object from the scheme, we can use the strategic patching mechanism // (i.e. take advantage of patchStrategy in the type) versionedObject, err := scheme.Scheme.New(base.GroupVersionKind()) if err != nil { return nil, err } // strategic merge patch newBytes, err := strategicpatch.StrategicMergePatch(baseJSON, overlayJSON, versionedObject) if err != nil { return nil, fmt.Errorf("get error: %s to merge patch:\n%s for base:\n%s", err, overlayJSON, baseJSON) } newObj, err := object.ParseJSONToK8sObject(newBytes) if err != nil { return nil, err } return newObj.ResolveK8sConflict(), nil } // createPatchObjectFromPath constructs patch object for node with path, returns nil object and error if the path is invalid. // eg. node: // - name: NEW_VAR // value: new_value // // and path: // // spec.template.spec.containers.[name:discovery].env // will constructs the following patch object: // spec: // template: // spec: // containers: // - name: discovery // env: // - name: NEW_VAR // value: new_value func createPatchObjectFromPath(node interface{}, path util.Path) (map[string]interface{}, error) { if len(path) == 0 { return nil, fmt.Errorf("empty path %s", path) } if util.IsKVPathElement(path[0]) { return nil, fmt.Errorf("path %s has an unexpected first element %s", path, path[0]) } length := len(path) if util.IsKVPathElement(path[length-1]) { return nil, fmt.Errorf("path %s has an unexpected last element %s", path, path[length-1]) } patchObj := make(map[string]interface{}) var currentNode, nextNode interface{} nextNode = patchObj for i, pe := range path { currentNode = nextNode // last path element if i == length-1 { currentNode, ok := currentNode.(map[string]interface{}) if !ok { return nil, fmt.Errorf("path %s has an unexpected non KV element %s", path, pe) } currentNode[pe] = node break } if util.IsKVPathElement(pe) { currentNode, ok := currentNode.([]interface{}) if !ok { return nil, fmt.Errorf("path %s has an unexpected KV element %s", path, pe) } k, v, err := util.PathKV(pe) if err != nil { return nil, err } if k == "" || v == "" { return nil, fmt.Errorf("path %s has an invalid KV element %s", path, pe) } currentNode[0] = map[string]interface{}{k: v} nextNode = currentNode[0] continue } currentNode, ok := currentNode.(map[string]interface{}) if !ok { return nil, fmt.Errorf("path %s has an unexpected non KV element %s", path, pe) } // next path element determines the next node type if util.IsKVPathElement(path[i+1]) { currentNode[pe] = make([]interface{}, 1) } else { currentNode[pe] = make(map[string]interface{}) } nextNode = currentNode[pe] } return patchObj, nil } // IOPStoIOP takes an IstioOperatorSpec and returns a corresponding IstioOperator with the given name and namespace. func IOPStoIOP(iops proto.Message, name, namespace string) (*iopv1alpha1.IstioOperator, error) { iopStr, err := IOPStoIOPstr(iops, name, namespace) if err != nil { return nil, err } iop, err := istio.UnmarshalIstioOperator(iopStr, false) if err != nil { return nil, err } return iop, nil } // IOPStoIOPstr takes an IstioOperatorSpec and returns a corresponding IstioOperator string with the given name and namespace. func IOPStoIOPstr(iops proto.Message, name, namespace string) (string, error) { iopsStr, err := util.MarshalWithJSONPB(iops) if err != nil { return "", err } spec, err := tpath.AddSpecRoot(iopsStr) if err != nil { return "", err } tmpl := ` apiVersion: install.istio.io/v1alpha1 kind: IstioOperator metadata: namespace: {{ .Namespace }} name: {{ .Name }} ` // Passing into template causes reformatting, use simple concatenation instead. tmpl += spec type Temp struct { Namespace string Name string } ts := Temp{ Namespace: namespace, Name: name, } return util.RenderTemplate(tmpl, ts) }