diff --git a/pkg/plugin/installer/http_installer.go b/pkg/plugin/installer/http_installer.go index cbe877777..203f038f2 100644 --- a/pkg/plugin/installer/http_installer.go +++ b/pkg/plugin/installer/http_installer.go @@ -16,188 +16,192 @@ limitations under the License. package installer // import "k8s.io/helm/pkg/plugin/installer" import ( - "k8s.io/helm/pkg/helm/helmpath" - "path/filepath" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/plugin/cache" - "compress/gzip" - "archive/tar" - "io" - "os" - "fmt" - "strings" - "bytes" - "regexp" + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "io" + "k8s.io/helm/pkg/getter" + "k8s.io/helm/pkg/helm/environment" + "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/plugin/cache" + "os" + "path/filepath" + "regexp" + "strings" ) -type HttpInstaller struct { - CacheDir string - PluginName string - base - extractor Extractor - getter getter.Getter +// HTTPInstaller installs plugins from an archive served by a web server. +type HTTPInstaller struct { + CacheDir string + PluginName string + base + extractor Extractor + getter getter.Getter } -type TarGzExtractor struct {} +// TarGzExtractor extracts gzip compressed tar archives +type TarGzExtractor struct{} +// Extractor provides an interface for extracting archives type Extractor interface { - Extract(buffer *bytes.Buffer, targetDir string) error + Extract(buffer *bytes.Buffer, targetDir string) error } -var Extractors = map[string]Extractor { - ".tar.gz": &TarGzExtractor{}, - ".tgz": &TarGzExtractor{}, +// Extractors contains a map of suffixes and matching implementations of extractor to return +var Extractors = map[string]Extractor{ + ".tar.gz": &TarGzExtractor{}, + ".tgz": &TarGzExtractor{}, } // NewExtractor creates a new extractor matching the source file name func NewExtractor(source string) (Extractor, error) { - for suffix, extractor := range Extractors { - if strings.HasSuffix(source, suffix) { - return extractor, nil - } - } - return nil, fmt.Errorf("no extractor implemented yet for %s", source) + for suffix, extractor := range Extractors { + if strings.HasSuffix(source, suffix) { + return extractor, nil + } + } + return nil, fmt.Errorf("no extractor implemented yet for %s", source) } -// NewHttpInstaller creates a new HttpInstaller. -func NewHttpInstaller(source string, home helmpath.Home) (*HttpInstaller, error) { - - key, err := cache.Key(source) - if err != nil { - return nil, err - } - - extractor, err := NewExtractor(source) - if err != nil { - return nil, err - } - - getConstructor, err := getter.ByScheme("http", environment.EnvSettings{}) - if err != nil { - return nil, err - } - - get, err := getConstructor.New(source,"", "", "") - if err != nil { - return nil, err - } - - i := &HttpInstaller{ - CacheDir: home.Path("cache", "plugins", key), - PluginName: stripPluginName(filepath.Base(source)), - base: newBase(source, home), - extractor: extractor, - getter: get, - } - return i, nil +// NewHTTPInstaller creates a new HttpInstaller. +func NewHTTPInstaller(source string, home helmpath.Home) (*HTTPInstaller, error) { + + key, err := cache.Key(source) + if err != nil { + return nil, err + } + + extractor, err := NewExtractor(source) + if err != nil { + return nil, err + } + + getConstructor, err := getter.ByScheme("http", environment.EnvSettings{}) + if err != nil { + return nil, err + } + + get, err := getConstructor.New(source, "", "", "") + if err != nil { + return nil, err + } + + i := &HTTPInstaller{ + CacheDir: home.Path("cache", "plugins", key), + PluginName: stripPluginName(filepath.Base(source)), + base: newBase(source, home), + extractor: extractor, + getter: get, + } + return i, nil } // helper that relies on some sort of convention for plugin name (plugin-name-) func stripPluginName(name string) string { - var strippedName string - for suffix := range Extractors { - if strings.HasSuffix(name, suffix) { - strippedName = strings.TrimSuffix(name, suffix) - break - } - } - re := regexp.MustCompile(`(.*)-[0-9]+\..*`) - return re.ReplaceAllString(strippedName, `$1`) + var strippedName string + for suffix := range Extractors { + if strings.HasSuffix(name, suffix) { + strippedName = strings.TrimSuffix(name, suffix) + break + } + } + re := regexp.MustCompile(`(.*)-[0-9]+\..*`) + return re.ReplaceAllString(strippedName, `$1`) } -// Install() downloads and extracts the tarball into the cache directory and creates a symlink to the plugin directory in $HELM_HOME. +// Install downloads and extracts the tarball into the cache directory and creates a symlink to the plugin directory in $HELM_HOME. // // Implements Installer. -func (i *HttpInstaller) Install() error { +func (i *HTTPInstaller) Install() error { - pluginData, err := i.getter.Get(i.Source) - if err != nil { - return err - } + pluginData, err := i.getter.Get(i.Source) + if err != nil { + return err + } - err = i.extractor.Extract(pluginData, i.CacheDir) - if err != nil { - return err - } + err = i.extractor.Extract(pluginData, i.CacheDir) + if err != nil { + return err + } - if !isPlugin(i.CacheDir) { - return ErrMissingMetadata - } + if !isPlugin(i.CacheDir) { + return ErrMissingMetadata + } - src, err := filepath.Abs(i.CacheDir) - if err != nil { - return err - } + src, err := filepath.Abs(i.CacheDir) + if err != nil { + return err + } - return i.link(src) + return i.link(src) } // Update updates a local repository // Not implemented for now since tarball most likely will be packaged by version -func (i *HttpInstaller) Update() error { - return fmt.Errorf("method Update() not implemented for HttpInstaller") +func (i *HTTPInstaller) Update() error { + return fmt.Errorf("method Update() not implemented for HttpInstaller") } // Override link because we want to use HttpInstaller.Path() not base.Path() -func (i *HttpInstaller) link(from string) error { - debug("symlinking %s to %s", from, i.Path()) - return os.Symlink(from, i.Path()) +func (i *HTTPInstaller) link(from string) error { + debug("symlinking %s to %s", from, i.Path()) + return os.Symlink(from, i.Path()) } -// Override Path() because we want to join on the plugin name not the file name -func (i HttpInstaller) Path() string { - if i.base.Source == "" { - return "" - } - return filepath.Join(i.base.HelmHome.Plugins(), i.PluginName) +// Path is overridden because we want to join on the plugin name not the file name +func (i HTTPInstaller) Path() string { + if i.base.Source == "" { + return "" + } + return filepath.Join(i.base.HelmHome.Plugins(), i.PluginName) } -// Extracts tar.gz archive +// Extract extracts compressed archives // // Implements Extractor. func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error { - uncompressedStream, err := gzip.NewReader(buffer) - if err != nil { - return err - } - - tarReader := tar.NewReader(uncompressedStream) - - os.MkdirAll(targetDir, 0755) - - for true { - header, err := tarReader.Next() - - if err == io.EOF { - break - } - - if err != nil { - return err - } - - path := filepath.Join(targetDir, header.Name) - - switch header.Typeflag { - case tar.TypeDir: - if err := os.Mkdir(path, 0755); err != nil { - return err - } - case tar.TypeReg: - outFile, err := os.Create(path) - if err != nil { - return err - } - defer outFile.Close() - if _, err := io.Copy(outFile, tarReader); err != nil { - return err - } - default: - return fmt.Errorf("unknown type: %s in %s", header.Typeflag, header.Name) - } - } - - return nil - -} \ No newline at end of file + uncompressedStream, err := gzip.NewReader(buffer) + if err != nil { + return err + } + + tarReader := tar.NewReader(uncompressedStream) + + os.MkdirAll(targetDir, 0755) + + for true { + header, err := tarReader.Next() + + if err == io.EOF { + break + } + + if err != nil { + return err + } + + path := filepath.Join(targetDir, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + if err := os.Mkdir(path, 0755); err != nil { + return err + } + case tar.TypeReg: + outFile, err := os.Create(path) + if err != nil { + return err + } + defer outFile.Close() + if _, err := io.Copy(outFile, tarReader); err != nil { + return err + } + default: + return fmt.Errorf("unknown type: %b in %s", header.Typeflag, header.Name) + } + } + + return nil + +} diff --git a/pkg/plugin/installer/http_installer_test.go b/pkg/plugin/installer/http_installer_test.go index 730c33af5..3c061e68d 100644 --- a/pkg/plugin/installer/http_installer_test.go +++ b/pkg/plugin/installer/http_installer_test.go @@ -16,88 +16,88 @@ limitations under the License. package installer // import "k8s.io/helm/pkg/plugin/installer" import ( - "testing" - "io/ioutil" - "os" - "k8s.io/helm/pkg/helm/helmpath" - "bytes" - "encoding/base64" + "bytes" + "encoding/base64" + "io/ioutil" + "k8s.io/helm/pkg/helm/helmpath" + "os" + "testing" ) -var _ Installer = new(HttpInstaller) +var _ Installer = new(HTTPInstaller) // Fake http client -type TestHttpGetter struct { - MockResponse *bytes.Buffer +type TestHTTPGetter struct { + MockResponse *bytes.Buffer } -func (t *TestHttpGetter) Get(href string) (*bytes.Buffer, error) { return t.MockResponse, nil } +func (t *TestHTTPGetter) Get(href string) (*bytes.Buffer, error) { return t.MockResponse, nil } // Fake plugin tarball data var fakePluginB64 = "H4sIAKRj51kAA+3UX0vCUBgGcC9jn+Iwuk3Peza3GeyiUlJQkcogCOzgli7dJm4TvYk+a5+k479UqquUCJ/fLs549sLO2TnvWnJa9aXnjwujYdYLovxMhsPcfnHOLdNkOXthM/IVQQYjg2yyLLJ4kXGhLp5j0z3P41tZksqxmspL3B/O+j/XtZu1y8rdYzkOZRCxduKPk53ny6Wwz/GfIIf1As8lxzGJSmoHNLJZphKHG4YpTCE0wVk3DULfpSJ3DMMqkj3P5JfMYLdX1Vr9Ie/5E5cstcdC8K04iGLX5HaJuKpWL17F0TCIBi5pf/0pjtLhun5j3f9v6r7wfnI/H0eNp9d1/5P6Gez0vzo7wsoxfrAZbTny/o9k6J8z/VkO/LPlWdC1iVpbEEcq5nmeJ13LEtmbV0k2r2PrOs9PuuNglC5rL1Y5S/syXRQmutaNw1BGnnp8Wq3UG51WvX1da3bKtZtCN/R09DwAAAAAAAAAAAAAAAAAAADAb30AoMczDwAoAAA=" func TestStripName(t *testing.T) { - if stripPluginName("fake-plugin-0.0.1.tar.gz") != "fake-plugin" { - t.Errorf("name does not match expected value") - } - if stripPluginName("fake-plugin-0.0.1.tgz") != "fake-plugin" { - t.Errorf("name does not match expected value") - } - if stripPluginName("fake-plugin.tgz") != "fake-plugin" { - t.Errorf("name does not match expected value") - } - if stripPluginName("fake-plugin.tar.gz") != "fake-plugin" { - t.Errorf("name does not match expected value") - } + if stripPluginName("fake-plugin-0.0.1.tar.gz") != "fake-plugin" { + t.Errorf("name does not match expected value") + } + if stripPluginName("fake-plugin-0.0.1.tgz") != "fake-plugin" { + t.Errorf("name does not match expected value") + } + if stripPluginName("fake-plugin.tgz") != "fake-plugin" { + t.Errorf("name does not match expected value") + } + if stripPluginName("fake-plugin.tar.gz") != "fake-plugin" { + t.Errorf("name does not match expected value") + } } -func TestHttpInstaller(t *testing.T) { - source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz" - hh, err := ioutil.TempDir("", "helm-home-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(hh) - - home := helmpath.Home(hh) - if err := os.MkdirAll(home.Plugins(), 0755); err != nil { - t.Fatalf("Could not create %s: %s", home.Plugins(), err) - } - - i, err := NewForSource(source, "0.0.1", home) - if err != nil { - t.Errorf("unexpected error: %s", err) - } - - // ensure a HttpInstaller was returned - httpInstaller, ok := i.(*HttpInstaller) - if !ok { - t.Error("expected a HttpInstaller") - } - - // inject fake http client responding with minimal plugin tarball - mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64) - if err != nil { - t.Fatalf("Could not decode fake tgz plugin: %s", err) - } - - httpInstaller.getter = &TestHttpGetter{ - MockResponse: bytes.NewBuffer(mockTgz), - } - - // install the plugin - if err := Install(i); err != nil { - t.Error(err) - } - if i.Path() != home.Path("plugins", "fake-plugin") { - t.Errorf("expected path '$HELM_HOME/plugins/fake-plugin', got %q", i.Path()) - } - - // Install again to test plugin exists error - if err := Install(i); err == nil { - t.Error("expected error for plugin exists, got none") - } else if err.Error() != "plugin already exists" { - t.Errorf("expected error for plugin exists, got (%v)", err) - } - -} \ No newline at end of file +func TestHTTPInstaller(t *testing.T) { + source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz" + hh, err := ioutil.TempDir("", "helm-home-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(hh) + + home := helmpath.Home(hh) + if err := os.MkdirAll(home.Plugins(), 0755); err != nil { + t.Fatalf("Could not create %s: %s", home.Plugins(), err) + } + + i, err := NewForSource(source, "0.0.1", home) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + // ensure a HttpInstaller was returned + httpInstaller, ok := i.(*HTTPInstaller) + if !ok { + t.Error("expected a HttpInstaller") + } + + // inject fake http client responding with minimal plugin tarball + mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64) + if err != nil { + t.Fatalf("Could not decode fake tgz plugin: %s", err) + } + + httpInstaller.getter = &TestHTTPGetter{ + MockResponse: bytes.NewBuffer(mockTgz), + } + + // install the plugin + if err := Install(i); err != nil { + t.Error(err) + } + if i.Path() != home.Path("plugins", "fake-plugin") { + t.Errorf("expected path '$HELM_HOME/plugins/fake-plugin', got %q", i.Path()) + } + + // Install again to test plugin exists error + if err := Install(i); err == nil { + t.Error("expected error for plugin exists, got none") + } else if err.Error() != "plugin already exists" { + t.Errorf("expected error for plugin exists, got (%v)", err) + } + +} diff --git a/pkg/plugin/installer/installer.go b/pkg/plugin/installer/installer.go index b82e2f8b2..10433f4cd 100644 --- a/pkg/plugin/installer/installer.go +++ b/pkg/plugin/installer/installer.go @@ -69,8 +69,8 @@ func NewForSource(source, version string, home helmpath.Home) (Installer, error) // Check if source is a local directory if isLocalReference(source) { return NewLocalInstaller(source, home) - } else if isRemoteHttpArchive(source) { - return NewHttpInstaller(source, home) + } else if isRemoteHTTPArchive(source) { + return NewHTTPInstaller(source, home) } return NewVCSInstaller(source, version, home) } @@ -90,10 +90,10 @@ func isLocalReference(source string) bool { return err == nil } -// isRemoteHttpArchive checks if the source is a http/https url and is an archive -func isRemoteHttpArchive(source string) bool { +// isRemoteHTTPArchive checks if the source is a http/https url and is an archive +func isRemoteHTTPArchive(source string) bool { if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { - for suffix, _ := range Extractors { + for suffix := range Extractors { if strings.HasSuffix(source, suffix) { return true }