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 }