istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/translate/translate.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package translate defines translations from installer proto to values.yaml.
    16  package translate
    17  
    18  import (
    19  	"encoding/json"
    20  	"fmt"
    21  	"reflect"
    22  	"sort"
    23  	"strings"
    24  
    25  	"google.golang.org/protobuf/proto"
    26  	"google.golang.org/protobuf/types/known/structpb"
    27  	v1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    30  	"k8s.io/client-go/kubernetes/scheme"
    31  	"sigs.k8s.io/yaml"
    32  
    33  	"istio.io/api/operator/v1alpha1"
    34  	"istio.io/istio/operator/pkg/apis/istio"
    35  	iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
    36  	"istio.io/istio/operator/pkg/name"
    37  	"istio.io/istio/operator/pkg/object"
    38  	"istio.io/istio/operator/pkg/tpath"
    39  	"istio.io/istio/operator/pkg/util"
    40  	"istio.io/istio/operator/pkg/version"
    41  	oversion "istio.io/istio/operator/version"
    42  	"istio.io/istio/pkg/log"
    43  	"istio.io/istio/pkg/util/sets"
    44  )
    45  
    46  const (
    47  	// HelmValuesEnabledSubpath is the subpath from the component root to the enabled parameter.
    48  	HelmValuesEnabledSubpath = "enabled"
    49  	// HelmValuesNamespaceSubpath is the subpath from the component root to the namespace parameter.
    50  	HelmValuesNamespaceSubpath = "namespace"
    51  	// HelmValuesHubSubpath is the subpath from the component root to the hub parameter.
    52  	HelmValuesHubSubpath = "hub"
    53  	// HelmValuesTagSubpath is the subpath from the component root to the tag parameter.
    54  	HelmValuesTagSubpath = "tag"
    55  )
    56  
    57  var scope = log.RegisterScope("translator", "API translator")
    58  
    59  // Translator is a set of mappings to translate between API paths, charts, values.yaml and k8s paths.
    60  type Translator struct {
    61  	// Translations remain the same within a minor version.
    62  	Version version.MinorVersion
    63  	// APIMapping is a mapping between an API path and the corresponding values.yaml path using longest prefix
    64  	// match. If the path is a non-leaf node, the output path is the matching portion of the path, plus any remaining
    65  	// output path.
    66  	APIMapping map[string]*Translation `yaml:"apiMapping"`
    67  	// KubernetesMapping defines mappings from an IstioOperator API paths to k8s resource paths.
    68  	KubernetesMapping map[string]*Translation `yaml:"kubernetesMapping"`
    69  	// GlobalNamespaces maps feature namespaces to Helm global namespace definitions.
    70  	GlobalNamespaces map[name.ComponentName]string `yaml:"globalNamespaces"`
    71  	// ComponentMaps is a set of mappings for each Istio component.
    72  	ComponentMaps map[name.ComponentName]*ComponentMaps `yaml:"componentMaps"`
    73  }
    74  
    75  // ComponentMaps is a set of mappings for an Istio component.
    76  type ComponentMaps struct {
    77  	// ResourceType maps a ComponentName to the type of the rendered k8s resource.
    78  	ResourceType string
    79  	// ResourceName maps a ComponentName to the name of the rendered k8s resource.
    80  	ResourceName string
    81  	// ContainerName maps a ComponentName to the name of the container in a Deployment.
    82  	ContainerName string
    83  	// HelmSubdir is a mapping between a component name and the subdirectory of the component Chart.
    84  	HelmSubdir string
    85  	// ToHelmValuesTreeRoot is the tree root in values YAML files for the component.
    86  	ToHelmValuesTreeRoot string
    87  	// SkipReverseTranslate defines whether reverse translate of this component need to be skipped.
    88  	SkipReverseTranslate bool
    89  	// FlattenValues, if true, means the component expects values not prefixed with ToHelmValuesTreeRoot
    90  	// For example `.name=foo` instead of `.component.name=foo`.
    91  	FlattenValues bool
    92  }
    93  
    94  // TranslationFunc maps a yamlStr API path into a YAML values tree.
    95  type TranslationFunc func(t *Translation, root map[string]any, valuesPath string, value any) error
    96  
    97  // Translation is a mapping to an output path using a translation function.
    98  type Translation struct {
    99  	// OutPath defines the position in the yaml file
   100  	OutPath         string `yaml:"outPath"`
   101  	translationFunc TranslationFunc
   102  }
   103  
   104  // NewTranslator creates a new translator for minorVersion and returns a ptr to it.
   105  func NewTranslator() *Translator {
   106  	t := &Translator{
   107  		Version: oversion.OperatorBinaryVersion.MinorVersion,
   108  		APIMapping: map[string]*Translation{
   109  			"hub":                  {OutPath: "global.hub"},
   110  			"tag":                  {OutPath: "global.tag"},
   111  			"revision":             {OutPath: "revision"},
   112  			"meshConfig":           {OutPath: "meshConfig"},
   113  			"compatibilityVersion": {OutPath: "compatibilityVersion"},
   114  		},
   115  		GlobalNamespaces: map[name.ComponentName]string{
   116  			name.PilotComponentName: "istioNamespace",
   117  		},
   118  		ComponentMaps: map[name.ComponentName]*ComponentMaps{
   119  			name.IstioBaseComponentName: {
   120  				HelmSubdir:           "base",
   121  				ToHelmValuesTreeRoot: "global",
   122  				SkipReverseTranslate: true,
   123  			},
   124  			name.PilotComponentName: {
   125  				ResourceType:         "Deployment",
   126  				ResourceName:         "istiod",
   127  				ContainerName:        "discovery",
   128  				HelmSubdir:           "istio-control/istio-discovery",
   129  				ToHelmValuesTreeRoot: "pilot",
   130  			},
   131  			name.IngressComponentName: {
   132  				ResourceType:         "Deployment",
   133  				ResourceName:         "istio-ingressgateway",
   134  				ContainerName:        "istio-proxy",
   135  				HelmSubdir:           "gateways/istio-ingress",
   136  				ToHelmValuesTreeRoot: "gateways.istio-ingressgateway",
   137  			},
   138  			name.EgressComponentName: {
   139  				ResourceType:         "Deployment",
   140  				ResourceName:         "istio-egressgateway",
   141  				ContainerName:        "istio-proxy",
   142  				HelmSubdir:           "gateways/istio-egress",
   143  				ToHelmValuesTreeRoot: "gateways.istio-egressgateway",
   144  			},
   145  			name.CNIComponentName: {
   146  				ResourceType:         "DaemonSet",
   147  				ResourceName:         "istio-cni-node",
   148  				ContainerName:        "install-cni",
   149  				HelmSubdir:           "istio-cni",
   150  				ToHelmValuesTreeRoot: "cni",
   151  			},
   152  			name.IstiodRemoteComponentName: {
   153  				HelmSubdir:           "istiod-remote",
   154  				ToHelmValuesTreeRoot: "global",
   155  				SkipReverseTranslate: true,
   156  			},
   157  			name.ZtunnelComponentName: {
   158  				ResourceType:         "DaemonSet",
   159  				ResourceName:         "ztunnel",
   160  				HelmSubdir:           "ztunnel",
   161  				ToHelmValuesTreeRoot: "ztunnel",
   162  				ContainerName:        "istio-proxy",
   163  				FlattenValues:        true,
   164  			},
   165  		},
   166  		// nolint: lll
   167  		KubernetesMapping: map[string]*Translation{
   168  			"Components.{{.ComponentName}}.K8S.Affinity":            {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.affinity"},
   169  			"Components.{{.ComponentName}}.K8S.Env":                 {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.containers.[name:{{.ContainerName}}].env"},
   170  			"Components.{{.ComponentName}}.K8S.HpaSpec":             {OutPath: "[HorizontalPodAutoscaler:{{.ResourceName}}].spec"},
   171  			"Components.{{.ComponentName}}.K8S.ImagePullPolicy":     {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.containers.[name:{{.ContainerName}}].imagePullPolicy"},
   172  			"Components.{{.ComponentName}}.K8S.NodeSelector":        {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.nodeSelector"},
   173  			"Components.{{.ComponentName}}.K8S.PodDisruptionBudget": {OutPath: "[PodDisruptionBudget:{{.ResourceName}}].spec"},
   174  			"Components.{{.ComponentName}}.K8S.PodAnnotations":      {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.metadata.annotations"},
   175  			"Components.{{.ComponentName}}.K8S.PriorityClassName":   {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.priorityClassName."},
   176  			"Components.{{.ComponentName}}.K8S.ReadinessProbe":      {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.containers.[name:{{.ContainerName}}].readinessProbe"},
   177  			"Components.{{.ComponentName}}.K8S.ReplicaCount":        {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.replicas"},
   178  			"Components.{{.ComponentName}}.K8S.Resources":           {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.containers.[name:{{.ContainerName}}].resources"},
   179  			"Components.{{.ComponentName}}.K8S.Strategy":            {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.strategy"},
   180  			"Components.{{.ComponentName}}.K8S.Tolerations":         {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.tolerations"},
   181  			"Components.{{.ComponentName}}.K8S.ServiceAnnotations":  {OutPath: "[Service:{{.ResourceName}}].metadata.annotations"},
   182  			"Components.{{.ComponentName}}.K8S.Service":             {OutPath: "[Service:{{.ResourceName}}].spec"},
   183  			"Components.{{.ComponentName}}.K8S.SecurityContext":     {OutPath: "[{{.ResourceType}}:{{.ResourceName}}].spec.template.spec.securityContext"},
   184  		},
   185  	}
   186  	return t
   187  }
   188  
   189  // OverlayK8sSettings overlays k8s settings from iop over the manifest objects, based on t's translation mappings.
   190  func (t *Translator) OverlayK8sSettings(yml string, iop *v1alpha1.IstioOperatorSpec, componentName name.ComponentName,
   191  	resourceName string, index int) (string, error,
   192  ) {
   193  	// om is a map of kind:name string to Object ptr.
   194  	// This is lazy loaded to avoid parsing when there are no overlays
   195  	var om map[string]*object.K8sObject
   196  	var objects object.K8sObjects
   197  
   198  	for inPath, v := range t.KubernetesMapping {
   199  		inPath, err := renderFeatureComponentPathTemplate(inPath, componentName)
   200  		if err != nil {
   201  			return "", err
   202  		}
   203  		renderedInPath := strings.Replace(inPath, "gressGateways.", "gressGateways."+fmt.Sprint(index)+".", 1)
   204  		scope.Debugf("Checking for path %s in IstioOperatorSpec", renderedInPath)
   205  
   206  		m, found, err := tpath.GetFromStructPath(iop, renderedInPath)
   207  		if err != nil {
   208  			return "", err
   209  		}
   210  		if !found {
   211  			scope.Debugf("path %s not found in IstioOperatorSpec, skip mapping.", renderedInPath)
   212  			continue
   213  		}
   214  		if mstr, ok := m.(string); ok && mstr == "" {
   215  			scope.Debugf("path %s is empty string, skip mapping.", renderedInPath)
   216  			continue
   217  		}
   218  		// Zero int values are due to proto3 compiling to scalars rather than ptrs. Skip these because values of 0 are
   219  		// the default in destination fields and need not be set explicitly.
   220  		if mint, ok := util.ToIntValue(m); ok && mint == 0 {
   221  			scope.Debugf("path %s is int 0, skip mapping.", renderedInPath)
   222  			continue
   223  		}
   224  		if componentName == name.IstioBaseComponentName {
   225  			return "", fmt.Errorf("base component can only have k8s.overlays, not other K8s settings")
   226  		}
   227  		inPathParts := strings.Split(inPath, ".")
   228  		outPath, err := t.renderResourceComponentPathTemplate(v.OutPath, componentName, resourceName, iop.Revision)
   229  		if err != nil {
   230  			return "", err
   231  		}
   232  		scope.Debugf("path has value in IstioOperatorSpec, mapping to output path %s", outPath)
   233  		path := util.PathFromString(outPath)
   234  		pe := path[0]
   235  		// Output path must start with [kind:name], which is used to map to the object to overlay.
   236  		if !util.IsKVPathElement(pe) {
   237  			return "", fmt.Errorf("path %s has an unexpected first element %s in OverlayK8sSettings", path, pe)
   238  		}
   239  
   240  		// We need to apply overlay, lazy load om
   241  		if om == nil {
   242  			objects, err = object.ParseK8sObjectsFromYAMLManifest(yml)
   243  			if err != nil {
   244  				return "", err
   245  			}
   246  			if scope.DebugEnabled() {
   247  				scope.Debugf("Manifest contains the following objects:")
   248  				for _, o := range objects {
   249  					scope.Debugf("%s", o.HashNameKind())
   250  				}
   251  			}
   252  			om = objects.ToNameKindMap()
   253  		}
   254  
   255  		// After brackets are removed, the remaining "kind:name" is the same format as the keys in om.
   256  		pe, _ = util.RemoveBrackets(pe)
   257  		oo, ok := om[pe]
   258  		if !ok {
   259  			// skip to overlay the K8s settings if the corresponding resource doesn't exist.
   260  			scope.Infof("resource Kind:name %s doesn't exist in the output manifest, skip overlay.", pe)
   261  			continue
   262  		}
   263  
   264  		// When autoscale is enabled we should not overwrite replica count, consider following scenario:
   265  		// 0. Set values.pilot.autoscaleEnabled=true, components.pilot.k8s.replicaCount=1
   266  		// 1. In istio operator it "caches" the generated manifests (with istiod.replicas=1)
   267  		// 2. HPA autoscales our pilot replicas to 3
   268  		// 3. Set values.pilot.autoscaleEnabled=false
   269  		// 4. The generated manifests (with istiod.replicas=1) is same as istio operator "cache",
   270  		//    the deployment will not get updated unless istio operator is restarted.
   271  		if inPathParts[len(inPathParts)-1] == "ReplicaCount" {
   272  			if skipReplicaCountWithAutoscaleEnabled(iop, componentName) {
   273  				continue
   274  			}
   275  		}
   276  
   277  		// strategic merge overlay m to the base object oo
   278  		mergedObj, err := MergeK8sObject(oo, m, path[1:])
   279  		if err != nil {
   280  			return "", err
   281  		}
   282  
   283  		// Apply the workaround for merging service ports with (port,protocol) composite
   284  		// keys instead of just the merging by port.
   285  		if inPathParts[len(inPathParts)-1] == "Service" {
   286  			if msvc, ok := m.(*v1alpha1.ServiceSpec); ok {
   287  				mergedObj, err = t.fixMergedObjectWithCustomServicePortOverlay(oo, msvc, mergedObj)
   288  				if err != nil {
   289  					return "", err
   290  				}
   291  			}
   292  		}
   293  
   294  		// Update the original object in objects slice, since the output should be ordered.
   295  		*(om[pe]) = *mergedObj
   296  	}
   297  
   298  	if objects != nil {
   299  		return objects.YAMLManifest()
   300  	}
   301  	return yml, nil
   302  }
   303  
   304  var componentToAutoScaleEnabledPath = map[name.ComponentName]string{
   305  	name.PilotComponentName:   "pilot.autoscaleEnabled",
   306  	name.IngressComponentName: "gateways.istio-ingressgateway.autoscaleEnabled",
   307  	name.EgressComponentName:  "gateways.istio-egressgateway.autoscaleEnabled",
   308  }
   309  
   310  func skipReplicaCountWithAutoscaleEnabled(iop *v1alpha1.IstioOperatorSpec, componentName name.ComponentName) bool {
   311  	values := iop.GetValues().AsMap()
   312  	path, ok := componentToAutoScaleEnabledPath[componentName]
   313  	if !ok {
   314  		return false
   315  	}
   316  
   317  	enabledVal, found, err := tpath.GetFromStructPath(values, path)
   318  	if err != nil || !found {
   319  		return false
   320  	}
   321  
   322  	enabled, ok := enabledVal.(bool)
   323  	return ok && enabled
   324  }
   325  
   326  func (t *Translator) fixMergedObjectWithCustomServicePortOverlay(oo *object.K8sObject,
   327  	msvc *v1alpha1.ServiceSpec, mergedObj *object.K8sObject,
   328  ) (*object.K8sObject, error) {
   329  	var basePorts []*v1.ServicePort
   330  	bps, _, err := unstructured.NestedSlice(oo.Unstructured(), "spec", "ports")
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	bby, err := json.Marshal(bps)
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  	if err = json.Unmarshal(bby, &basePorts); err != nil {
   339  		return nil, err
   340  	}
   341  	overlayPorts := make([]*v1.ServicePort, 0, len(msvc.GetPorts()))
   342  	for _, p := range msvc.GetPorts() {
   343  		var pr v1.Protocol
   344  		switch strings.ToLower(p.GetProtocol()) {
   345  		case "udp":
   346  			pr = v1.ProtocolUDP
   347  		default:
   348  			pr = v1.ProtocolTCP
   349  		}
   350  		port := &v1.ServicePort{
   351  			Name:     p.GetName(),
   352  			Protocol: pr,
   353  			Port:     p.GetPort(),
   354  			NodePort: p.GetNodePort(),
   355  		}
   356  		if p.GetAppProtocol() != "" {
   357  			ap := p.AppProtocol
   358  			port.AppProtocol = &ap
   359  		}
   360  		if p.TargetPort != nil {
   361  			port.TargetPort = p.TargetPort.ToKubernetes()
   362  		}
   363  		overlayPorts = append(overlayPorts, port)
   364  	}
   365  	mergedPorts := strategicMergePorts(basePorts, overlayPorts)
   366  	mpby, err := json.Marshal(mergedPorts)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	var mergedPortSlice []any
   371  	if err = json.Unmarshal(mpby, &mergedPortSlice); err != nil {
   372  		return nil, err
   373  	}
   374  	if err = unstructured.SetNestedSlice(mergedObj.Unstructured(), mergedPortSlice, "spec", "ports"); err != nil {
   375  		return nil, err
   376  	}
   377  	// Now fix the merged object
   378  	mjsonby, err := json.Marshal(mergedObj.Unstructured())
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  	if mergedObj, err = object.ParseJSONToK8sObject(mjsonby); err != nil {
   383  		return nil, err
   384  	}
   385  	return mergedObj, nil
   386  }
   387  
   388  type portWithProtocol struct {
   389  	port     int32
   390  	protocol v1.Protocol
   391  }
   392  
   393  func portIndexOf(element portWithProtocol, data []portWithProtocol) int {
   394  	for k, v := range data {
   395  		if element == v {
   396  			return k
   397  		}
   398  	}
   399  	return len(data)
   400  }
   401  
   402  // strategicMergePorts merges the base with the given overlay considering both
   403  // port and the protocol as the merge keys. This is a workaround for the strategic
   404  // merge patch in Kubernetes which only uses port number as the key. This causes
   405  // an issue when we have to expose the same port with different protocols.
   406  // See - https://github.com/kubernetes/kubernetes/issues/103544
   407  // TODO(su225): Remove this once the above issue is addressed in Kubernetes
   408  func strategicMergePorts(base, overlay []*v1.ServicePort) []*v1.ServicePort {
   409  	// We want to keep the original port order with base first and then the newly
   410  	// added ports through the overlay. This is because there are some cases where
   411  	// port order actually matters. For instance, some cloud load balancers use the
   412  	// first port for health-checking (in Istio it is 15021). So we must keep maintain
   413  	// it in order not to break the users
   414  	// See - https://github.com/istio/istio/issues/12503 for more information
   415  	//
   416  	// Or changing port order might generate weird diffs while upgrading or changing
   417  	// IstioOperator spec. It is annoying. So better maintain original order while
   418  	// appending newly added ports through overlay.
   419  	portPriority := make([]portWithProtocol, 0, len(base)+len(overlay))
   420  	for _, p := range base {
   421  		if p.Protocol == "" {
   422  			p.Protocol = v1.ProtocolTCP
   423  		}
   424  		portPriority = append(portPriority, portWithProtocol{port: p.Port, protocol: p.Protocol})
   425  	}
   426  	for _, p := range overlay {
   427  		if p.Protocol == "" {
   428  			p.Protocol = v1.ProtocolTCP
   429  		}
   430  		portPriority = append(portPriority, portWithProtocol{port: p.Port, protocol: p.Protocol})
   431  	}
   432  	sortFn := func(ps []*v1.ServicePort) func(int, int) bool {
   433  		return func(i, j int) bool {
   434  			pi := portIndexOf(portWithProtocol{port: ps[i].Port, protocol: ps[i].Protocol}, portPriority)
   435  			pj := portIndexOf(portWithProtocol{port: ps[j].Port, protocol: ps[j].Protocol}, portPriority)
   436  			return pi < pj
   437  		}
   438  	}
   439  	if overlay == nil {
   440  		sort.Slice(base, sortFn(base))
   441  		return base
   442  	}
   443  	if base == nil {
   444  		sort.Slice(overlay, sortFn(overlay))
   445  		return overlay
   446  	}
   447  	// first add the base and then replace appropriate
   448  	// keys with the items in the overlay list
   449  	merged := make(map[portWithProtocol]*v1.ServicePort)
   450  	for _, p := range base {
   451  		key := portWithProtocol{port: p.Port, protocol: p.Protocol}
   452  		merged[key] = p
   453  	}
   454  	for _, p := range overlay {
   455  		key := portWithProtocol{port: p.Port, protocol: p.Protocol}
   456  		merged[key] = p
   457  	}
   458  	res := make([]*v1.ServicePort, 0, len(merged))
   459  	for _, pv := range merged {
   460  		res = append(res, pv)
   461  	}
   462  	sort.Slice(res, sortFn(res))
   463  	return res
   464  }
   465  
   466  // ProtoToValues traverses the supplied IstioOperatorSpec and returns a values.yaml translation from it.
   467  func (t *Translator) ProtoToValues(ii *v1alpha1.IstioOperatorSpec) (string, error) {
   468  	root, err := t.ProtoToHelmValues2(ii)
   469  	if err != nil {
   470  		return "", err
   471  	}
   472  
   473  	// Special additional handling not covered by simple translation rules.
   474  	if err := t.setComponentProperties(root, ii); err != nil {
   475  		return "", err
   476  	}
   477  
   478  	// Return blank string for empty case.
   479  	if len(root) == 0 {
   480  		return "", nil
   481  	}
   482  
   483  	y, err := yaml.Marshal(root)
   484  	if err != nil {
   485  		return "", err
   486  	}
   487  
   488  	return string(y), nil
   489  }
   490  
   491  // Fields, beyond 'global', that apply to each chart at the top level of values.yaml
   492  var topLevelFields = sets.New(
   493  	"ownerName",
   494  	"revision",
   495  	"compatibilityVersion",
   496  	"profile",
   497  )
   498  
   499  // TranslateHelmValues creates a Helm values.yaml config data tree from iop using the given translator.
   500  func (t *Translator) TranslateHelmValues(iop *v1alpha1.IstioOperatorSpec, componentsSpec any, componentName name.ComponentName) (string, error) {
   501  	apiVals := make(map[string]any)
   502  
   503  	// First, translate the IstioOperator API to helm Values.
   504  	apiValsStr, err := t.ProtoToValues(iop)
   505  	if err != nil {
   506  		return "", err
   507  	}
   508  	err = yaml.Unmarshal([]byte(apiValsStr), &apiVals)
   509  	if err != nil {
   510  		return "", err
   511  	}
   512  
   513  	scope.Debugf("Values translated from IstioOperator API:\n%s", apiValsStr)
   514  
   515  	// Add global overlay from IstioOperatorSpec.Values/UnvalidatedValues.
   516  	globalVals := iop.GetValues().AsMap()
   517  	globalUnvalidatedVals := iop.GetUnvalidatedValues().AsMap()
   518  
   519  	if scope.DebugEnabled() {
   520  		scope.Debugf("Values from IstioOperatorSpec.Values:\n%s", util.ToYAML(globalVals))
   521  		scope.Debugf("Values from IstioOperatorSpec.UnvalidatedValues:\n%s", util.ToYAML(globalUnvalidatedVals))
   522  	}
   523  
   524  	mergedVals, err := util.OverlayTrees(apiVals, globalVals)
   525  	if err != nil {
   526  		return "", err
   527  	}
   528  	mergedVals, err = util.OverlayTrees(mergedVals, globalUnvalidatedVals)
   529  	if err != nil {
   530  		return "", err
   531  	}
   532  	c, f := t.ComponentMaps[componentName]
   533  	if f && c.FlattenValues {
   534  		globals, ok := mergedVals["global"].(map[string]any)
   535  		if !ok {
   536  			return "", fmt.Errorf("global value isn't a map")
   537  		}
   538  		components, ok := mergedVals[c.ToHelmValuesTreeRoot].(map[string]any)
   539  		if !ok {
   540  			return "", fmt.Errorf("component value isn't a map")
   541  		}
   542  		finalVals := map[string]any{}
   543  		// strip out anything from the original apiVals which are a map[string]any but populate other top-level fields
   544  		for k, v := range apiVals {
   545  			_, isMap := v.(map[string]any)
   546  			if !isMap {
   547  				finalVals[k] = v
   548  			}
   549  		}
   550  		for k := range topLevelFields {
   551  			if v, f := mergedVals[k]; f {
   552  				finalVals[k] = v
   553  			}
   554  		}
   555  		for k, v := range globals {
   556  			finalVals[k] = v
   557  		}
   558  		for k, v := range components {
   559  			finalVals[k] = v
   560  		}
   561  		mergedVals = finalVals
   562  	}
   563  
   564  	mergedYAML, err := yaml.Marshal(mergedVals)
   565  	if err != nil {
   566  		return "", err
   567  	}
   568  
   569  	mergedYAML, err = applyGatewayTranslations(mergedYAML, componentName, componentsSpec)
   570  	if err != nil {
   571  		return "", err
   572  	}
   573  
   574  	return string(mergedYAML), err
   575  }
   576  
   577  // applyGatewayTranslations writes gateway name gwName at the appropriate values path in iop and maps k8s.service.ports
   578  // to values. It returns the resulting YAML tree.
   579  func applyGatewayTranslations(iop []byte, componentName name.ComponentName, componentSpec any) ([]byte, error) {
   580  	if !componentName.IsGateway() {
   581  		return iop, nil
   582  	}
   583  	iopt := make(map[string]any)
   584  	if err := yaml.Unmarshal(iop, &iopt); err != nil {
   585  		return nil, err
   586  	}
   587  	gwSpec := componentSpec.(*v1alpha1.GatewaySpec)
   588  	k8s := gwSpec.K8S
   589  	switch componentName {
   590  	case name.IngressComponentName:
   591  		setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-ingressgateway.name"), gwSpec.Name)
   592  		if len(gwSpec.Label) != 0 {
   593  			setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-ingressgateway.labels"), gwSpec.Label)
   594  		}
   595  		if k8s != nil && k8s.Service != nil && k8s.Service.Ports != nil {
   596  			setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-ingressgateway.ports"), k8s.Service.Ports)
   597  		}
   598  	case name.EgressComponentName:
   599  		setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-egressgateway.name"), gwSpec.Name)
   600  		if len(gwSpec.Label) != 0 {
   601  			setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-egressgateway.labels"), gwSpec.Label)
   602  		}
   603  		if k8s != nil && k8s.Service != nil && k8s.Service.Ports != nil {
   604  			setYAMLNodeByMapPath(iopt, util.PathFromString("gateways.istio-egressgateway.ports"), k8s.Service.Ports)
   605  		}
   606  	}
   607  	return yaml.Marshal(iopt)
   608  }
   609  
   610  // setYAMLNodeByMapPath sets the value at the given path to val in treeNode. The path cannot traverse lists and
   611  // treeNode must be a YAML tree unmarshaled into a plain map data structure.
   612  func setYAMLNodeByMapPath(treeNode any, path util.Path, val any) {
   613  	if len(path) == 0 || treeNode == nil {
   614  		return
   615  	}
   616  	pe := path[0]
   617  	switch nt := treeNode.(type) {
   618  	case map[any]any:
   619  		if len(path) == 1 {
   620  			nt[pe] = val
   621  			return
   622  		}
   623  		if nt[pe] == nil {
   624  			return
   625  		}
   626  		setYAMLNodeByMapPath(nt[pe], path[1:], val)
   627  	case map[string]any:
   628  		if len(path) == 1 {
   629  			nt[pe] = val
   630  			return
   631  		}
   632  		if nt[pe] == nil {
   633  			return
   634  		}
   635  		setYAMLNodeByMapPath(nt[pe], path[1:], val)
   636  	}
   637  }
   638  
   639  // ComponentMap returns a ComponentMaps struct ptr for the given component name if one exists.
   640  // If the name of the component is lower case, the function will use the capitalized version
   641  // of the name.
   642  func (t *Translator) ComponentMap(cns string) *ComponentMaps {
   643  	cn := name.TitleCase(name.ComponentName(cns))
   644  	return t.ComponentMaps[cn]
   645  }
   646  
   647  func (t *Translator) ProtoToHelmValues2(ii *v1alpha1.IstioOperatorSpec) (map[string]any, error) {
   648  	by, err := json.Marshal(ii)
   649  	if err != nil {
   650  		return nil, err
   651  	}
   652  	res := map[string]any{}
   653  	err = json.Unmarshal(by, &res)
   654  	if err != nil {
   655  		return nil, err
   656  	}
   657  	r2 := map[string]any{}
   658  	errs := t.ProtoToHelmValues(res, r2, nil)
   659  	return r2, errs.ToError()
   660  }
   661  
   662  // ProtoToHelmValues function below is used by third party for integrations and has to be public
   663  
   664  // ProtoToHelmValues takes an interface which must be a struct ptr and recursively iterates through all its fields.
   665  // For each leaf, if looks for a mapping from the struct data path to the corresponding YAML path and if one is
   666  // found, it calls the associated mapping function if one is defined to populate the values YAML path.
   667  // If no mapping function is defined, it uses the default mapping function.
   668  func (t *Translator) ProtoToHelmValues(node any, root map[string]any, path util.Path) (errs util.Errors) {
   669  	scope.Debugf("ProtoToHelmValues with path %s, %v (%T)", path, node, node)
   670  	if util.IsValueNil(node) {
   671  		return nil
   672  	}
   673  
   674  	vv := reflect.ValueOf(node)
   675  	vt := reflect.TypeOf(node)
   676  	switch vt.Kind() {
   677  	case reflect.Ptr:
   678  		if !util.IsNilOrInvalidValue(vv.Elem()) {
   679  			errs = util.AppendErrs(errs, t.ProtoToHelmValues(vv.Elem().Interface(), root, path))
   680  		}
   681  	case reflect.Struct:
   682  		scope.Debug("Struct")
   683  		for i := 0; i < vv.NumField(); i++ {
   684  			fieldName := vv.Type().Field(i).Name
   685  			fieldValue := vv.Field(i)
   686  			scope.Debugf("Checking field %s", fieldName)
   687  			if a, ok := vv.Type().Field(i).Tag.Lookup("json"); ok && a == "-" {
   688  				continue
   689  			}
   690  			if !fieldValue.CanInterface() {
   691  				continue
   692  			}
   693  			errs = util.AppendErrs(errs, t.ProtoToHelmValues(fieldValue.Interface(), root, append(path, fieldName)))
   694  		}
   695  	case reflect.Map:
   696  		scope.Debug("Map")
   697  		for _, key := range vv.MapKeys() {
   698  			nnp := append(path, key.String())
   699  			errs = util.AppendErrs(errs, t.insertLeaf(root, nnp, vv.MapIndex(key)))
   700  		}
   701  	case reflect.Slice:
   702  		scope.Debug("Slice")
   703  		for i := 0; i < vv.Len(); i++ {
   704  			errs = util.AppendErrs(errs, t.ProtoToHelmValues(vv.Index(i).Interface(), root, path))
   705  		}
   706  	default:
   707  		// Must be a leaf
   708  		scope.Debugf("field has kind %s", vt.Kind())
   709  		if vv.CanInterface() {
   710  			errs = util.AppendErrs(errs, t.insertLeaf(root, path, vv))
   711  		}
   712  	}
   713  
   714  	return errs
   715  }
   716  
   717  // setComponentProperties translates properties (e.g., enablement and namespace) of each component
   718  // in the baseYAML values tree, based on feature/component inheritance relationship.
   719  func (t *Translator) setComponentProperties(root map[string]any, iop *v1alpha1.IstioOperatorSpec) error {
   720  	var keys []string
   721  	for k := range t.ComponentMaps {
   722  		if k != name.IngressComponentName && k != name.EgressComponentName {
   723  			keys = append(keys, string(k))
   724  		}
   725  	}
   726  	sort.Strings(keys)
   727  	l := len(keys)
   728  	for i := l - 1; i >= 0; i-- {
   729  		cn := name.ComponentName(keys[i])
   730  		c := t.ComponentMaps[cn]
   731  		e, err := t.IsComponentEnabled(cn, iop)
   732  		if err != nil {
   733  			return err
   734  		}
   735  
   736  		enablementPath := c.ToHelmValuesTreeRoot
   737  		// CNI calls itself "cni" in the chart but "istio_cni" for enablement outside of the chart.
   738  		if cn == name.CNIComponentName {
   739  			enablementPath = "istio_cni"
   740  		}
   741  		if err := tpath.WriteNode(root, util.PathFromString(enablementPath+"."+HelmValuesEnabledSubpath), e); err != nil {
   742  			return err
   743  		}
   744  
   745  		ns, err := name.Namespace(cn, iop)
   746  		if err != nil {
   747  			return err
   748  		}
   749  		if err := tpath.WriteNode(root, util.PathFromString(c.ToHelmValuesTreeRoot+"."+HelmValuesNamespaceSubpath), ns); err != nil {
   750  			return err
   751  		}
   752  
   753  		hub, found, _ := tpath.GetFromStructPath(iop, "Components."+string(cn)+".Hub")
   754  		// Unmarshal unfortunately creates struct fields with "" for unset values. Skip these cases to avoid
   755  		// overwriting current value with an empty string.
   756  		hubStr, ok := hub.(string)
   757  		if found && !(ok && hubStr == "") {
   758  			if err := tpath.WriteNode(root, util.PathFromString(c.ToHelmValuesTreeRoot+"."+HelmValuesHubSubpath), hub); err != nil {
   759  				return err
   760  			}
   761  		}
   762  
   763  		tag, found, _ := tpath.GetFromStructPath(iop, "Components."+string(cn)+".Tag")
   764  		tagv, ok := tag.(*structpb.Value)
   765  		if found && !(ok && util.ValueString(tagv) == "") {
   766  			if err := tpath.WriteNode(root, util.PathFromString(c.ToHelmValuesTreeRoot+"."+HelmValuesTagSubpath), util.ValueString(tagv)); err != nil {
   767  				return err
   768  			}
   769  		}
   770  	}
   771  
   772  	for cn, gns := range t.GlobalNamespaces {
   773  		ns, err := name.Namespace(cn, iop)
   774  		if err != nil {
   775  			return err
   776  		}
   777  		if err := tpath.WriteNode(root, util.PathFromString("global."+gns), ns); err != nil {
   778  			return err
   779  		}
   780  	}
   781  
   782  	return nil
   783  }
   784  
   785  // IsComponentEnabled reports whether the component with name cn is enabled, according to the translations in t,
   786  // and the contents of ocp.
   787  func (t *Translator) IsComponentEnabled(cn name.ComponentName, iop *v1alpha1.IstioOperatorSpec) (bool, error) {
   788  	if t.ComponentMaps[cn] == nil {
   789  		return false, nil
   790  	}
   791  	return IsComponentEnabledInSpec(cn, iop)
   792  }
   793  
   794  // insertLeaf inserts a leaf with value into root at path, which is first mapped using t.APIMapping.
   795  func (t *Translator) insertLeaf(root map[string]any, path util.Path, value reflect.Value) (errs util.Errors) {
   796  	// Must be a scalar leaf. See if we have a mapping.
   797  	valuesPath, m := getValuesPathMapping(t.APIMapping, path)
   798  	var v any
   799  	if value.Kind() == reflect.Ptr {
   800  		v = value.Elem().Interface()
   801  	} else {
   802  		v = value.Interface()
   803  	}
   804  	switch {
   805  	case m == nil:
   806  		break
   807  	case m.translationFunc == nil:
   808  		// Use default translation which just maps to a different part of the tree.
   809  		errs = util.AppendErr(errs, defaultTranslationFunc(m, root, valuesPath, v))
   810  	default:
   811  		// Use a custom translation function.
   812  		errs = util.AppendErr(errs, m.translationFunc(m, root, valuesPath, v))
   813  	}
   814  	return errs
   815  }
   816  
   817  // getValuesPathMapping tries to map path against the passed in mappings with a longest prefix match. If a matching prefix
   818  // is found, it returns the translated YAML path and the corresponding translation.
   819  // e.g. for mapping "a.b"  -> "1.2", the input path "a.b.c.d" would yield "1.2.c.d".
   820  func getValuesPathMapping(mappings map[string]*Translation, path util.Path) (string, *Translation) {
   821  	p := path
   822  	var m *Translation
   823  	for ; len(p) > 0; p = p[0 : len(p)-1] {
   824  		m = mappings[p.String()]
   825  		if m != nil {
   826  			break
   827  		}
   828  	}
   829  	if m == nil {
   830  		return "", nil
   831  	}
   832  
   833  	if m.OutPath == "" {
   834  		return "", m
   835  	}
   836  
   837  	out := m.OutPath + "." + path[len(p):].String()
   838  	scope.Debugf("translating %s to %s", path, out)
   839  	return out, m
   840  }
   841  
   842  // renderFeatureComponentPathTemplate renders a template of the form <path>{{.ComponentName}}<path> with
   843  // the supplied parameters.
   844  func renderFeatureComponentPathTemplate(tmpl string, componentName name.ComponentName) (string, error) {
   845  	type Temp struct {
   846  		ComponentName name.ComponentName
   847  	}
   848  	ts := Temp{
   849  		ComponentName: componentName,
   850  	}
   851  	return util.RenderTemplate(tmpl, ts)
   852  }
   853  
   854  // renderResourceComponentPathTemplate renders a template of the form <path>{{.ResourceName}}<path>{{.ContainerName}}<path> with
   855  // the supplied parameters.
   856  func (t *Translator) renderResourceComponentPathTemplate(tmpl string, componentName name.ComponentName,
   857  	resourceName, revision string,
   858  ) (string, error) {
   859  	cn := string(componentName)
   860  	cmp := t.ComponentMap(cn)
   861  	if cmp == nil {
   862  		return "", fmt.Errorf("component: %s does not exist in the componentMap", cn)
   863  	}
   864  	if resourceName == "" {
   865  		resourceName = cmp.ResourceName
   866  	}
   867  	// The istiod resource will be istiod-<REVISION>, so we need to append the revision suffix
   868  	if revision != "" && resourceName == "istiod" {
   869  		resourceName += "-" + revision
   870  	}
   871  	ts := struct {
   872  		ResourceType  string
   873  		ResourceName  string
   874  		ContainerName string
   875  	}{
   876  		ResourceType:  cmp.ResourceType,
   877  		ResourceName:  resourceName,
   878  		ContainerName: cmp.ContainerName,
   879  	}
   880  	return util.RenderTemplate(tmpl, ts)
   881  }
   882  
   883  // defaultTranslationFunc is the default translation to values. It maps a Go data path into a YAML path.
   884  func defaultTranslationFunc(m *Translation, root map[string]any, valuesPath string, value any) error {
   885  	var path []string
   886  
   887  	if util.IsEmptyString(value) {
   888  		scope.Debugf("Skip empty string value for path %s", m.OutPath)
   889  		return nil
   890  	}
   891  	if valuesPath == "" {
   892  		scope.Debugf("Not mapping to values, resources path is %s", m.OutPath)
   893  		return nil
   894  	}
   895  
   896  	for _, p := range util.PathFromString(valuesPath) {
   897  		path = append(path, firstCharToLower(p))
   898  	}
   899  
   900  	return tpath.WriteNode(root, path, value)
   901  }
   902  
   903  func firstCharToLower(s string) string {
   904  	return strings.ToLower(s[0:1]) + s[1:]
   905  }
   906  
   907  // MergeK8sObject function below is used by third party for integrations and has to be public
   908  
   909  // MergeK8sObject does strategic merge for overlayNode on the base object.
   910  func MergeK8sObject(base *object.K8sObject, overlayNode any, path util.Path) (*object.K8sObject, error) {
   911  	overlay, err := createPatchObjectFromPath(overlayNode, path)
   912  	if err != nil {
   913  		return nil, err
   914  	}
   915  	overlayYAML, err := yaml.Marshal(overlay)
   916  	if err != nil {
   917  		return nil, err
   918  	}
   919  	overlayJSON, err := yaml.YAMLToJSON(overlayYAML)
   920  	if err != nil {
   921  		return nil, fmt.Errorf("yamlToJSON error in overlayYAML: %s\n%s", err, overlayYAML)
   922  	}
   923  	baseJSON, err := base.JSON()
   924  	if err != nil {
   925  		return nil, err
   926  	}
   927  
   928  	// get a versioned object from the scheme, we can use the strategic patching mechanism
   929  	// (i.e. take advantage of patchStrategy in the type)
   930  	versionedObject, err := scheme.Scheme.New(base.GroupVersionKind())
   931  	if err != nil {
   932  		return nil, err
   933  	}
   934  	// strategic merge patch
   935  	newBytes, err := strategicpatch.StrategicMergePatch(baseJSON, overlayJSON, versionedObject)
   936  	if err != nil {
   937  		return nil, fmt.Errorf("get error: %s to merge patch:\n%s for base:\n%s", err, overlayJSON, baseJSON)
   938  	}
   939  
   940  	newObj, err := object.ParseJSONToK8sObject(newBytes)
   941  	if err != nil {
   942  		return nil, err
   943  	}
   944  
   945  	return newObj.ResolveK8sConflict(), nil
   946  }
   947  
   948  // createPatchObjectFromPath constructs patch object for node with path, returns nil object and error if the path is invalid.
   949  // e.g. node:
   950  //   - name: NEW_VAR
   951  //     value: new_value
   952  //
   953  // and path:
   954  //
   955  //	  spec.template.spec.containers.[name:discovery].env
   956  //	will construct the following patch object:
   957  //	  spec:
   958  //	    template:
   959  //	      spec:
   960  //	        containers:
   961  //	        - name: discovery
   962  //	          env:
   963  //	          - name: NEW_VAR
   964  //	            value: new_value
   965  func createPatchObjectFromPath(node any, path util.Path) (map[string]any, error) {
   966  	if len(path) == 0 {
   967  		return nil, fmt.Errorf("empty path %s", path)
   968  	}
   969  	if util.IsKVPathElement(path[0]) {
   970  		return nil, fmt.Errorf("path %s has an unexpected first element %s", path, path[0])
   971  	}
   972  	length := len(path)
   973  	if util.IsKVPathElement(path[length-1]) {
   974  		return nil, fmt.Errorf("path %s has an unexpected last element %s", path, path[length-1])
   975  	}
   976  
   977  	patchObj := make(map[string]any)
   978  	var currentNode, nextNode any
   979  	nextNode = patchObj
   980  	for i, pe := range path {
   981  		currentNode = nextNode
   982  		// last path element
   983  		if i == length-1 {
   984  			currentNode, ok := currentNode.(map[string]any)
   985  			if !ok {
   986  				return nil, fmt.Errorf("path %s has an unexpected non KV element %s", path, pe)
   987  			}
   988  			currentNode[pe] = node
   989  			break
   990  		}
   991  
   992  		if util.IsKVPathElement(pe) {
   993  			currentNode, ok := currentNode.([]any)
   994  			if !ok {
   995  				return nil, fmt.Errorf("path %s has an unexpected KV element %s", path, pe)
   996  			}
   997  			k, v, err := util.PathKV(pe)
   998  			if err != nil {
   999  				return nil, err
  1000  			}
  1001  			if k == "" || v == "" {
  1002  				return nil, fmt.Errorf("path %s has an invalid KV element %s", path, pe)
  1003  			}
  1004  			currentNode[0] = map[string]any{k: v}
  1005  			nextNode = currentNode[0]
  1006  			continue
  1007  		}
  1008  
  1009  		currentNode, ok := currentNode.(map[string]any)
  1010  		if !ok {
  1011  			return nil, fmt.Errorf("path %s has an unexpected non KV element %s", path, pe)
  1012  		}
  1013  		// next path element determines the next node type
  1014  		if util.IsKVPathElement(path[i+1]) {
  1015  			currentNode[pe] = make([]any, 1)
  1016  		} else {
  1017  			currentNode[pe] = make(map[string]any)
  1018  		}
  1019  		nextNode = currentNode[pe]
  1020  	}
  1021  	return patchObj, nil
  1022  }
  1023  
  1024  // IOPStoIOP takes an IstioOperatorSpec and returns a corresponding IstioOperator with the given name and namespace.
  1025  func IOPStoIOP(iops proto.Message, name, namespace string) (*iopv1alpha1.IstioOperator, error) {
  1026  	iopStr, err := IOPStoIOPstr(iops, name, namespace)
  1027  	if err != nil {
  1028  		return nil, err
  1029  	}
  1030  	iop, err := istio.UnmarshalIstioOperator(iopStr, false)
  1031  	if err != nil {
  1032  		return nil, err
  1033  	}
  1034  	return iop, nil
  1035  }
  1036  
  1037  // IOPStoIOPstr takes an IstioOperatorSpec and returns a corresponding IstioOperator string with the given name and namespace.
  1038  func IOPStoIOPstr(iops proto.Message, name, namespace string) (string, error) {
  1039  	iopsStr, err := util.MarshalWithJSONPB(iops)
  1040  	if err != nil {
  1041  		return "", err
  1042  	}
  1043  	spec, err := tpath.AddSpecRoot(iopsStr)
  1044  	if err != nil {
  1045  		return "", err
  1046  	}
  1047  
  1048  	tmpl := `
  1049  apiVersion: install.istio.io/v1alpha1
  1050  kind: IstioOperator
  1051  metadata:
  1052    namespace: {{ .Namespace }}
  1053    name: {{ .Name }} 
  1054  `
  1055  	// Passing into template causes reformatting, use simple concatenation instead.
  1056  	tmpl += spec
  1057  
  1058  	type Temp struct {
  1059  		Namespace string
  1060  		Name      string
  1061  	}
  1062  	ts := Temp{
  1063  		Namespace: namespace,
  1064  		Name:      name,
  1065  	}
  1066  	return util.RenderTemplate(tmpl, ts)
  1067  }