diff --git a/docs/plugins.md b/docs/plugins.md index 3087d1b39..15f6a69f2 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -37,8 +37,11 @@ Plugins are installed using the `$ helm plugin install ` command. You $ helm plugin install https://github.com/technosophos/helm-template ``` -If you have a plugin tar distribution, simply untar the plugin into the -`$(helm home)/plugins` directory. +If you have a plugin tar distribution downloaded, you can install it directly: + +```console +$ helm plugin install helm-template.tgz +``` You can also install tarball plugins directly from url by issuing `helm plugin install http://domain/path/to/plugin.tar.gz` @@ -118,6 +121,16 @@ There are some strategies for working with plugin commands: Helm will use `usage` and `description` for `helm help` and `helm help myplugin`, but will not handle `helm myplugin --help`. +## Packaging + +If the primary means of plugin distribution will be via a VCS URI, then packaging is not +necessary and this section can be skipped. If however plugin distribution will be via +HTTP or a local file, then it is recommended to version and package the plugin. Helm +supports packages in a gzip compressed tarball format with file extensions of either +`.tgz` or `.tar.gz`. Additionally, the tarball can either contain a directory or forgo +one (i.e a [tarbomb](https://en.wikipedia.org/wiki/Tarbomb)). The package should follow a +naming convention of `-.tgz`. + ## Downloader Plugins By default, Helm is able to fetch Charts using HTTP/S. As of Helm 2.4.0, plugins can have a special capability to download Charts from arbitrary sources. diff --git a/pkg/plugin/installer/extractor.go b/pkg/plugin/installer/extractor.go new file mode 100644 index 000000000..2180d719d --- /dev/null +++ b/pkg/plugin/installer/extractor.go @@ -0,0 +1,132 @@ +/* +Copyright The Helm Authors. +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 ( + "archive/tar" + "bytes" // TarGzExtractor extracts gzip compressed tar archives + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + + fp "github.com/cyphar/filepath-securejoin" +) + +// Extractor provides an interface for extracting archives +type Extractor interface { + Extract(buffer *bytes.Buffer, targetDir string) (string, error) +} + +// TarGzExtractor extracts GZip compressed tar archives +type TarGzExtractor struct { + extension string +} + +// 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) +} + +// StripPluginName 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`) +} + +// Extract extracts compressed archives +// +// Implements Extractor. Returns the directory where the plugin.yaml is located or an error +func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) (string, error) { + uncompressedStream, err := gzip.NewReader(buffer) + if err != nil { + return "", err + } + defer uncompressedStream.Close() + + tarReader := tar.NewReader(uncompressedStream) + + err = os.MkdirAll(targetDir, 0755) + if err != nil { + return "", err + } + + pluginDir := targetDir + + for true { + header, err := tarReader.Next() + + if err == io.EOF { + break + } + + if err != nil { + return "", err + } + + path, err := fp.SecureJoin(targetDir, header.Name) + if err != nil { + return "", err + } + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(path, 0755); err != nil { + return "", err + } + case tar.TypeReg: + outFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + if err != nil { + return "", err + } + if _, err := io.Copy(outFile, tarReader); err != nil { + outFile.Close() + return "", err + } + outFile.Close() // Manually close since defering in a loop may cause a resource leak + + path, file := filepath.Split(outFile.Name()) + if file == pluginFile { + pluginDir = path + } + default: + return "", fmt.Errorf("unknown type: %b in %s", header.Typeflag, header.Name) + } + } + + return pluginDir, nil +} diff --git a/pkg/plugin/installer/extractor_test.go b/pkg/plugin/installer/extractor_test.go new file mode 100644 index 000000000..5a3068627 --- /dev/null +++ b/pkg/plugin/installer/extractor_test.go @@ -0,0 +1,183 @@ +/* +Copyright The Helm Authors. +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 ( + "archive/tar" + "bytes" + "compress/gzip" + "encoding/base64" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "k8s.io/helm/pkg/helm/helmpath" +) + +// 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=" + +var fakePluginWithDirB64 = "H4sICCOnslwAA2Zha2UtcGx1Z2luLTAuMC4xLnRhcgDtks1qwzAQhHP2UwjRa52VrR/IrZDSBtJSCj0HYUupSWQbyw7k7bux3eKLe0oIIf4uA6tB2t2R1TvzWO6bbZbPZxcCAJQQpFUFrULEO+0hLOaCiyiOeEyAcQ7xjIhLNTSk8bWusBWbVcYnRWlGfGiz9p97+jn+9Eawg/w7CY/a7c/6Bu5Dcj6eP1MK82eKSckkCMxfCoX5w1m7GOHO88+1MwtCB9+ABgdT+azIsQwhhIwGjdfbk0uTk4/8+lJcWJWVded96uuk/tZ1a/Q0SArndJ7i8cPr8/pt87H+elm9b5arz3niUhpce/yJiYmJu+UHTs7l6wAKAAA=" + +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 TestExtract(t *testing.T) { + //create a temp home + 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) + } + + cacheDir := filepath.Join(home.Cache(), "plugins", "plugin-key") + if err := os.MkdirAll(cacheDir, 0755); err != nil { + t.Fatalf("Could not create %s: %s", cacheDir, err) + } + + //{"plugin.yaml", "plugin metadata up in here"}, + //{"README.md", "so you know what's upp"}, + //{"script.sh", "echo script"}, + + var tarbuf bytes.Buffer + tw := tar.NewWriter(&tarbuf) + var files = []struct { + Name, Body string + }{ + {"../../plugin.yaml", "sneaky plugin metadata"}, + {"README.md", "some text"}, + } + for _, file := range files { + hdr := &tar.Header{ + Name: file.Name, + Typeflag: tar.TypeReg, + Mode: 0600, + Size: int64(len(file.Body)), + } + if err := tw.WriteHeader(hdr); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte(file.Body)); err != nil { + t.Fatal(err) + } + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + if _, err := gz.Write(tarbuf.Bytes()); err != nil { + t.Fatal(err) + } + gz.Close() + + source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tgz" + extr, err := NewExtractor(source) + if err != nil { + t.Fatal(err) + } + + if _, err := extr.Extract(&buf, cacheDir); err != nil { + t.Errorf("Did not expect error but got error: %v", err) + } + + pluginYAMLFullPath := filepath.Join(cacheDir, "plugin.yaml") + if _, err := os.Stat(pluginYAMLFullPath); err != nil { + if os.IsNotExist(err) { + t.Errorf("Expected %s to exist but doesn't", pluginYAMLFullPath) + } else { + t.Error(err) + } + } + + readmeFullPath := filepath.Join(cacheDir, "README.md") + if _, err := os.Stat(readmeFullPath); err != nil { + if os.IsNotExist(err) { + t.Errorf("Expected %s to exist but doesn't", readmeFullPath) + } else { + t.Error(err) + } + } + +} + +func TestExtractWithDir(t *testing.T) { + //create a temp home + 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) + } + + cacheDir := filepath.Join(home.Cache(), "plugins", "fake-plugin") + if err := os.MkdirAll(cacheDir, 0755); err != nil { + t.Fatalf("Could not create %s: %s", cacheDir, err) + } + + source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tgz" + extr, err := NewExtractor(source) + if err != nil { + t.Fatal(err) + } + + // inject fake http client responding with minimal plugin tarball + buf, err := base64.StdEncoding.DecodeString(fakePluginWithDirB64) + if err != nil { + t.Fatalf("Could not decode fake tgz plugin: %s", err) + } + + pluginDir, err := extr.Extract(bytes.NewBuffer(buf), cacheDir) + if err != nil { + t.Errorf("Did not expect error but got error: %v", err) + } + + if filepath.Clean(pluginDir) != filepath.Join(cacheDir, "fake-plugin") { + t.Errorf("Did not detect plugin.yaml in sub-directory") + } + + pluginYAMLFullPath := filepath.Join(pluginDir, "plugin.yaml") + if _, err := os.Stat(pluginYAMLFullPath); err != nil { + if os.IsNotExist(err) { + t.Errorf("Expected %s to exist but doesn't", pluginYAMLFullPath) + } else { + t.Error(err) + } + } +} diff --git a/pkg/plugin/installer/http_installer.go b/pkg/plugin/installer/http_installer.go index fd58b88ca..bafc2745e 100644 --- a/pkg/plugin/installer/http_installer.go +++ b/pkg/plugin/installer/http_installer.go @@ -16,17 +16,9 @@ limitations under the License. package installer // import "k8s.io/helm/pkg/plugin/installer" import ( - "archive/tar" - "bytes" - "compress/gzip" "fmt" - "io" "os" "path/filepath" - "regexp" - "strings" - - fp "github.com/cyphar/filepath-securejoin" "k8s.io/helm/pkg/getter" "k8s.io/helm/pkg/helm/environment" @@ -43,30 +35,6 @@ type HTTPInstaller struct { getter getter.Getter } -// 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 -} - -// 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) -} - // NewHTTPInstaller creates a new HttpInstaller. func NewHTTPInstaller(source string, home helmpath.Home) (*HTTPInstaller, error) { @@ -100,19 +68,6 @@ func NewHTTPInstaller(source string, home helmpath.Home) (*HTTPInstaller, error) 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. @@ -123,10 +78,11 @@ func (i *HTTPInstaller) Install() error { return err } - err = i.extractor.Extract(pluginData, i.CacheDir) + pluginDir, err := i.extractor.Extract(pluginData, i.CacheDir) if err != nil { return err } + i.CacheDir = pluginDir // plugin.yaml could be in a sub-folder if !isPlugin(i.CacheDir) { return ErrMissingMetadata @@ -159,56 +115,3 @@ func (i HTTPInstaller) Path() string { } return filepath.Join(i.base.HelmHome.Plugins(), i.PluginName) } - -// 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, err := fp.SecureJoin(targetDir, header.Name) - if err != nil { - return err - } - - 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 - } - if _, err := io.Copy(outFile, tarReader); err != nil { - outFile.Close() - return err - } - outFile.Close() - 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 73af75e8c..10110eb2a 100644 --- a/pkg/plugin/installer/http_installer_test.go +++ b/pkg/plugin/installer/http_installer_test.go @@ -16,16 +16,14 @@ limitations under the License. package installer // import "k8s.io/helm/pkg/plugin/installer" import ( - "archive/tar" "bytes" - "compress/gzip" "encoding/base64" "fmt" "io/ioutil" - "k8s.io/helm/pkg/helm/helmpath" "os" - "path/filepath" "testing" + + "k8s.io/helm/pkg/helm/helmpath" ) var _ Installer = new(HTTPInstaller) @@ -38,24 +36,6 @@ type TestHTTPGetter struct { 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=" - -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-") @@ -168,7 +148,7 @@ func TestHTTPInstallerUpdate(t *testing.T) { } // inject fake http client responding with minimal plugin tarball - mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64) + mockTgz, err := base64.StdEncoding.DecodeString(fakePluginWithDirB64) if err != nil { t.Fatalf("Could not decode fake tgz plugin: %s", err) } @@ -190,88 +170,3 @@ func TestHTTPInstallerUpdate(t *testing.T) { t.Error("update method not implemented for http installer") } } - -func TestExtract(t *testing.T) { - //create a temp home - 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) - } - - cacheDir := filepath.Join(home.Cache(), "plugins", "plugin-key") - if err := os.MkdirAll(cacheDir, 0755); err != nil { - t.Fatalf("Could not create %s: %s", cacheDir, err) - } - - //{"plugin.yaml", "plugin metadata up in here"}, - //{"README.md", "so you know what's upp"}, - //{"script.sh", "echo script"}, - - var tarbuf bytes.Buffer - tw := tar.NewWriter(&tarbuf) - var files = []struct { - Name, Body string - }{ - {"../../plugin.yaml", "sneaky plugin metadata"}, - {"README.md", "some text"}, - } - for _, file := range files { - hdr := &tar.Header{ - Name: file.Name, - Typeflag: tar.TypeReg, - Mode: 0600, - Size: int64(len(file.Body)), - } - if err := tw.WriteHeader(hdr); err != nil { - t.Fatal(err) - } - if _, err := tw.Write([]byte(file.Body)); err != nil { - t.Fatal(err) - } - } - if err := tw.Close(); err != nil { - t.Fatal(err) - } - - var buf bytes.Buffer - gz := gzip.NewWriter(&buf) - if _, err := gz.Write(tarbuf.Bytes()); err != nil { - t.Fatal(err) - } - gz.Close() - - source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tgz" - extr, err := NewExtractor(source) - if err != nil { - t.Fatal(err) - } - - if err = extr.Extract(&buf, cacheDir); err != nil { - t.Errorf("Did not expect error but got error: %v", err) - } - - pluginYAMLFullPath := filepath.Join(cacheDir, "plugin.yaml") - if _, err := os.Stat(pluginYAMLFullPath); err != nil { - if os.IsNotExist(err) { - t.Errorf("Expected %s to exist but doesn't", pluginYAMLFullPath) - } else { - t.Error(err) - } - } - - readmeFullPath := filepath.Join(cacheDir, "README.md") - if _, err := os.Stat(readmeFullPath); err != nil { - if os.IsNotExist(err) { - t.Errorf("Expected %s to exist but doesn't", readmeFullPath) - } else { - t.Error(err) - } - } - -} diff --git a/pkg/plugin/installer/installer.go b/pkg/plugin/installer/installer.go index 76c751d50..452212c03 100644 --- a/pkg/plugin/installer/installer.go +++ b/pkg/plugin/installer/installer.go @@ -26,6 +26,10 @@ import ( "k8s.io/helm/pkg/helm/helmpath" ) +const ( + pluginFile = "plugin.yaml" +) + // ErrMissingMetadata indicates that plugin.yaml is missing. var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing") @@ -93,10 +97,8 @@ func isLocalReference(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 { - if strings.HasSuffix(source, suffix) { - return true - } + if _, err := NewExtractor(source); err == nil { + return true } } return false @@ -104,7 +106,7 @@ func isRemoteHTTPArchive(source string) bool { // isPlugin checks if the directory contains a plugin.yaml file. func isPlugin(dirname string) bool { - _, err := os.Stat(filepath.Join(dirname, "plugin.yaml")) + _, err := os.Stat(filepath.Join(dirname, pluginFile)) return err == nil } diff --git a/pkg/plugin/installer/local_installer.go b/pkg/plugin/installer/local_installer.go index f39086a6e..b1264eb70 100644 --- a/pkg/plugin/installer/local_installer.go +++ b/pkg/plugin/installer/local_installer.go @@ -16,15 +16,21 @@ limitations under the License. package installer // import "k8s.io/helm/pkg/plugin/installer" import ( + "bytes" "fmt" + "io/ioutil" "path/filepath" "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/plugin/cache" ) // LocalInstaller installs plugins from the filesystem. type LocalInstaller struct { base + CacheDir string + PluginName string + extractor Extractor } // NewLocalInstaller creates a new LocalInstaller. @@ -33,8 +39,19 @@ func NewLocalInstaller(source string, home helmpath.Home) (*LocalInstaller, erro if err != nil { return nil, fmt.Errorf("unable to get absolute path to plugin: %v", err) } + + key, err := cache.Key(source) + if err != nil { + return nil, err + } + + // Don't check error since extractor is optional + extractor, _ := NewExtractor(source) + i := &LocalInstaller{ - base: newBase(src, home), + base: newBase(src, home), + CacheDir: home.Path("cache", "plugins", key), + extractor: extractor, } return i, nil } @@ -43,6 +60,21 @@ func NewLocalInstaller(source string, home helmpath.Home) (*LocalInstaller, erro // // Implements Installer. func (i *LocalInstaller) Install() error { + if i.extractor != nil { + pluginData, err := ioutil.ReadFile(i.Source) + if err != nil { + return err + } + + pluginDir, err := i.extractor.Extract(bytes.NewBuffer(pluginData), i.CacheDir) + if err != nil { + return err + } + + i.PluginName = stripPluginName(filepath.Base(i.Source)) + i.Source = pluginDir + } + if !isPlugin(i.Source) { return ErrMissingMetadata } @@ -54,3 +86,14 @@ func (i *LocalInstaller) Update() error { debug("local repository is auto-updated") return nil } + +// Path is overridden because we want to join on the plugin name not the file name +func (i LocalInstaller) Path() string { + if i.base.Source == "" { + return "" + } + if i.PluginName != "" { + return filepath.Join(i.base.HelmHome.Plugins(), i.PluginName) + } + return filepath.Join(i.HelmHome.Plugins(), filepath.Base(i.Source)) +} diff --git a/pkg/plugin/installer/local_installer_test.go b/pkg/plugin/installer/local_installer_test.go index fb5fa2675..e5991c9ce 100644 --- a/pkg/plugin/installer/local_installer_test.go +++ b/pkg/plugin/installer/local_installer_test.go @@ -16,6 +16,7 @@ limitations under the License. package installer // import "k8s.io/helm/pkg/plugin/installer" import ( + "encoding/base64" "io/ioutil" "os" "path/filepath" @@ -54,6 +55,12 @@ func TestLocalInstaller(t *testing.T) { t.Errorf("unexpected error: %s", err) } + // ensure a LocalInstaller was returned + _, ok := i.(*LocalInstaller) + if !ok { + t.Error("expected a LocalInstaller") + } + if err := Install(i); err != nil { t.Error(err) } @@ -62,3 +69,52 @@ func TestLocalInstaller(t *testing.T) { t.Errorf("expected path '$HELM_HOME/plugins/helm-env', got %q", i.Path()) } } + +func TestLocalInstallerTgz(t *testing.T) { + 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) + } + + // Make a temp dir + tdir, err := ioutil.TempDir("", "helm-installer-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tdir) + + mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64) + if err != nil { + t.Fatalf("Could not decode fake tgz plugin: %s", err) + } + + source := filepath.Join(tdir, "fake-plugin-0.0.1.tgz") + if err := ioutil.WriteFile(source, mockTgz, 0644); err != nil { + t.Fatal(err) + } + + i, err := NewForSource(source, "", home) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + // ensure a LocalInstaller was returned + _, ok := i.(*LocalInstaller) + if !ok { + t.Error("expected a LocalInstaller") + } + + 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()) + } +}