From d43d5ab4529a80cb8944762ab8dd15d111af6df6 Mon Sep 17 00:00:00 2001 From: Johan Lyheden Date: Thu, 19 Oct 2017 09:38:11 +0200 Subject: [PATCH 1/3] Add plugin installer from http archive --- pkg/plugin/installer/http_installer.go | 203 ++++++++++++++++++++ pkg/plugin/installer/http_installer_test.go | 103 ++++++++++ pkg/plugin/installer/installer.go | 15 ++ 3 files changed, 321 insertions(+) create mode 100644 pkg/plugin/installer/http_installer.go create mode 100644 pkg/plugin/installer/http_installer_test.go diff --git a/pkg/plugin/installer/http_installer.go b/pkg/plugin/installer/http_installer.go new file mode 100644 index 000000000..cbe877777 --- /dev/null +++ b/pkg/plugin/installer/http_installer.go @@ -0,0 +1,203 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +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" +) + +type HttpInstaller struct { + CacheDir string + PluginName string + base + extractor Extractor + getter getter.Getter +} + +type TarGzExtractor struct {} + +type Extractor interface { + Extract(buffer *bytes.Buffer, targetDir string) error +} + +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) +} + +// 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`) +} + +// 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 { + + pluginData, err := i.getter.Get(i.Source) + if err != nil { + return err + } + + err = i.extractor.Extract(pluginData, i.CacheDir) + if err != nil { + return err + } + + if !isPlugin(i.CacheDir) { + return ErrMissingMetadata + } + + src, err := filepath.Abs(i.CacheDir) + if err != nil { + return err + } + + 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") +} + +// 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()) +} + +// 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) +} + +// Extracts tar.gz archive +// +// 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 diff --git a/pkg/plugin/installer/http_installer_test.go b/pkg/plugin/installer/http_installer_test.go new file mode 100644 index 000000000..730c33af5 --- /dev/null +++ b/pkg/plugin/installer/http_installer_test.go @@ -0,0 +1,103 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +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" +) + +var _ Installer = new(HttpInstaller) + +// Fake http client +type TestHttpGetter struct { + MockResponse *bytes.Buffer +} + +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") + } +} + +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 diff --git a/pkg/plugin/installer/installer.go b/pkg/plugin/installer/installer.go index 6ecec980a..b82e2f8b2 100644 --- a/pkg/plugin/installer/installer.go +++ b/pkg/plugin/installer/installer.go @@ -23,6 +23,7 @@ import ( "path/filepath" "k8s.io/helm/pkg/helm/helmpath" + "strings" ) // ErrMissingMetadata indicates that plugin.yaml is missing. @@ -68,6 +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) } return NewVCSInstaller(source, version, home) } @@ -87,6 +90,18 @@ 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 { + if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { + for suffix, _ := range Extractors { + if strings.HasSuffix(source, suffix) { + return true + } + } + } + return false +} + // isPlugin checks if the directory contains a plugin.yaml file. func isPlugin(dirname string) bool { _, err := os.Stat(filepath.Join(dirname, "plugin.yaml")) From f1a08adb4132b474115044a4de8784b8f6403a7f Mon Sep 17 00:00:00 2001 From: Johan Lyheden Date: Thu, 19 Oct 2017 13:35:18 +0200 Subject: [PATCH 2/3] Update to comply with linter rules and gofmt --- pkg/plugin/installer/http_installer.go | 292 ++++++++++---------- pkg/plugin/installer/http_installer_test.go | 144 +++++----- pkg/plugin/installer/installer.go | 10 +- 3 files changed, 225 insertions(+), 221 deletions(-) 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 } From 4831089500a967507465fd81a66a808fbaed2224 Mon Sep 17 00:00:00 2001 From: Johan Lyheden Date: Fri, 20 Oct 2017 10:28:40 +0200 Subject: [PATCH 3/3] Add tests to HTTPInstaller, update plugin documentation --- docs/plugins.md | 2 + pkg/plugin/installer/http_installer_test.go | 92 ++++++++++++++++++++- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index 1bee8bd56..de80491aa 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -41,6 +41,8 @@ $ cp -a myplugin/ $(helm home)/plugins/ If you have a plugin tar distribution, simply untar the plugin into the `$(helm home)/plugins` directory. +You can also install tarball plugins directly from url by issuing `helm plugin install http://domain/path/to/plugin.tar.gz` + ## Building Plugins In many ways, a plugin is similar to a chart. Each plugin has a top-level diff --git a/pkg/plugin/installer/http_installer_test.go b/pkg/plugin/installer/http_installer_test.go index 3c061e68d..ca1a71e3e 100644 --- a/pkg/plugin/installer/http_installer_test.go +++ b/pkg/plugin/installer/http_installer_test.go @@ -18,6 +18,7 @@ package installer // import "k8s.io/helm/pkg/plugin/installer" import ( "bytes" "encoding/base64" + "fmt" "io/ioutil" "k8s.io/helm/pkg/helm/helmpath" "os" @@ -29,9 +30,10 @@ var _ Installer = new(HTTPInstaller) // Fake http client type TestHTTPGetter struct { MockResponse *bytes.Buffer + MockError error } -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, t.MockError } // 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=" @@ -69,10 +71,10 @@ func TestHTTPInstaller(t *testing.T) { t.Errorf("unexpected error: %s", err) } - // ensure a HttpInstaller was returned + // ensure a HTTPInstaller was returned httpInstaller, ok := i.(*HTTPInstaller) if !ok { - t.Error("expected a HttpInstaller") + t.Error("expected a HTTPInstaller") } // inject fake http client responding with minimal plugin tarball @@ -101,3 +103,87 @@ func TestHTTPInstaller(t *testing.T) { } } + +func TestHTTPInstallerNonExistentVersion(t *testing.T) { + source := "https://repo.localdomain/plugins/fake-plugin-0.0.2.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.2", 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 error + httpInstaller.getter = &TestHTTPGetter{ + MockError: fmt.Errorf("failed to download plugin for some reason"), + } + + // attempt to install the plugin + if err := Install(i); err == nil { + t.Error("expected error from http client") + } + +} + +func TestHTTPInstallerUpdate(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 before updating + 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()) + } + + // Update plugin, should fail because it is not implemented + if err := Update(i); err == nil { + t.Error("update method not implemented for http installer") + } +}