istio

Форк
0
/
translate.go 
1067 строк · 36.3 Кб
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
}
1068

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.