Plugin: Support local tgz install & non-tarbombs

Signed-off-by: Steven Sheehy <ssheehy@firescope.com>
pull/5627/head
Steven Sheehy 7 years ago
parent 74209401d3
commit 83fedf66e1
No known key found for this signature in database
GPG Key ID: 14CCAB401CBE86B7

@ -37,8 +37,11 @@ Plugins are installed using the `$ helm plugin install <path|url>` 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 `<name>-<version>.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.

@ -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-<version>)
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
}

@ -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)
}
}
}

@ -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-<version>)
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
}

@ -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)
}
}
}

@ -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,18 +97,16 @@ 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) {
if _, err := NewExtractor(source); err == nil {
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"))
_, err := os.Stat(filepath.Join(dirname, pluginFile))
return err == nil
}

@ -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),
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))
}

@ -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())
}
}

Loading…
Cancel
Save