1// Copyright 2019 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 15package helm 16 17import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "html/template" 22 "io/ioutil" 23 "os" 24 "path/filepath" 25 "sort" 26 "strings" 27 28 "helm.sh/helm/v3/pkg/chart" 29 "helm.sh/helm/v3/pkg/chart/loader" 30 "helm.sh/helm/v3/pkg/engine" 31 "sigs.k8s.io/yaml" 32 33 "helm.sh/helm/v3/pkg/chartutil" 34 35 "istio.io/pkg/log" 36 37 "istio.io/istio/operator/pkg/util" 38 "istio.io/istio/operator/pkg/vfs" 39) 40 41const ( 42 // YAMLSeparator is a separator for multi-document YAML files. 43 YAMLSeparator = "\n---\n" 44 45 // DefaultProfileString is the name of the default profile. 46 DefaultProfileString = "default" 47 48 // notes file name suffix for the helm chart. 49 NotesFileNameSuffix = ".txt" 50) 51 52var ( 53 scope = log.RegisterScope("installer", "installer", 0) 54) 55 56// TemplateRenderer defines a helm template renderer interface. 57type TemplateRenderer interface { 58 // Run starts the renderer and should be called before using it. 59 Run() error 60 // RenderManifest renders the associated helm charts with the given values YAML string and returns the resulting 61 // string. 62 RenderManifest(values string) (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.) 68func NewHelmRenderer(operatorDataDir, helmSubdir, componentName, namespace string) (TemplateRenderer, error) { 69 dir := filepath.Join(ChartsSubdirName, helmSubdir) 70 switch { 71 case operatorDataDir == "": 72 return NewVFSRenderer(dir, componentName, namespace), nil 73 default: 74 return NewFileTemplateRenderer(filepath.Join(operatorDataDir, dir), componentName, namespace), nil 75 } 76} 77 78// ReadProfileYAML reads the YAML values associated with the given profile. It uses an appropriate reader for the 79// profile format (compiled-in, file, HTTP, etc.). 80func ReadProfileYAML(profile string) (string, error) { 81 var err error 82 var globalValues string 83 if profile == "" { 84 scope.Infof("ReadProfileYAML for profile name: [Empty]") 85 } else { 86 scope.Infof("ReadProfileYAML for profile name: %s", profile) 87 } 88 89 // Get global values from profile. 90 switch { 91 case IsBuiltinProfileName(profile): 92 if globalValues, err = LoadValuesVFS(profile); err != nil { 93 return "", err 94 } 95 case util.IsFilePath(profile): 96 scope.Infof("Loading values from local filesystem at path %s", profile) 97 if globalValues, err = readFile(profile); err != nil { 98 return "", err 99 } 100 default: 101 return "", fmt.Errorf("unsupported Profile type: %s", profile) 102 } 103 104 return globalValues, nil 105} 106 107// renderChart renders the given chart with the given values and returns the resulting YAML manifest string. 108func renderChart(namespace, values string, chrt *chart.Chart) (string, error) { 109 options := chartutil.ReleaseOptions{ 110 Name: "istio", 111 Namespace: namespace, 112 } 113 valuesMap := map[string]interface{}{} 114 if err := yaml.Unmarshal([]byte(values), &valuesMap); err != nil { 115 return "", fmt.Errorf("failed to unmarshal values: %v", err) 116 } 117 118 vals, err := chartutil.ToRenderValues(chrt, valuesMap, options, nil) 119 if err != nil { 120 return "", err 121 } 122 123 files, err := engine.Render(chrt, vals) 124 crdFiles := chrt.CRDObjects() 125 if err != nil { 126 return "", err 127 } 128 129 // Create sorted array of keys to iterate over, to stabilize the order of the rendered templates 130 keys := make([]string, 0, len(files)) 131 for k := range files { 132 if strings.HasSuffix(k, NotesFileNameSuffix) { 133 continue 134 } 135 keys = append(keys, k) 136 } 137 sort.Strings(keys) 138 139 var sb strings.Builder 140 for i := 0; i < len(keys); i++ { 141 f := files[keys[i]] 142 // add yaml separator if the rendered file doesn't have one at the end 143 f = strings.TrimSpace(f) + "\n" 144 if !strings.HasSuffix(f, YAMLSeparator) { 145 f += YAMLSeparator 146 } 147 _, err := sb.WriteString(f) 148 if err != nil { 149 return "", err 150 } 151 } 152 for _, crdFile := range crdFiles { 153 f := string(crdFile.File.Data) 154 // add yaml separator if the rendered file doesn't have one at the end 155 f = strings.TrimSpace(f) + "\n" 156 if !strings.HasSuffix(f, YAMLSeparator) { 157 f += YAMLSeparator 158 } 159 _, err := sb.WriteString(f) 160 if err != nil { 161 return "", err 162 } 163 } 164 165 return sb.String(), nil 166} 167 168// GenerateHubTagOverlay creates an IstioOperatorSpec overlay YAML for hub and tag. 169func GenerateHubTagOverlay(hub, tag string) (string, error) { 170 hubTagYAMLTemplate := ` 171spec: 172 hub: {{.Hub}} 173 tag: {{.Tag}} 174` 175 ts := struct { 176 Hub string 177 Tag string 178 }{ 179 Hub: hub, 180 Tag: tag, 181 } 182 return renderTemplate(hubTagYAMLTemplate, ts) 183} 184 185// helper method to render template 186func renderTemplate(tmpl string, ts interface{}) (string, error) { 187 t, err := template.New("").Parse(tmpl) 188 if err != nil { 189 return "", err 190 } 191 buf := new(bytes.Buffer) 192 err = t.Execute(buf, ts) 193 if err != nil { 194 return "", err 195 } 196 return buf.String(), nil 197} 198 199// DefaultFilenameForProfile returns the profile name of the default profile for the given profile. 200func DefaultFilenameForProfile(profile string) (string, error) { 201 switch { 202 case util.IsFilePath(profile): 203 return filepath.Join(filepath.Dir(profile), DefaultProfileFilename), nil 204 default: 205 if _, ok := ProfileNames[profile]; ok || profile == "" { 206 return DefaultProfileString, nil 207 } 208 return "", fmt.Errorf("bad profile string %s", profile) 209 } 210} 211 212// IsDefaultProfile reports whether the given profile is the default profile. 213func IsDefaultProfile(profile string) bool { 214 return profile == "" || profile == DefaultProfileString || filepath.Base(profile) == DefaultProfileFilename 215} 216 217func readFile(path string) (string, error) { 218 b, err := ioutil.ReadFile(path) 219 return string(b), err 220} 221 222// GetAddonNamesFromCharts scans the charts directory for addon-components 223func GetAddonNamesFromCharts(chartsRootDir string, capitalize bool) (addonChartNames []string, err error) { 224 if chartsRootDir == "" { 225 // VFS 226 fnames, err := vfs.GetFilesRecursive(ChartsSubdirName) 227 if err != nil { 228 return nil, err 229 } 230 231 for _, fname := range fnames { 232 basename := filepath.Base(fname) 233 if basename == "Chart.yaml" { 234 b, err := vfs.ReadFile(fname) 235 if err != nil { 236 return nil, err 237 } 238 bf := &loader.BufferedFile{ 239 Name: basename, 240 Data: b, 241 } 242 bfs := []*loader.BufferedFile{bf} 243 scope.Debugf("Chart loaded: %s", bf.Name) 244 chart, err := loader.LoadFiles(bfs) 245 if err != nil { 246 return nil, err 247 } else if addonName := getAddonName(chart.Metadata); addonName != nil { 248 addonChartNames = append(addonChartNames, *addonName) 249 } 250 } 251 } 252 } else { 253 // filesystem 254 var chartFilenames []string 255 err = filepath.Walk(chartsRootDir, func(path string, info os.FileInfo, err error) error { 256 if err != nil { 257 return err 258 } 259 if info.IsDir() { 260 if ok, err := chartutil.IsChartDir(path); ok && err == nil { 261 chartFilenames = append(chartFilenames, filepath.Join(path, chartutil.ChartfileName)) 262 } 263 } 264 return nil 265 }) 266 if err != nil { 267 return nil, err 268 } 269 for _, filename := range chartFilenames { 270 metadata, err := chartutil.LoadChartfile(filename) 271 if err != nil { 272 continue 273 } 274 if addonName := getAddonName(metadata); addonName != nil { 275 addonChartNames = append(addonChartNames, *addonName) 276 } 277 } 278 } 279 // sort for consistent results 280 sort.Strings(addonChartNames) 281 // check for duplicates 282 seen := make(map[string]bool) 283 for i, name := range addonChartNames { 284 if capitalize { 285 name = strings.ToUpper(name[:1]) + name[1:] 286 addonChartNames[i] = name 287 } 288 if seen[name] { 289 return nil, errors.New("Duplicate AddonComponent defined: " + name) 290 } 291 seen[name] = true 292 } 293 return addonChartNames, nil 294} 295 296func getAddonName(metadata *chart.Metadata) *string { 297 for _, str := range metadata.Keywords { 298 if str == "istio-addon" { 299 return &metadata.Name 300 } 301 } 302 return nil 303} 304 305// GetProfileYAML returns the YAML for the given profile name, using the given profileOrPath string, which may be either 306// a profile label or a file path. 307func GetProfileYAML(installPackagePath, profileOrPath string) (string, error) { 308 if profileOrPath == "" { 309 profileOrPath = "default" 310 } 311 // If charts are a file path and profile is a name like default, transform it to the file path. 312 if installPackagePath != "" && IsBuiltinProfileName(profileOrPath) { 313 profileOrPath = filepath.Join(installPackagePath, "profiles", profileOrPath+".yaml") 314 } 315 // This contains the IstioOperator CR. 316 baseCRYAML, err := ReadProfileYAML(profileOrPath) 317 if err != nil { 318 return "", err 319 } 320 321 if !IsDefaultProfile(profileOrPath) { 322 // Profile definitions are relative to the default profileOrPath, so read that first. 323 dfn, err := DefaultFilenameForProfile(profileOrPath) 324 if err != nil { 325 return "", err 326 } 327 defaultYAML, err := ReadProfileYAML(dfn) 328 if err != nil { 329 return "", err 330 } 331 baseCRYAML, err = util.OverlayYAML(defaultYAML, baseCRYAML) 332 if err != nil { 333 return "", err 334 } 335 } 336 337 return baseCRYAML, nil 338} 339