1/*
2Copyright The Helm Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package loader
18
19import (
20	"archive/tar"
21	"bytes"
22	"compress/gzip"
23	"io/ioutil"
24	"os"
25	"path/filepath"
26	"runtime"
27	"strings"
28	"testing"
29	"time"
30
31	"helm.sh/helm/v3/pkg/chart"
32)
33
34func TestLoadDir(t *testing.T) {
35	l, err := Loader("testdata/frobnitz")
36	if err != nil {
37		t.Fatalf("Failed to load testdata: %s", err)
38	}
39	c, err := l.Load()
40	if err != nil {
41		t.Fatalf("Failed to load testdata: %s", err)
42	}
43	verifyFrobnitz(t, c)
44	verifyChart(t, c)
45	verifyDependencies(t, c)
46	verifyDependenciesLock(t, c)
47}
48
49func TestLoadDirWithDevNull(t *testing.T) {
50	if runtime.GOOS == "windows" {
51		t.Skip("test only works on unix systems with /dev/null present")
52	}
53
54	l, err := Loader("testdata/frobnitz_with_dev_null")
55	if err != nil {
56		t.Fatalf("Failed to load testdata: %s", err)
57	}
58	if _, err := l.Load(); err == nil {
59		t.Errorf("packages with an irregular file (/dev/null) should not load")
60	}
61}
62
63func TestLoadDirWithSymlink(t *testing.T) {
64	sym := filepath.Join("..", "LICENSE")
65	link := filepath.Join("testdata", "frobnitz_with_symlink", "LICENSE")
66
67	if err := os.Symlink(sym, link); err != nil {
68		t.Fatal(err)
69	}
70
71	defer os.Remove(link)
72
73	l, err := Loader("testdata/frobnitz_with_symlink")
74	if err != nil {
75		t.Fatalf("Failed to load testdata: %s", err)
76	}
77
78	c, err := l.Load()
79	if err != nil {
80		t.Fatalf("Failed to load testdata: %s", err)
81	}
82	verifyFrobnitz(t, c)
83	verifyChart(t, c)
84	verifyDependencies(t, c)
85	verifyDependenciesLock(t, c)
86}
87
88func TestLoadV1(t *testing.T) {
89	l, err := Loader("testdata/frobnitz.v1")
90	if err != nil {
91		t.Fatalf("Failed to load testdata: %s", err)
92	}
93	c, err := l.Load()
94	if err != nil {
95		t.Fatalf("Failed to load testdata: %s", err)
96	}
97	verifyDependencies(t, c)
98	verifyDependenciesLock(t, c)
99}
100
101func TestLoadFileV1(t *testing.T) {
102	l, err := Loader("testdata/frobnitz.v1.tgz")
103	if err != nil {
104		t.Fatalf("Failed to load testdata: %s", err)
105	}
106	c, err := l.Load()
107	if err != nil {
108		t.Fatalf("Failed to load testdata: %s", err)
109	}
110	verifyDependencies(t, c)
111	verifyDependenciesLock(t, c)
112}
113
114func TestLoadFile(t *testing.T) {
115	l, err := Loader("testdata/frobnitz-1.2.3.tgz")
116	if err != nil {
117		t.Fatalf("Failed to load testdata: %s", err)
118	}
119	c, err := l.Load()
120	if err != nil {
121		t.Fatalf("Failed to load testdata: %s", err)
122	}
123	verifyFrobnitz(t, c)
124	verifyChart(t, c)
125	verifyDependencies(t, c)
126}
127
128func TestLoadFiles(t *testing.T) {
129	goodFiles := []*BufferedFile{
130		{
131			Name: "Chart.yaml",
132			Data: []byte(`apiVersion: v1
133name: frobnitz
134description: This is a frobnitz.
135version: "1.2.3"
136keywords:
137  - frobnitz
138  - sprocket
139  - dodad
140maintainers:
141  - name: The Helm Team
142    email: helm@example.com
143  - name: Someone Else
144    email: nobody@example.com
145sources:
146  - https://example.com/foo/bar
147home: http://example.com
148icon: https://example.com/64x64.png
149`),
150		},
151		{
152			Name: "values.yaml",
153			Data: []byte("var: some values"),
154		},
155		{
156			Name: "values.schema.json",
157			Data: []byte("type: Values"),
158		},
159		{
160			Name: "templates/deployment.yaml",
161			Data: []byte("some deployment"),
162		},
163		{
164			Name: "templates/service.yaml",
165			Data: []byte("some service"),
166		},
167	}
168
169	c, err := LoadFiles(goodFiles)
170	if err != nil {
171		t.Errorf("Expected good files to be loaded, got %v", err)
172	}
173
174	if c.Name() != "frobnitz" {
175		t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Name())
176	}
177
178	if c.Values["var"] != "some values" {
179		t.Error("Expected chart values to be populated with default values")
180	}
181
182	if len(c.Raw) != 5 {
183		t.Errorf("Expected %d files, got %d", 5, len(c.Raw))
184	}
185
186	if !bytes.Equal(c.Schema, []byte("type: Values")) {
187		t.Error("Expected chart schema to be populated with default values")
188	}
189
190	if len(c.Templates) != 2 {
191		t.Errorf("Expected number of templates == 2, got %d", len(c.Templates))
192	}
193
194	if _, err = LoadFiles([]*BufferedFile{}); err == nil {
195		t.Fatal("Expected err to be non-nil")
196	}
197	if err.Error() != "validation: chart.metadata is required" {
198		t.Errorf("Expected chart metadata missing error, got '%s'", err.Error())
199	}
200}
201
202// Packaging the chart on a Windows machine will produce an
203// archive that has \\ as delimiters. Test that we support these archives
204func TestLoadFileBackslash(t *testing.T) {
205	c, err := Load("testdata/frobnitz_backslash-1.2.3.tgz")
206	if err != nil {
207		t.Fatalf("Failed to load testdata: %s", err)
208	}
209	verifyChartFileAndTemplate(t, c, "frobnitz_backslash")
210	verifyChart(t, c)
211	verifyDependencies(t, c)
212}
213
214func TestLoadV2WithReqs(t *testing.T) {
215	l, err := Loader("testdata/frobnitz.v2.reqs")
216	if err != nil {
217		t.Fatalf("Failed to load testdata: %s", err)
218	}
219	c, err := l.Load()
220	if err != nil {
221		t.Fatalf("Failed to load testdata: %s", err)
222	}
223	verifyDependencies(t, c)
224	verifyDependenciesLock(t, c)
225}
226
227func TestLoadInvalidArchive(t *testing.T) {
228	tmpdir, err := ioutil.TempDir("", "helm-test-")
229	if err != nil {
230		t.Fatal(err)
231	}
232	defer os.Remove(tmpdir)
233
234	writeTar := func(filename, internalPath string, body []byte) {
235		dest, err := os.Create(filename)
236		if err != nil {
237			t.Fatal(err)
238		}
239		zipper := gzip.NewWriter(dest)
240		tw := tar.NewWriter(zipper)
241
242		h := &tar.Header{
243			Name:    internalPath,
244			Mode:    0755,
245			Size:    int64(len(body)),
246			ModTime: time.Now(),
247		}
248		if err := tw.WriteHeader(h); err != nil {
249			t.Fatal(err)
250		}
251		if _, err := tw.Write(body); err != nil {
252			t.Fatal(err)
253		}
254		tw.Close()
255		zipper.Close()
256		dest.Close()
257	}
258
259	for _, tt := range []struct {
260		chartname   string
261		internal    string
262		expectError string
263	}{
264		{"illegal-dots.tgz", "../../malformed-helm-test", "chart illegally references parent directory"},
265		{"illegal-dots2.tgz", "/foo/../../malformed-helm-test", "chart illegally references parent directory"},
266		{"illegal-dots3.tgz", "/../../malformed-helm-test", "chart illegally references parent directory"},
267		{"illegal-dots4.tgz", "./../../malformed-helm-test", "chart illegally references parent directory"},
268		{"illegal-name.tgz", "./.", "chart illegally contains content outside the base directory"},
269		{"illegal-name2.tgz", "/./.", "chart illegally contains content outside the base directory"},
270		{"illegal-name3.tgz", "missing-leading-slash", "chart illegally contains content outside the base directory"},
271		{"illegal-name4.tgz", "/missing-leading-slash", "validation: chart.metadata is required"},
272		{"illegal-abspath.tgz", "//foo", "chart illegally contains absolute paths"},
273		{"illegal-abspath2.tgz", "///foo", "chart illegally contains absolute paths"},
274		{"illegal-abspath3.tgz", "\\\\foo", "chart illegally contains absolute paths"},
275		{"illegal-abspath3.tgz", "\\..\\..\\foo", "chart illegally references parent directory"},
276
277		// Under special circumstances, this can get normalized to things that look like absolute Windows paths
278		{"illegal-abspath4.tgz", "\\.\\c:\\\\foo", "chart contains illegally named files"},
279		{"illegal-abspath5.tgz", "/./c://foo", "chart contains illegally named files"},
280		{"illegal-abspath6.tgz", "\\\\?\\Some\\windows\\magic", "chart illegally contains absolute paths"},
281	} {
282		illegalChart := filepath.Join(tmpdir, tt.chartname)
283		writeTar(illegalChart, tt.internal, []byte("hello: world"))
284		_, err = Load(illegalChart)
285		if err == nil {
286			t.Fatal("expected error when unpacking illegal files")
287		}
288		if !strings.Contains(err.Error(), tt.expectError) {
289			t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname)
290		}
291	}
292
293	// Make sure that absolute path gets interpreted as relative
294	illegalChart := filepath.Join(tmpdir, "abs-path.tgz")
295	writeTar(illegalChart, "/Chart.yaml", []byte("hello: world"))
296	_, err = Load(illegalChart)
297	if err.Error() != "validation: chart.metadata.name is required" {
298		t.Error(err)
299	}
300
301	// And just to validate that the above was not spurious
302	illegalChart = filepath.Join(tmpdir, "abs-path2.tgz")
303	writeTar(illegalChart, "files/whatever.yaml", []byte("hello: world"))
304	_, err = Load(illegalChart)
305	if err.Error() != "validation: chart.metadata is required" {
306		t.Error(err)
307	}
308
309	// Finally, test that drive letter gets stripped off on Windows
310	illegalChart = filepath.Join(tmpdir, "abs-winpath.tgz")
311	writeTar(illegalChart, "c:\\Chart.yaml", []byte("hello: world"))
312	_, err = Load(illegalChart)
313	if err.Error() != "validation: chart.metadata.name is required" {
314		t.Error(err)
315	}
316}
317
318func verifyChart(t *testing.T, c *chart.Chart) {
319	t.Helper()
320	if c.Name() == "" {
321		t.Fatalf("No chart metadata found on %v", c)
322	}
323	t.Logf("Verifying chart %s", c.Name())
324	if len(c.Templates) != 1 {
325		t.Errorf("Expected 1 template, got %d", len(c.Templates))
326	}
327
328	numfiles := 6
329	if len(c.Files) != numfiles {
330		t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files))
331		for _, n := range c.Files {
332			t.Logf("\t%s", n.Name)
333		}
334	}
335
336	if len(c.Dependencies()) != 2 {
337		t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies()), c.Dependencies())
338		for _, d := range c.Dependencies() {
339			t.Logf("\tSubchart: %s\n", d.Name())
340		}
341	}
342
343	expect := map[string]map[string]string{
344		"alpine": {
345			"version": "0.1.0",
346		},
347		"mariner": {
348			"version": "4.3.2",
349		},
350	}
351
352	for _, dep := range c.Dependencies() {
353		if dep.Metadata == nil {
354			t.Fatalf("expected metadata on dependency: %v", dep)
355		}
356		exp, ok := expect[dep.Name()]
357		if !ok {
358			t.Fatalf("Unknown dependency %s", dep.Name())
359		}
360		if exp["version"] != dep.Metadata.Version {
361			t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version)
362		}
363	}
364
365}
366
367func verifyDependencies(t *testing.T, c *chart.Chart) {
368	if len(c.Metadata.Dependencies) != 2 {
369		t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
370	}
371	tests := []*chart.Dependency{
372		{Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
373		{Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
374	}
375	for i, tt := range tests {
376		d := c.Metadata.Dependencies[i]
377		if d.Name != tt.Name {
378			t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
379		}
380		if d.Version != tt.Version {
381			t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
382		}
383		if d.Repository != tt.Repository {
384			t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
385		}
386	}
387}
388
389func verifyDependenciesLock(t *testing.T, c *chart.Chart) {
390	if len(c.Metadata.Dependencies) != 2 {
391		t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies))
392	}
393	tests := []*chart.Dependency{
394		{Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"},
395		{Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"},
396	}
397	for i, tt := range tests {
398		d := c.Metadata.Dependencies[i]
399		if d.Name != tt.Name {
400			t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name)
401		}
402		if d.Version != tt.Version {
403			t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version)
404		}
405		if d.Repository != tt.Repository {
406			t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository)
407		}
408	}
409}
410
411func verifyFrobnitz(t *testing.T, c *chart.Chart) {
412	verifyChartFileAndTemplate(t, c, "frobnitz")
413}
414
415func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) {
416	if c.Metadata == nil {
417		t.Fatal("Metadata is nil")
418	}
419	if c.Name() != name {
420		t.Errorf("Expected %s, got %s", name, c.Name())
421	}
422	if len(c.Templates) != 1 {
423		t.Fatalf("Expected 1 template, got %d", len(c.Templates))
424	}
425	if c.Templates[0].Name != "templates/template.tpl" {
426		t.Errorf("Unexpected template: %s", c.Templates[0].Name)
427	}
428	if len(c.Templates[0].Data) == 0 {
429		t.Error("No template data.")
430	}
431	if len(c.Files) != 6 {
432		t.Fatalf("Expected 6 Files, got %d", len(c.Files))
433	}
434	if len(c.Dependencies()) != 2 {
435		t.Fatalf("Expected 2 Dependency, got %d", len(c.Dependencies()))
436	}
437	if len(c.Metadata.Dependencies) != 2 {
438		t.Fatalf("Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies))
439	}
440	if len(c.Lock.Dependencies) != 2 {
441		t.Fatalf("Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies))
442	}
443
444	for _, dep := range c.Dependencies() {
445		switch dep.Name() {
446		case "mariner":
447		case "alpine":
448			if len(dep.Templates) != 1 {
449				t.Fatalf("Expected 1 template, got %d", len(dep.Templates))
450			}
451			if dep.Templates[0].Name != "templates/alpine-pod.yaml" {
452				t.Errorf("Unexpected template: %s", dep.Templates[0].Name)
453			}
454			if len(dep.Templates[0].Data) == 0 {
455				t.Error("No template data.")
456			}
457			if len(dep.Files) != 1 {
458				t.Fatalf("Expected 1 Files, got %d", len(dep.Files))
459			}
460			if len(dep.Dependencies()) != 2 {
461				t.Fatalf("Expected 2 Dependency, got %d", len(dep.Dependencies()))
462			}
463		default:
464			t.Errorf("Unexpected dependency %s", dep.Name())
465		}
466	}
467}
468