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  }