...

Source file src/helm.sh/helm/v3/pkg/plugin/installer/http_installer_test.go

Documentation: helm.sh/helm/v3/pkg/plugin/installer

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

View as plain text