| // 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) |
| } |