istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/manifest/shared.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 manifest 16 17 import ( 18 "fmt" 19 "io" 20 "os" 21 "reflect" 22 "strconv" 23 "strings" 24 25 "k8s.io/apimachinery/pkg/version" 26 "sigs.k8s.io/yaml" 27 28 "istio.io/api/operator/v1alpha1" 29 "istio.io/istio/operator/pkg/apis/istio" 30 iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" 31 "istio.io/istio/operator/pkg/apis/istio/v1alpha1/validation" 32 "istio.io/istio/operator/pkg/controlplane" 33 "istio.io/istio/operator/pkg/helm" 34 "istio.io/istio/operator/pkg/name" 35 "istio.io/istio/operator/pkg/object" 36 "istio.io/istio/operator/pkg/tpath" 37 "istio.io/istio/operator/pkg/translate" 38 "istio.io/istio/operator/pkg/util" 39 "istio.io/istio/operator/pkg/util/clog" 40 "istio.io/istio/operator/pkg/validate" 41 "istio.io/istio/pkg/kube" 42 "istio.io/istio/pkg/log" 43 "istio.io/istio/pkg/util/sets" 44 pkgversion "istio.io/istio/pkg/version" 45 ) 46 47 // installerScope is the scope for shared manifest package. 48 var installerScope = log.RegisterScope("installer", "installer") 49 50 // GenManifests generates a manifest map, keyed by the component name, from input file list and a YAML tree 51 // representation of path-values passed through the --set flag. 52 // If force is set, validation errors will not cause processing to abort but will result in warnings going to the 53 // supplied logger. 54 func GenManifests(inFilename []string, setFlags []string, force bool, filter []string, 55 client kube.Client, l clog.Logger, 56 ) (name.ManifestMap, *iopv1alpha1.IstioOperator, error) { 57 mergedYAML, _, err := GenerateConfig(inFilename, setFlags, force, client, l) 58 if err != nil { 59 return nil, nil, err 60 } 61 mergedIOPS, err := unmarshalAndValidateIOP(mergedYAML, force, false, l) 62 if err != nil { 63 return nil, nil, err 64 } 65 66 t := translate.NewTranslator() 67 var ver *version.Info 68 if client != nil { 69 ver, err = client.GetKubernetesVersion() 70 if err != nil { 71 return nil, nil, err 72 } 73 } 74 cp, err := controlplane.NewIstioControlPlane(mergedIOPS.Spec, t, filter, ver) 75 if err != nil { 76 return nil, nil, err 77 } 78 if err := cp.Run(); err != nil { 79 return nil, nil, err 80 } 81 82 manifests, errs := cp.RenderManifest() 83 if errs != nil { 84 return manifests, mergedIOPS, errs.ToError() 85 } 86 return manifests, mergedIOPS, nil 87 } 88 89 // GenerateConfig creates an IstioOperatorSpec from the following sources, overlaid sequentially: 90 // 1. Compiled in base, or optionally base from paths pointing to one or multiple ICP/IOP files at inFilenames. 91 // 2. Profile overlay, if non-default overlay is selected. This also comes either from compiled in or path specified in IOP contained in inFilenames. 92 // 3. User overlays stored in inFilenames. 93 // 4. setOverlayYAML, which comes from --set flag passed to manifest command. 94 // 95 // Note that the user overlay at inFilenames can optionally contain a file path to a set of profiles different from the 96 // ones that are compiled in. If it does, the starting point will be the base and profile YAMLs at that file path. 97 // Otherwise it will be the compiled in profile YAMLs. 98 // In step 3, the remaining fields in the same user overlay are applied on the resulting profile base. 99 // The force flag causes validation errors not to abort but only emit log/console warnings. 100 func GenerateConfig(inFilenames []string, setFlags []string, force bool, client kube.Client, 101 l clog.Logger, 102 ) (string, *iopv1alpha1.IstioOperator, error) { 103 if err := validateSetFlags(setFlags); err != nil { 104 return "", nil, err 105 } 106 107 fy, profile, err := ReadYamlProfile(inFilenames, setFlags, force, l) 108 if err != nil { 109 return "", nil, err 110 } 111 112 return OverlayYAMLStrings(profile, fy, setFlags, force, client, l) 113 } 114 115 func OverlayYAMLStrings(profile string, fy string, 116 setFlags []string, force bool, client kube.Client, l clog.Logger, 117 ) (string, *iopv1alpha1.IstioOperator, error) { 118 iopsString, iops, err := GenIOPFromProfile(profile, fy, setFlags, force, false, client, l) 119 if err != nil { 120 return "", nil, err 121 } 122 123 errs, warning := validation.ValidateConfig(false, iops.Spec) 124 if warning != "" { 125 l.LogAndError(warning) 126 } 127 128 if errs.ToError() != nil { 129 return "", nil, fmt.Errorf("generated config failed semantic validation: %v", errs) 130 } 131 return iopsString, iops, nil 132 } 133 134 // GenIOPFromProfile generates an IstioOperator from the given profile name or path, and overlay YAMLs from user 135 // files and the --set flag. If successful, it returns an IstioOperator string and struct. 136 func GenIOPFromProfile(profileOrPath, fileOverlayYAML string, setFlags []string, skipValidation, allowUnknownField bool, 137 client kube.Client, l clog.Logger, 138 ) (string, *iopv1alpha1.IstioOperator, error) { 139 installPackagePath, err := getInstallPackagePath(fileOverlayYAML) 140 if err != nil { 141 return "", nil, err 142 } 143 if sfp := GetValueForSetFlag(setFlags, "installPackagePath"); sfp != "" { 144 // set flag installPackagePath has the highest precedence, if set. 145 installPackagePath = sfp 146 } 147 148 // To generate the base profileOrPath for overlaying with user values, we need the installPackagePath where the profiles 149 // can be found, and the selected profileOrPath. Both of these can come from either the user overlay file or --set flag. 150 outYAML, err := helm.GetProfileYAML(installPackagePath, profileOrPath) 151 if err != nil { 152 return "", nil, err 153 } 154 155 // Hub and tag are only known at build time and must be passed in here during runtime from build stamps. 156 outYAML, err = overlayHubAndTag(outYAML) 157 if err != nil { 158 return "", nil, err 159 } 160 161 // Merge k8s specific values. 162 if client != nil { 163 kubeOverrides, err := getClusterSpecificValues(client) 164 if err != nil { 165 return "", nil, err 166 } 167 installerScope.Infof("Applying Cluster specific settings: %v", kubeOverrides) 168 outYAML, err = util.OverlayYAML(outYAML, kubeOverrides) 169 if err != nil { 170 return "", nil, err 171 } 172 } 173 174 // Combine file and --set overlays and translate any K8s settings in values to IOP format. Users should not set 175 // these but we have to support this path until it's deprecated. 176 overlayYAML, err := overlaySetFlagValues(fileOverlayYAML, setFlags) 177 if err != nil { 178 return "", nil, err 179 } 180 t := translate.NewReverseTranslator() 181 overlayYAML, err = t.TranslateK8SfromValueToIOP(overlayYAML) 182 if err != nil { 183 return "", nil, fmt.Errorf("could not overlay k8s settings from values to IOP: %s", err) 184 } 185 186 // Merge user file and --set flags. 187 outYAML, err = util.OverlayIOP(outYAML, overlayYAML) 188 if err != nil { 189 return "", nil, fmt.Errorf("could not overlay user config over base: %s", err) 190 } 191 192 // If enablement came from user values overlay (file or --set), translate into addonComponents paths and overlay that. 193 outYAML, err = translate.OverlayValuesEnablement(outYAML, overlayYAML, overlayYAML) 194 if err != nil { 195 return "", nil, err 196 } 197 198 // convertDefaultIOPMapValues converts default paths values into string, prevent errors when unmarshalling. 199 outYAML, err = convertDefaultIOPMapValues(outYAML, setFlags) 200 if err != nil { 201 return "", nil, err 202 } 203 204 finalIOP, err := unmarshalAndValidateIOP(outYAML, skipValidation, allowUnknownField, l) 205 if err != nil { 206 return "", nil, err 207 } 208 209 // Validate Final IOP config against K8s cluster 210 if client != nil { 211 err = util.ValidateIOPCAConfig(client, finalIOP) 212 if err != nil { 213 return "", nil, err 214 } 215 } 216 // InstallPackagePath may have been a URL, change to extracted to local file path. 217 finalIOP.Spec.InstallPackagePath = installPackagePath 218 if ns := GetValueForSetFlag(setFlags, "values.global.istioNamespace"); ns != "" { 219 finalIOP.Namespace = ns 220 } 221 if finalIOP.Spec.Profile == "" { 222 finalIOP.Spec.Profile = name.DefaultProfileName 223 } 224 return util.MustToYAMLGeneric(finalIOP), finalIOP, nil 225 } 226 227 // ReadYamlProfile gets the overlay yaml file from list of files and return profile value from file overlay and set overlay. 228 func ReadYamlProfile(inFilenames []string, setFlags []string, force bool, l clog.Logger) (string, string, error) { 229 profile := name.DefaultProfileName 230 // Get the overlay YAML from the list of files passed in. Also get the profile from the overlay files. 231 fy, fp, err := ParseYAMLFiles(inFilenames, force, l) 232 if err != nil { 233 return "", "", err 234 } 235 if fp != "" { 236 profile = fp 237 } 238 // The profile coming from --set flag has the highest precedence. 239 psf := GetValueForSetFlag(setFlags, "profile") 240 if psf != "" { 241 profile = psf 242 } 243 return fy, profile, nil 244 } 245 246 // ParseYAMLFiles parses the given slice of filenames containing YAML and merges them into a single IstioOperator 247 // format YAML strings. It returns the overlay YAML, the profile name and error result. 248 func ParseYAMLFiles(inFilenames []string, force bool, l clog.Logger) (overlayYAML string, profile string, err error) { 249 if inFilenames == nil { 250 return "", "", nil 251 } 252 y, err := ReadLayeredYAMLs(inFilenames) 253 if err != nil { 254 return "", "", err 255 } 256 var fileOverlayIOP *iopv1alpha1.IstioOperator 257 fileOverlayIOP, err = validate.UnmarshalIOP(y) 258 if err != nil { 259 return "", "", err 260 } 261 if err := validate.ValidIOP(fileOverlayIOP); err != nil { 262 if !force { 263 return "", "", fmt.Errorf("validation errors (use --force to override): \n%s", err) 264 } 265 l.LogAndErrorf("Validation errors (continuing because of --force):\n%s", err) 266 } 267 if fileOverlayIOP.Spec != nil && fileOverlayIOP.Spec.Profile != "" { 268 profile = fileOverlayIOP.Spec.Profile 269 } 270 return y, profile, nil 271 } 272 273 func ReadLayeredYAMLs(filenames []string) (string, error) { 274 return readLayeredYAMLs(filenames, os.Stdin) 275 } 276 277 func readLayeredYAMLs(filenames []string, stdinReader io.Reader) (string, error) { 278 var ly string 279 var stdin bool 280 for _, fn := range filenames { 281 var b []byte 282 var err error 283 if fn == "-" { 284 if stdin { 285 continue 286 } 287 stdin = true 288 b, err = io.ReadAll(stdinReader) 289 } else { 290 b, err = os.ReadFile(strings.TrimSpace(fn)) 291 } 292 if err != nil { 293 return "", err 294 } 295 multiple := false 296 multiple, err = hasMultipleIOPs(string(b)) 297 if err != nil { 298 return "", err 299 } 300 if multiple { 301 return "", fmt.Errorf("input file %s contains multiple IstioOperator CRs, only one per file is supported", fn) 302 } 303 ly, err = util.OverlayIOP(ly, string(b)) 304 if err != nil { 305 return "", err 306 } 307 } 308 return ly, nil 309 } 310 311 func hasMultipleIOPs(s string) (bool, error) { 312 objs, err := object.ParseK8sObjectsFromYAMLManifest(s) 313 if err != nil { 314 return false, err 315 } 316 found := false 317 for _, o := range objs { 318 if o.Kind == name.IstioOperator { 319 if found { 320 return true, nil 321 } 322 found = true 323 } 324 } 325 return false, nil 326 } 327 328 func GetProfile(iop *iopv1alpha1.IstioOperator) string { 329 profile := "default" 330 if iop != nil && iop.Spec != nil && iop.Spec.Profile != "" { 331 profile = iop.Spec.Profile 332 } 333 return profile 334 } 335 336 func GetMergedIOP(userIOPStr, profile, manifestsPath, revision string, client kube.Client, 337 logger clog.Logger, 338 ) (*iopv1alpha1.IstioOperator, error) { 339 extraFlags := make([]string, 0) 340 if manifestsPath != "" { 341 extraFlags = append(extraFlags, fmt.Sprintf("installPackagePath=%s", manifestsPath)) 342 } 343 if revision != "" { 344 extraFlags = append(extraFlags, fmt.Sprintf("revision=%s", revision)) 345 } 346 _, mergedIOP, err := OverlayYAMLStrings(profile, userIOPStr, extraFlags, false, client, logger) 347 if err != nil { 348 return nil, err 349 } 350 return mergedIOP, nil 351 } 352 353 // validateSetFlags validates that setFlags all have path=value format. 354 func validateSetFlags(setFlags []string) error { 355 for _, sf := range setFlags { 356 pv := strings.Split(sf, "=") 357 if len(pv) != 2 { 358 return fmt.Errorf("set flag %s has incorrect format, must be path=value", sf) 359 } 360 if pv[0] == "profile" && pv[1] == "external" { 361 return fmt.Errorf("\"external\" profile has been removed, use \"remote\" profile instead") 362 } 363 } 364 return nil 365 } 366 367 // Due to the fact that base profile is compiled in before a tag can be created, we must allow an additional 368 // override from variables that are set during release build time. 369 func overlayHubAndTag(yml string) (string, error) { 370 hub := pkgversion.DockerInfo.Hub 371 tag := pkgversion.DockerInfo.Tag 372 out := yml 373 if hub != "unknown" && tag != "unknown" { 374 buildHubTagOverlayYAML, err := helm.GenerateHubTagOverlay(hub, tag) 375 if err != nil { 376 return "", err 377 } 378 out, err = util.OverlayYAML(yml, buildHubTagOverlayYAML) 379 if err != nil { 380 return "", err 381 } 382 } 383 return out, nil 384 } 385 386 func getClusterSpecificValues(client kube.Client) (string, error) { 387 overlays := []string{} 388 389 cni := getCNISettings(client) 390 if cni != "" { 391 overlays = append(overlays, cni) 392 } 393 return makeTreeFromSetList(overlays) 394 } 395 396 // getCNISettings gets auto-detected values based on the Kubernetes environment. 397 // Note: there are other settings as well; however, these are detected inline in the helm chart. 398 // This ensures helm users also get them. 399 func getCNISettings(client kube.Client) string { 400 ver, err := client.GetKubernetesVersion() 401 if err != nil { 402 return "" 403 } 404 // https://istio.io/latest/docs/setup/additional-setup/cni/#hosted-kubernetes-settings 405 // GKE requires deployment in kube-system namespace. 406 if strings.Contains(ver.GitVersion, "-gke") { 407 return "components.cni.namespace=kube-system" 408 } 409 // TODO: OpenShift 410 return "" 411 } 412 413 // makeTreeFromSetList creates a YAML tree from a string slice containing key-value pairs in the format key=value. 414 func makeTreeFromSetList(setOverlay []string) (string, error) { 415 if len(setOverlay) == 0 { 416 return "", nil 417 } 418 tree := make(map[string]any) 419 for _, kv := range setOverlay { 420 kvv := strings.Split(kv, "=") 421 if len(kvv) != 2 { 422 return "", fmt.Errorf("bad argument %s: expect format key=value", kv) 423 } 424 k := kvv[0] 425 v := util.ParseValue(kvv[1]) 426 if err := tpath.WriteNode(tree, util.PathFromString(k), v); err != nil { 427 return "", err 428 } 429 // To make errors more user friendly, test the path and error out immediately if we cannot unmarshal. 430 testTree, err := yaml.Marshal(tree) 431 if err != nil { 432 return "", err 433 } 434 iops := &v1alpha1.IstioOperatorSpec{} 435 if err := util.UnmarshalWithJSONPB(string(testTree), iops, false); err != nil { 436 return "", fmt.Errorf("bad path=value %s: %v", kv, err) 437 } 438 } 439 out, err := yaml.Marshal(tree) 440 if err != nil { 441 return "", err 442 } 443 return tpath.AddSpecRoot(string(out)) 444 } 445 446 // unmarshalAndValidateIOP unmarshals a string containing IstioOperator YAML, validates it, and returns a struct 447 // representation if successful. If force is set, validation errors are written to logger rather than causing an 448 // error. 449 func unmarshalAndValidateIOP(iopsYAML string, force, allowUnknownField bool, l clog.Logger) (*iopv1alpha1.IstioOperator, error) { 450 iop, err := istio.UnmarshalIstioOperator(iopsYAML, allowUnknownField) 451 if err != nil { 452 return nil, fmt.Errorf("could not unmarshal merged YAML: %s\n\nYAML:\n%s", err, iopsYAML) 453 } 454 if errs := validate.CheckIstioOperatorSpec(iop.Spec, true); len(errs) != 0 && !force { 455 l.LogAndError("Run the command with the --force flag if you want to ignore the validation error and proceed.") 456 return iop, fmt.Errorf(errs.Error()) 457 } 458 return iop, nil 459 } 460 461 // getInstallPackagePath returns the installPackagePath in the given IstioOperator YAML string. 462 func getInstallPackagePath(iopYAML string) (string, error) { 463 iop, err := validate.UnmarshalIOP(iopYAML) 464 if err != nil { 465 return "", err 466 } 467 if iop.Spec == nil { 468 return "", nil 469 } 470 return iop.Spec.InstallPackagePath, nil 471 } 472 473 // alwaysString represents types that should always be decoded as strings 474 // TODO: this could be automatically derived from the value_types.proto? 475 var alwaysString = sets.New("values.compatibilityVersion", "compatibilityVersion") 476 477 // overlaySetFlagValues overlays each of the setFlags on top of the passed in IOP YAML string. 478 func overlaySetFlagValues(iopYAML string, setFlags []string) (string, error) { 479 iop := make(map[string]any) 480 if err := yaml.Unmarshal([]byte(iopYAML), &iop); err != nil { 481 return "", err 482 } 483 // Unmarshal returns nil for empty manifests but we need something to insert into. 484 if iop == nil { 485 iop = make(map[string]any) 486 } 487 488 for _, sf := range setFlags { 489 p, v := getPV(sf) 490 p = strings.TrimPrefix(p, "spec.") 491 inc, _, err := tpath.GetPathContext(iop, util.PathFromString("spec."+p), true) 492 if err != nil { 493 return "", err 494 } 495 // input value type is always string, transform it to correct type before setting. 496 var val any = v 497 if !alwaysString.Contains(p) { 498 val = util.ParseValue(v) 499 } 500 if err := tpath.WritePathContext(inc, val, false); err != nil { 501 return "", err 502 } 503 } 504 505 out, err := yaml.Marshal(iop) 506 if err != nil { 507 return "", err 508 } 509 510 return string(out), nil 511 } 512 513 var defaultSetFlagConvertPaths = []string{ 514 "meshConfig.defaultConfig.proxyMetadata", 515 } 516 517 // convertDefaultIOPMapValues converts default map[string]string values into string. 518 func convertDefaultIOPMapValues(outYAML string, setFlags []string) (string, error) { 519 return convertIOPMapValues(outYAML, setFlags, defaultSetFlagConvertPaths) 520 } 521 522 // convertIOPMapValues converts certain paths of map[string]string values into string. 523 func convertIOPMapValues(outYAML string, setFlags []string, convertPaths []string) (string, error) { 524 for _, setFlagConvertPath := range convertPaths { 525 if containParentPath(setFlags, setFlagConvertPath) { 526 var ( 527 converter = map[string]interface{}{} 528 convertedProxyMetadata = map[string]string{} 529 subPaths = strings.Split(setFlagConvertPath, ".") 530 ) 531 532 if err := yaml.Unmarshal([]byte(outYAML), &converter); err != nil { 533 return outYAML, err 534 } 535 originMap, ok := converter["spec"].(map[string]any) 536 if !ok { 537 return outYAML, nil 538 } 539 540 for index, subPath := range subPaths { 541 if _, ok := originMap[subPath].(map[string]any); !ok { 542 return outYAML, fmt.Errorf("can not convert subPath %s in setFlag path %s", 543 subPath, setFlagConvertPath) 544 } 545 546 if index == len(subPaths)-1 { 547 for key, value := range originMap[subPath].(map[string]any) { 548 if reflect.TypeOf(value).Kind() == reflect.Int { 549 convertedProxyMetadata[key] = strconv.FormatInt(value.(int64), 10) 550 } 551 if reflect.TypeOf(value).Kind() == reflect.Bool { 552 convertedProxyMetadata[key] = strconv.FormatBool(value.(bool)) 553 } 554 if reflect.TypeOf(value).Kind() == reflect.Float64 { 555 convertedProxyMetadata[key] = fmt.Sprint(value) 556 } 557 if reflect.TypeOf(value).Kind() == reflect.String { 558 convertedProxyMetadata[key] = value.(string) 559 } 560 } 561 originMap[subPath] = convertedProxyMetadata 562 } else { 563 originMap = originMap[subPath].(map[string]any) 564 } 565 } 566 567 convertedYaml, err := yaml.Marshal(converter) 568 if err != nil { 569 return outYAML, err 570 } 571 return string(convertedYaml), nil 572 } 573 } 574 575 return outYAML, nil 576 } 577 578 // containParentPath checks if setFlags contain parent path. 579 func containParentPath(setFlags []string, parentPath string) bool { 580 ret := false 581 for _, sf := range setFlags { 582 p, _ := getPV(sf) 583 if strings.Contains(p, parentPath) { 584 ret = true 585 break 586 } 587 } 588 return ret 589 } 590 591 // GetValueForSetFlag parses the passed set flags which have format key=value and if any set the given path, 592 // returns the corresponding value, otherwise returns the empty string. setFlags must have valid format. 593 func GetValueForSetFlag(setFlags []string, path string) string { 594 ret := "" 595 for _, sf := range setFlags { 596 p, v := getPV(sf) 597 if p == path { 598 ret = v 599 } 600 // if set multiple times, return last set value 601 } 602 return ret 603 } 604 605 // getPV returns the path and value components for the given set flag string, which must be in path=value format. 606 func getPV(setFlag string) (path string, value string) { 607 pv := strings.Split(setFlag, "=") 608 if len(pv) != 2 { 609 return setFlag, "" 610 } 611 path, value = strings.TrimSpace(pv[0]), strings.TrimSpace(pv[1]) 612 return 613 }