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