hgctl/pkg/helm/common.go (264 lines of code) (raw):

// Copyright (c) 2022 Alibaba Group Holding Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package helm import ( "fmt" "io" "os" "path/filepath" "strings" "github.com/alibaba/higress/hgctl/pkg/helm/tpath" "github.com/alibaba/higress/hgctl/pkg/util" "sigs.k8s.io/yaml" ) // GetProfileFromFlags get profile name from flags. func GetProfileFromFlags(setFlags []string) (string, error) { profileName := DefaultProfileName // The profile coming from --set flag has the highest precedence. psf := GetValueForSetFlag(setFlags, "profile") if psf != "" { profileName = psf } return profileName, nil } func GetValuesOverylayFromFiles(inFilenames []string) (string, error) { // Convert layeredYamls under values node in profile file to support helm values overLayYamls := "" // Get Overlays from files if len(inFilenames) > 0 { layeredYamls, err := ReadLayeredYAMLs(inFilenames) if err != nil { return "", err } vals := make(map[string]any) if err := yaml.Unmarshal([]byte(layeredYamls), &vals); err != nil { return "", fmt.Errorf("%s:\n\nYAML:\n%s", err, layeredYamls) } values := make(map[string]any) values["values"] = vals out, err := yaml.Marshal(values) if err != nil { return "", err } overLayYamls = string(out) } return overLayYamls, nil } func GetUninstallProfileName() string { return DefaultUninstallProfileName } func ReadLayeredYAMLs(filenames []string) (string, error) { return readLayeredYAMLs(filenames, os.Stdin) } func readLayeredYAMLs(filenames []string, stdinReader io.Reader) (string, error) { var ly string var stdin bool for _, fn := range filenames { var b []byte var err error if fn == "-" { if stdin { continue } stdin = true b, err = io.ReadAll(stdinReader) } else { b, err = os.ReadFile(strings.TrimSpace(fn)) } if err != nil { return "", err } ly, err = util.OverlayYAML(ly, string(b)) if err != nil { return "", err } } return ly, nil } // GetValueForSetFlag parses the passed set flags which have format key=value and if any set the given path, // returns the corresponding value, otherwise returns the empty string. setFlags must have valid format. func GetValueForSetFlag(setFlags []string, path string) string { ret := "" for _, sf := range setFlags { p, v := getPV(sf) if p == path { ret = v } // if set multiple times, return last set value } return ret } // getPV returns the path and value components for the given set flag string, which must be in path=value format. func getPV(setFlag string) (path string, value string) { pv := strings.Split(setFlag, "=") if len(pv) != 2 { return setFlag, "" } path, value = strings.TrimSpace(pv[0]), strings.TrimSpace(pv[1]) return } func GenerateConfig(inFilenames []string, setFlags []string) (string, *Profile, string, error) { if err := validateSetFlags(setFlags); err != nil { return "", nil, "", err } profileName, err := GetProfileFromFlags(setFlags) if err != nil { return "", nil, "", err } valuesOverlay, err := GetValuesOverylayFromFiles(inFilenames) if err != nil { return "", nil, "", err } profileString, profile, err := GenProfile(profileName, valuesOverlay, setFlags) if err != nil { return "", nil, "", err } return profileString, profile, profileName, nil } // validateSetFlags validates that setFlags all have path=value format. func validateSetFlags(setFlags []string) error { for _, sf := range setFlags { pv := strings.Split(sf, "=") if len(pv) != 2 { return fmt.Errorf("set flag %s has incorrect format, must be path=value", sf) } } return nil } func overlaySetFlagValues(iopYAML string, setFlags []string) (string, error) { iop := make(map[string]any) if err := yaml.Unmarshal([]byte(iopYAML), &iop); err != nil { return "", err } // Unmarshal returns nil for empty manifests but we need something to insert into. if iop == nil { iop = make(map[string]any) } for _, sf := range setFlags { p, v := getPV(sf) inc, _, err := tpath.GetPathContext(iop, util.PathFromString(p), true) if err != nil { return "", err } // input value type is always string, transform it to correct type before setting. if err := tpath.WritePathContext(inc, util.ParseValue(v), false); err != nil { return "", err } } out, err := yaml.Marshal(iop) if err != nil { return "", err } return string(out), nil } // getInstallPackagePath returns the installPackagePath in the given IstioOperator YAML string. func getInstallPackagePath(profileYAML string) (string, error) { profile, err := UnmarshalProfile(profileYAML) if err != nil { return "", err } if profile == nil { return "", nil } return profile.InstallPackagePath, nil } // GetProfileYAML returns the YAML for the given profile name, using the given profileOrPath string, which may be either // a profile label or a file path. func GetProfileYAML(installPackagePath, profileOrPath string) (string, error) { if profileOrPath == "" { profileOrPath = DefaultProfileFilename } profiles, err := readProfiles(installPackagePath) if err != nil { return "", fmt.Errorf("failed to read profiles: %v", err) } // If charts are a file path and profile is a name like default, transform it to the file path. if profiles[profileOrPath] && installPackagePath != "" { profileOrPath = filepath.Join(installPackagePath, "profiles", profileOrPath+".yaml") } // This contains the IstioOperator CR. baseCRYAML, err := ReadProfileYAML(profileOrPath, installPackagePath) if err != nil { return "", err } //if !IsDefaultProfile(profileOrPath) { // // Profile definitions are relative to the default profileOrPath, so read that first. // dfn := DefaultFilenameForProfile(profileOrPath) // defaultYAML, err := ReadProfileYAML(dfn, installPackagePath) // if err != nil { // return "", err // } // baseCRYAML, err = util.OverlayYAML(defaultYAML, baseCRYAML) // if err != nil { // return "", err // } //} return baseCRYAML, nil } // IsDefaultProfile reports whether the given profile is the default profile. func IsDefaultProfile(profile string) bool { return profile == "" || profile == DefaultProfileName || filepath.Base(profile) == DefaultProfileFilename } // DefaultFilenameForProfile returns the profile name of the default profile for the given profile. func DefaultFilenameForProfile(profile string) string { switch { case util.IsFilePath(profile): return filepath.Join(filepath.Dir(profile), DefaultProfileFilename) default: return DefaultProfileName } } // ReadProfileYAML reads the YAML values associated with the given profile. It uses an appropriate reader for the // profile format (compiled-in, file, HTTP, etc.). func ReadProfileYAML(profile, manifestsPath string) (string, error) { var err error var globalValues string // Get global values from profile. switch { case util.IsFilePath(profile): if globalValues, err = readFile(profile); err != nil { return "", err } default: if globalValues, err = LoadValues(profile, manifestsPath); err != nil { return "", fmt.Errorf("failed to read profile %v from %v: %v", profile, manifestsPath, err) } } return globalValues, nil } func readFile(path string) (string, error) { b, err := os.ReadFile(path) return string(b), err } // UnmarshalProfile unmarshals a string containing Profile as YAML. func UnmarshalProfile(profileYAML string) (*Profile, error) { profile := &Profile{} if err := yaml.Unmarshal([]byte(profileYAML), profile); err != nil { return nil, fmt.Errorf("%s:\n\nYAML:\n%s", err, profileYAML) } return profile, nil } // GenProfile generates an Profile from the given profile name or path, and overlay YAMLs from user // files and the --set flag. If successful, it returns an Profile string and struct. func GenProfile(profileOrPath, fileOverlayYAML string, setFlags []string) (string, *Profile, error) { installPackagePath, err := getInstallPackagePath(fileOverlayYAML) if err != nil { return "", nil, err } if sfp := GetValueForSetFlag(setFlags, "installPackagePath"); sfp != "" { // set flag installPackagePath has the highest precedence, if set. installPackagePath = sfp } // To generate the base profileOrPath for overlaying with user values, we need the installPackagePath where the profiles // can be found, and the selected profileOrPath. Both of these can come from either the user overlay file or --set flag. outYAML, err := GetProfileYAML(installPackagePath, profileOrPath) if err != nil { return "", nil, err } // Combine file and --set overlays and translate any K8s settings in values to Profile format overlayYAML, err := overlaySetFlagValues(fileOverlayYAML, setFlags) if err != nil { return "", nil, err } // Merge user file and --set flags. outYAML, err = util.OverlayYAML(outYAML, overlayYAML) if err != nil { return "", nil, fmt.Errorf("could not overlay user config over base: %s", err) } finalProfile, err := UnmarshalProfile(outYAML) if err != nil { return "", nil, err } if len(installPackagePath) > 0 { finalProfile.InstallPackagePath = installPackagePath } if finalProfile.Profile == "" { finalProfile.Profile = DefaultProfileName } return util.ToYAML(finalProfile), finalProfile, nil } func GenProfileFromProfileContent(profileContent, fileOverlayYAML string, setFlags []string) (string, *Profile, error) { installPackagePath, err := getInstallPackagePath(fileOverlayYAML) if err != nil { return "", nil, err } if sfp := GetValueForSetFlag(setFlags, "installPackagePath"); sfp != "" { // set flag installPackagePath has the highest precedence, if set. installPackagePath = sfp } // Combine file and --set overlays and translate any K8s settings in values to Profile format overlayYAML, err := overlaySetFlagValues(fileOverlayYAML, setFlags) if err != nil { return "", nil, err } // Merge user file and --set flags. outYAML, err := util.OverlayYAML(profileContent, overlayYAML) if err != nil { return "", nil, fmt.Errorf("could not overlay user config over base: %s", err) } finalProfile, err := UnmarshalProfile(outYAML) if err != nil { return "", nil, err } if len(installPackagePath) > 0 { finalProfile.InstallPackagePath = installPackagePath } if finalProfile.Profile == "" { finalProfile.Profile = DefaultProfileName } return util.ToYAML(finalProfile), finalProfile, nil }