istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/helm/helm.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 helm 16 17 import ( 18 "fmt" 19 "os" 20 "path/filepath" 21 "sort" 22 "strconv" 23 "strings" 24 25 "helm.sh/helm/v3/pkg/chart" 26 "helm.sh/helm/v3/pkg/chartutil" 27 "helm.sh/helm/v3/pkg/engine" 28 "k8s.io/apimachinery/pkg/version" 29 "sigs.k8s.io/yaml" 30 31 "istio.io/istio/istioctl/pkg/install/k8sversion" 32 "istio.io/istio/manifests" 33 "istio.io/istio/operator/pkg/util" 34 "istio.io/istio/pkg/log" 35 ) 36 37 const ( 38 // YAMLSeparator is a separator for multi-document YAML files. 39 YAMLSeparator = "\n---\n" 40 41 // DefaultProfileString is the name of the default profile. 42 DefaultProfileString = "default" 43 44 // NotesFileNameSuffix is the file name suffix for helm notes. 45 // see https://helm.sh/docs/chart_template_guide/notes_files/ 46 NotesFileNameSuffix = ".txt" 47 ) 48 49 var scope = log.RegisterScope("installer", "installer") 50 51 // TemplateFilterFunc filters templates to render by their file name 52 type TemplateFilterFunc func(string) bool 53 54 // TemplateRenderer defines a helm template renderer interface. 55 type TemplateRenderer interface { 56 // Run starts the renderer and should be called before using it. 57 Run() error 58 // RenderManifest renders the associated helm charts with the given values YAML string and returns the resulting 59 // string. 60 RenderManifest(values string) (string, error) 61 // RenderManifestFiltered filters manifests to render by template file name 62 RenderManifestFiltered(values string, filter TemplateFilterFunc) (string, error) 63 } 64 65 // NewHelmRenderer creates a new helm renderer with the given parameters and returns an interface to it. 66 // The format of helmBaseDir and profile strings determines the type of helm renderer returned (compiled-in, file, 67 // HTTP etc.) 68 func NewHelmRenderer(operatorDataDir, helmSubdir, componentName, namespace string, version *version.Info) TemplateRenderer { 69 dir := strings.Join([]string{ChartsSubdirName, helmSubdir}, "/") 70 return NewGenericRenderer(manifests.BuiltinOrDir(operatorDataDir), dir, componentName, namespace, version) 71 } 72 73 // ReadProfileYAML reads the YAML values associated with the given profile. It uses an appropriate reader for the 74 // profile format (compiled-in, file, HTTP, etc.). 75 func ReadProfileYAML(profile, manifestsPath string) (string, error) { 76 var err error 77 var globalValues string 78 79 // Get global values from profile. 80 switch { 81 case util.IsFilePath(profile): 82 if globalValues, err = readFile(profile); err != nil { 83 return "", err 84 } 85 default: 86 if globalValues, err = LoadValues(profile, manifestsPath); err != nil { 87 return "", fmt.Errorf("failed to read profile %v from %v: %v", profile, manifestsPath, err) 88 } 89 } 90 91 return globalValues, nil 92 } 93 94 // renderChart renders the given chart with the given values and returns the resulting YAML manifest string. 95 func renderChart(namespace, values string, chrt *chart.Chart, filterFunc TemplateFilterFunc, version *version.Info) (string, error) { 96 options := chartutil.ReleaseOptions{ 97 Name: "istio", 98 Namespace: namespace, 99 } 100 valuesMap := map[string]any{} 101 if err := yaml.Unmarshal([]byte(values), &valuesMap); err != nil { 102 return "", fmt.Errorf("failed to unmarshal values: %v", err) 103 } 104 105 caps := *chartutil.DefaultCapabilities 106 107 // overwrite helm default capabilities 108 operatorVersion, _ := chartutil.ParseKubeVersion("1." + strconv.Itoa(k8sversion.MinK8SVersion) + ".0") 109 caps.KubeVersion = *operatorVersion 110 111 if version != nil { 112 caps.KubeVersion = chartutil.KubeVersion{ 113 Version: version.GitVersion, 114 Major: version.Major, 115 Minor: version.Minor, 116 } 117 } 118 vals, err := chartutil.ToRenderValues(chrt, valuesMap, options, &caps) 119 if err != nil { 120 return "", err 121 } 122 123 if filterFunc != nil { 124 filteredTemplates := []*chart.File{} 125 for _, t := range chrt.Templates { 126 // Always include required templates that do not produce any output 127 if filterFunc(t.Name) || strings.HasSuffix(t.Name, ".tpl") || t.Name == "templates/zzz_profile.yaml" { 128 filteredTemplates = append(filteredTemplates, t) 129 } 130 } 131 chrt.Templates = filteredTemplates 132 } 133 134 files, err := engine.Render(chrt, vals) 135 crdFiles := chrt.CRDObjects() 136 if err != nil { 137 return "", err 138 } 139 if chrt.Metadata.Name == "base" { 140 base, _ := valuesMap["base"].(map[string]any) 141 if enableIstioConfigCRDs, ok := base["enableIstioConfigCRDs"].(bool); ok && !enableIstioConfigCRDs { 142 crdFiles = []chart.CRD{} 143 } 144 } 145 146 // Create sorted array of keys to iterate over, to stabilize the order of the rendered templates 147 keys := make([]string, 0, len(files)) 148 for k := range files { 149 if strings.HasSuffix(k, NotesFileNameSuffix) { 150 continue 151 } 152 keys = append(keys, k) 153 } 154 sort.Strings(keys) 155 156 var sb strings.Builder 157 for i := 0; i < len(keys); i++ { 158 f := files[keys[i]] 159 // add yaml separator if the rendered file doesn't have one at the end 160 f = strings.TrimSpace(f) + "\n" 161 if !strings.HasSuffix(f, YAMLSeparator) { 162 f += YAMLSeparator 163 } 164 _, err := sb.WriteString(f) 165 if err != nil { 166 return "", err 167 } 168 } 169 170 // Sort crd files by name to ensure stable manifest output 171 sort.Slice(crdFiles, func(i, j int) bool { return crdFiles[i].Name < crdFiles[j].Name }) 172 for _, crdFile := range crdFiles { 173 f := string(crdFile.File.Data) 174 // add yaml separator if the rendered file doesn't have one at the end 175 f = strings.TrimSpace(f) + "\n" 176 if !strings.HasSuffix(f, YAMLSeparator) { 177 f += YAMLSeparator 178 } 179 _, err := sb.WriteString(f) 180 if err != nil { 181 return "", err 182 } 183 } 184 185 return sb.String(), nil 186 } 187 188 // GenerateHubTagOverlay creates an IstioOperatorSpec overlay YAML for hub and tag. 189 func GenerateHubTagOverlay(hub, tag string) (string, error) { 190 hubTagYAMLTemplate := ` 191 spec: 192 hub: {{.Hub}} 193 tag: {{.Tag}} 194 ` 195 ts := struct { 196 Hub string 197 Tag string 198 }{ 199 Hub: hub, 200 Tag: tag, 201 } 202 return util.RenderTemplate(hubTagYAMLTemplate, ts) 203 } 204 205 // DefaultFilenameForProfile returns the profile name of the default profile for the given profile. 206 func DefaultFilenameForProfile(profile string) string { 207 switch { 208 case util.IsFilePath(profile): 209 return filepath.Join(filepath.Dir(profile), DefaultProfileFilename) 210 default: 211 return DefaultProfileString 212 } 213 } 214 215 // IsDefaultProfile reports whether the given profile is the default profile. 216 func IsDefaultProfile(profile string) bool { 217 return profile == "" || profile == DefaultProfileString || filepath.Base(profile) == DefaultProfileFilename 218 } 219 220 func readFile(path string) (string, error) { 221 b, err := os.ReadFile(path) 222 return string(b), err 223 } 224 225 // GetProfileYAML returns the YAML for the given profile name, using the given profileOrPath string, which may be either 226 // a profile label or a file path. 227 func GetProfileYAML(installPackagePath, profileOrPath string) (string, error) { 228 if profileOrPath == "" { 229 profileOrPath = "default" 230 } 231 profiles, err := readProfiles(installPackagePath) 232 if err != nil { 233 return "", fmt.Errorf("failed to read profiles: %v", err) 234 } 235 // If charts are a file path and profile is a name like default, transform it to the file path. 236 if profiles[profileOrPath] && installPackagePath != "" { 237 profileOrPath = filepath.Join(installPackagePath, "profiles", profileOrPath+".yaml") 238 } 239 // This contains the IstioOperator CR. 240 baseCRYAML, err := ReadProfileYAML(profileOrPath, installPackagePath) 241 if err != nil { 242 return "", err 243 } 244 245 if !IsDefaultProfile(profileOrPath) { 246 // Profile definitions are relative to the default profileOrPath, so read that first. 247 dfn := DefaultFilenameForProfile(profileOrPath) 248 defaultYAML, err := ReadProfileYAML(dfn, installPackagePath) 249 if err != nil { 250 return "", err 251 } 252 baseCRYAML, err = util.OverlayIOP(defaultYAML, baseCRYAML) 253 if err != nil { 254 return "", err 255 } 256 } 257 258 return baseCRYAML, nil 259 }