1/*
2Copyright The Helm Authors.
3Licensed under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License.
5You may obtain a copy of the License at
6
7http://www.apache.org/licenses/LICENSE-2.0
8
9Unless required by applicable law or agreed to in writing, software
10distributed under the License is distributed on an "AS IS" BASIS,
11WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12See the License for the specific language governing permissions and
13limitations under the License.
14*/
15
16package installer // import "helm.sh/helm/v3/pkg/plugin/installer"
17
18import (
19	"archive/tar"
20	"bytes"
21	"compress/gzip"
22	"encoding/base64"
23	"fmt"
24	"io/ioutil"
25	"net/http"
26	"net/http/httptest"
27	"os"
28	"path/filepath"
29	"strings"
30	"syscall"
31	"testing"
32
33	"github.com/pkg/errors"
34
35	"helm.sh/helm/v3/internal/test/ensure"
36	"helm.sh/helm/v3/pkg/getter"
37	"helm.sh/helm/v3/pkg/helmpath"
38)
39
40var _ Installer = new(HTTPInstaller)
41
42// Fake http client
43type TestHTTPGetter struct {
44	MockResponse *bytes.Buffer
45	MockError    error
46}
47
48func (t *TestHTTPGetter) Get(href string, _ ...getter.Option) (*bytes.Buffer, error) {
49	return t.MockResponse, t.MockError
50}
51
52// Fake plugin tarball data
53var fakePluginB64 = "H4sIAKRj51kAA+3UX0vCUBgGcC9jn+Iwuk3Peza3GeyiUlJQkcogCOzgli7dJm4TvYk+a5+k479UqquUCJ/fLs549sLO2TnvWnJa9aXnjwujYdYLovxMhsPcfnHOLdNkOXthM/IVQQYjg2yyLLJ4kXGhLp5j0z3P41tZksqxmspL3B/O+j/XtZu1y8rdYzkOZRCxduKPk53ny6Wwz/GfIIf1As8lxzGJSmoHNLJZphKHG4YpTCE0wVk3DULfpSJ3DMMqkj3P5JfMYLdX1Vr9Ie/5E5cstcdC8K04iGLX5HaJuKpWL17F0TCIBi5pf/0pjtLhun5j3f9v6r7wfnI/H0eNp9d1/5P6Gez0vzo7wsoxfrAZbTny/o9k6J8z/VkO/LPlWdC1iVpbEEcq5nmeJ13LEtmbV0k2r2PrOs9PuuNglC5rL1Y5S/syXRQmutaNw1BGnnp8Wq3UG51WvX1da3bKtZtCN/R09DwAAAAAAAAAAAAAAAAAAADAb30AoMczDwAoAAA="
54
55func TestStripName(t *testing.T) {
56	if stripPluginName("fake-plugin-0.0.1.tar.gz") != "fake-plugin" {
57		t.Errorf("name does not match expected value")
58	}
59	if stripPluginName("fake-plugin-0.0.1.tgz") != "fake-plugin" {
60		t.Errorf("name does not match expected value")
61	}
62	if stripPluginName("fake-plugin.tgz") != "fake-plugin" {
63		t.Errorf("name does not match expected value")
64	}
65	if stripPluginName("fake-plugin.tar.gz") != "fake-plugin" {
66		t.Errorf("name does not match expected value")
67	}
68}
69
70func mockArchiveServer() *httptest.Server {
71	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
72		if !strings.HasSuffix(r.URL.Path, ".tar.gz") {
73			w.Header().Add("Content-Type", "text/html")
74			fmt.Fprintln(w, "broken")
75			return
76		}
77		w.Header().Add("Content-Type", "application/gzip")
78		fmt.Fprintln(w, "test")
79	}))
80}
81
82func TestHTTPInstaller(t *testing.T) {
83	defer ensure.HelmHome(t)()
84
85	srv := mockArchiveServer()
86	defer srv.Close()
87	source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
88
89	if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
90		t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
91	}
92
93	i, err := NewForSource(source, "0.0.1")
94	if err != nil {
95		t.Fatalf("unexpected error: %s", err)
96	}
97
98	// ensure a HTTPInstaller was returned
99	httpInstaller, ok := i.(*HTTPInstaller)
100	if !ok {
101		t.Fatal("expected a HTTPInstaller")
102	}
103
104	// inject fake http client responding with minimal plugin tarball
105	mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64)
106	if err != nil {
107		t.Fatalf("Could not decode fake tgz plugin: %s", err)
108	}
109
110	httpInstaller.getter = &TestHTTPGetter{
111		MockResponse: bytes.NewBuffer(mockTgz),
112	}
113
114	// install the plugin
115	if err := Install(i); err != nil {
116		t.Fatal(err)
117	}
118	if i.Path() != helmpath.DataPath("plugins", "fake-plugin") {
119		t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path())
120	}
121
122	// Install again to test plugin exists error
123	if err := Install(i); err == nil {
124		t.Fatal("expected error for plugin exists, got none")
125	} else if err.Error() != "plugin already exists" {
126		t.Fatalf("expected error for plugin exists, got (%v)", err)
127	}
128
129}
130
131func TestHTTPInstallerNonExistentVersion(t *testing.T) {
132	defer ensure.HelmHome(t)()
133	srv := mockArchiveServer()
134	defer srv.Close()
135	source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
136
137	if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
138		t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
139	}
140
141	i, err := NewForSource(source, "0.0.2")
142	if err != nil {
143		t.Fatalf("unexpected error: %s", err)
144	}
145
146	// ensure a HTTPInstaller was returned
147	httpInstaller, ok := i.(*HTTPInstaller)
148	if !ok {
149		t.Fatal("expected a HTTPInstaller")
150	}
151
152	// inject fake http client responding with error
153	httpInstaller.getter = &TestHTTPGetter{
154		MockError: errors.Errorf("failed to download plugin for some reason"),
155	}
156
157	// attempt to install the plugin
158	if err := Install(i); err == nil {
159		t.Fatal("expected error from http client")
160	}
161
162}
163
164func TestHTTPInstallerUpdate(t *testing.T) {
165	srv := mockArchiveServer()
166	defer srv.Close()
167	source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
168	defer ensure.HelmHome(t)()
169
170	if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
171		t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
172	}
173
174	i, err := NewForSource(source, "0.0.1")
175	if err != nil {
176		t.Fatalf("unexpected error: %s", err)
177	}
178
179	// ensure a HTTPInstaller was returned
180	httpInstaller, ok := i.(*HTTPInstaller)
181	if !ok {
182		t.Fatal("expected a HTTPInstaller")
183	}
184
185	// inject fake http client responding with minimal plugin tarball
186	mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64)
187	if err != nil {
188		t.Fatalf("Could not decode fake tgz plugin: %s", err)
189	}
190
191	httpInstaller.getter = &TestHTTPGetter{
192		MockResponse: bytes.NewBuffer(mockTgz),
193	}
194
195	// install the plugin before updating
196	if err := Install(i); err != nil {
197		t.Fatal(err)
198	}
199	if i.Path() != helmpath.DataPath("plugins", "fake-plugin") {
200		t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path())
201	}
202
203	// Update plugin, should fail because it is not implemented
204	if err := Update(i); err == nil {
205		t.Fatal("update method not implemented for http installer")
206	}
207}
208
209func TestExtract(t *testing.T) {
210	source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz"
211
212	tempDir, err := ioutil.TempDir("", "")
213	if err != nil {
214		t.Fatal(err)
215	}
216	defer os.RemoveAll(tempDir)
217
218	// Set the umask to default open permissions so we can actually test
219	oldmask := syscall.Umask(0000)
220	defer func() {
221		syscall.Umask(oldmask)
222	}()
223
224	// Write a tarball to a buffer for us to extract
225	var tarbuf bytes.Buffer
226	tw := tar.NewWriter(&tarbuf)
227	var files = []struct {
228		Name, Body string
229		Mode       int64
230	}{
231		{"plugin.yaml", "plugin metadata", 0600},
232		{"README.md", "some text", 0777},
233	}
234	for _, file := range files {
235		hdr := &tar.Header{
236			Name:     file.Name,
237			Typeflag: tar.TypeReg,
238			Mode:     file.Mode,
239			Size:     int64(len(file.Body)),
240		}
241		if err := tw.WriteHeader(hdr); err != nil {
242			t.Fatal(err)
243		}
244		if _, err := tw.Write([]byte(file.Body)); err != nil {
245			t.Fatal(err)
246		}
247	}
248
249	// Add pax global headers. This should be ignored.
250	// Note the PAX header that isn't global cannot be written using WriteHeader.
251	// Details are in the internal Go function for the tar packaged named
252	// allowedFormats. For a TypeXHeader it will return a message stating
253	// "cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headers"
254	if err := tw.WriteHeader(&tar.Header{
255		Name:     "pax_global_header",
256		Typeflag: tar.TypeXGlobalHeader,
257	}); err != nil {
258		t.Fatal(err)
259	}
260
261	if err := tw.Close(); err != nil {
262		t.Fatal(err)
263	}
264
265	var buf bytes.Buffer
266	gz := gzip.NewWriter(&buf)
267	if _, err := gz.Write(tarbuf.Bytes()); err != nil {
268		t.Fatal(err)
269	}
270	gz.Close()
271	// END tarball creation
272
273	extractor, err := NewExtractor(source)
274	if err != nil {
275		t.Fatal(err)
276	}
277
278	if err = extractor.Extract(&buf, tempDir); err != nil {
279		t.Fatalf("Did not expect error but got error: %v", err)
280	}
281
282	pluginYAMLFullPath := filepath.Join(tempDir, "plugin.yaml")
283	if info, err := os.Stat(pluginYAMLFullPath); err != nil {
284		if os.IsNotExist(err) {
285			t.Fatalf("Expected %s to exist but doesn't", pluginYAMLFullPath)
286		}
287		t.Fatal(err)
288	} else if info.Mode().Perm() != 0600 {
289		t.Fatalf("Expected %s to have 0600 mode it but has %o", pluginYAMLFullPath, info.Mode().Perm())
290	}
291
292	readmeFullPath := filepath.Join(tempDir, "README.md")
293	if info, err := os.Stat(readmeFullPath); err != nil {
294		if os.IsNotExist(err) {
295			t.Fatalf("Expected %s to exist but doesn't", readmeFullPath)
296		}
297		t.Fatal(err)
298	} else if info.Mode().Perm() != 0777 {
299		t.Fatalf("Expected %s to have 0777 mode it but has %o", readmeFullPath, info.Mode().Perm())
300	}
301
302}
303
304func TestCleanJoin(t *testing.T) {
305	for i, fixture := range []struct {
306		path        string
307		expect      string
308		expectError bool
309	}{
310		{"foo/bar.txt", "/tmp/foo/bar.txt", false},
311		{"/foo/bar.txt", "", true},
312		{"./foo/bar.txt", "/tmp/foo/bar.txt", false},
313		{"./././././foo/bar.txt", "/tmp/foo/bar.txt", false},
314		{"../../../../foo/bar.txt", "", true},
315		{"foo/../../../../bar.txt", "", true},
316		{"c:/foo/bar.txt", "/tmp/c:/foo/bar.txt", true},
317		{"foo\\bar.txt", "/tmp/foo/bar.txt", false},
318		{"c:\\foo\\bar.txt", "", true},
319	} {
320		out, err := cleanJoin("/tmp", fixture.path)
321		if err != nil {
322			if !fixture.expectError {
323				t.Errorf("Test %d: Path was not cleaned: %s", i, err)
324			}
325			continue
326		}
327		if fixture.expect != out {
328			t.Errorf("Test %d: Expected %q but got %q", i, fixture.expect, out)
329		}
330	}
331
332}
333
334func TestMediaTypeToExtension(t *testing.T) {
335
336	for mt, shouldPass := range map[string]bool{
337		"":                   false,
338		"application/gzip":   true,
339		"application/x-gzip": true,
340		"application/x-tgz":  true,
341		"application/x-gtar": true,
342		"application/json":   false,
343	} {
344		ext, ok := mediaTypeToExtension(mt)
345		if ok != shouldPass {
346			t.Errorf("Media type %q failed test", mt)
347		}
348		if shouldPass && ext == "" {
349			t.Errorf("Expected an extension but got empty string")
350		}
351		if !shouldPass && len(ext) != 0 {
352			t.Error("Expected extension to be empty for unrecognized type")
353		}
354	}
355}
356