Merge pull request from adamreese/feat/plugin-management

feat(helm): add plugin management commands
pull/2229/head
Adam Reese 8 years ago committed by GitHub
commit a19dee52c2

@ -136,12 +136,16 @@ func newRootCmd(out io.Writer) *cobra.Command {
addFlagsTLS(newStatusCmd(nil, out)), addFlagsTLS(newStatusCmd(nil, out)),
addFlagsTLS(newUpgradeCmd(nil, out)), addFlagsTLS(newUpgradeCmd(nil, out)),
addFlagsTLS(newReleaseTestCmd(nil, out)),
addFlagsTLS(newResetCmd(nil, out)),
addFlagsTLS(newVersionCmd(nil, out)),
newCompletionCmd(out), newCompletionCmd(out),
newHomeCmd(out), newHomeCmd(out),
newInitCmd(out), newInitCmd(out),
addFlagsTLS(newResetCmd(nil, out)), newResetCmd(nil, out),
addFlagsTLS(newVersionCmd(nil, out)), newVersionCmd(nil, out),
addFlagsTLS(newReleaseTestCmd(nil, out)), newReleaseTestCmd(nil, out),
newPluginCmd(out),
// Hidden documentation generator command: 'helm docs' // Hidden documentation generator command: 'helm docs'
newDocsCmd(out), newDocsCmd(out),

@ -31,6 +31,13 @@ import (
const pluginEnvVar = "HELM_PLUGIN" const pluginEnvVar = "HELM_PLUGIN"
func pluginDirs(home helmpath.Home) string {
if dirs := os.Getenv(pluginEnvVar); dirs != "" {
return dirs
}
return home.Plugins()
}
// loadPlugins loads plugins into the command list. // loadPlugins loads plugins into the command list.
// //
// This follows a different pattern than the other commands because it has // This follows a different pattern than the other commands because it has
@ -43,11 +50,7 @@ func loadPlugins(baseCmd *cobra.Command, home helmpath.Home, out io.Writer) {
return return
} }
plugdirs := os.Getenv(pluginEnvVar) plugdirs := pluginDirs(home)
if plugdirs == "" {
plugdirs = home.Plugins()
}
found, err := findPlugins(plugdirs) found, err := findPlugins(plugdirs)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err) fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err)

@ -0,0 +1,72 @@
/*
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 main
import (
"fmt"
"io"
"os"
"os/exec"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/plugin"
"github.com/spf13/cobra"
)
const pluginHelp = `
Manage client-side Helm plugins.
`
func newPluginCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "plugin",
Short: "add, list, or remove Helm plugins",
Long: pluginHelp,
}
cmd.AddCommand(
newPluginInstallCmd(out),
newPluginListCmd(out),
newPluginRemoveCmd(out),
)
return cmd
}
// runHook will execute a plugin hook.
func runHook(p *plugin.Plugin, event string, home helmpath.Home) error {
hook := p.Metadata.Hooks.Get(event)
if hook == "" {
return nil
}
prog := exec.Command("sh", "-c", hook)
// TODO make this work on windows
// I think its ... ¯\_(ツ)_/¯
// prog := exec.Command("cmd", "/C", p.Metadata.Hooks.Install())
debug("running %s hook: %s", event, prog)
setupEnv(p.Metadata.Name, p.Dir, home.Plugins(), home)
prog.Stdout, prog.Stderr = os.Stdout, os.Stderr
if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
os.Stderr.Write(eerr.Stderr)
return fmt.Errorf("plugin %s hook for %q exited with error", event, p.Metadata.Name)
}
return err
}
return nil
}

@ -0,0 +1,84 @@
/*
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 main
import (
"fmt"
"io"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/plugin"
"k8s.io/helm/pkg/plugin/installer"
"github.com/spf13/cobra"
)
type pluginInstallCmd struct {
source string
version string
home helmpath.Home
out io.Writer
}
func newPluginInstallCmd(out io.Writer) *cobra.Command {
pcmd := &pluginInstallCmd{out: out}
cmd := &cobra.Command{
Use: "install [options] <path|url>...",
Short: "install one or more Helm plugins",
PreRunE: func(cmd *cobra.Command, args []string) error {
return pcmd.complete(args)
},
RunE: func(cmd *cobra.Command, args []string) error {
return pcmd.run()
},
}
cmd.Flags().StringVar(&pcmd.version, "version", "", "specify a version constraint. If this is not specified, the latest version is installed")
return cmd
}
func (pcmd *pluginInstallCmd) complete(args []string) error {
if err := checkArgsLength(len(args), "plugin"); err != nil {
return err
}
pcmd.source = args[0]
pcmd.home = helmpath.Home(homePath())
return nil
}
func (pcmd *pluginInstallCmd) run() error {
installer.Debug = flagDebug
i, err := installer.NewForSource(pcmd.source, pcmd.version, pcmd.home)
if err != nil {
return err
}
if err := installer.Install(i); err != nil {
return err
}
debug("loading plugin from %s", i.Path())
p, err := plugin.LoadDir(i.Path())
if err != nil {
return err
}
if err := runHook(p, plugin.Install, pcmd.home); err != nil {
return err
}
fmt.Fprintf(pcmd.out, "Installed plugin: %s\n", p.Metadata.Name)
return nil
}

@ -0,0 +1,63 @@
/*
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 main
import (
"fmt"
"io"
"k8s.io/helm/pkg/helm/helmpath"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
)
type pluginListCmd struct {
home helmpath.Home
out io.Writer
}
func newPluginListCmd(out io.Writer) *cobra.Command {
pcmd := &pluginListCmd{out: out}
cmd := &cobra.Command{
Use: "list",
Short: "list installed Helm plugins",
RunE: func(cmd *cobra.Command, args []string) error {
pcmd.home = helmpath.Home(homePath())
return pcmd.run()
},
}
return cmd
}
func (pcmd *pluginListCmd) run() error {
plugdirs := pluginDirs(pcmd.home)
debug("pluginDirs: %s", plugdirs)
plugins, err := findPlugins(plugdirs)
if err != nil {
return err
}
table := uitable.New()
table.AddRow("NAME", "VERSION", "DESCRIPTION")
for _, p := range plugins {
table.AddRow(p.Metadata.Name, p.Metadata.Version, p.Metadata.Description)
}
fmt.Fprintln(pcmd.out, table)
return nil
}

@ -0,0 +1,92 @@
/*
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 main
import (
"fmt"
"io"
"os"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/plugin"
"github.com/spf13/cobra"
)
type pluginRemoveCmd struct {
names []string
home helmpath.Home
out io.Writer
}
func newPluginRemoveCmd(out io.Writer) *cobra.Command {
pcmd := &pluginRemoveCmd{out: out}
cmd := &cobra.Command{
Use: "remove <plugin>...",
Short: "remove one or more Helm plugins",
PreRunE: func(cmd *cobra.Command, args []string) error {
return pcmd.complete(args)
},
RunE: func(cmd *cobra.Command, args []string) error {
return pcmd.run()
},
}
return cmd
}
func (pcmd *pluginRemoveCmd) complete(args []string) error {
if err := checkArgsLength(len(args), "plugin"); err != nil {
return err
}
pcmd.names = args
pcmd.home = helmpath.Home(homePath())
return nil
}
func (pcmd *pluginRemoveCmd) run() error {
plugdirs := pluginDirs(pcmd.home)
debug("loading installed plugins from %s", plugdirs)
plugins, err := findPlugins(plugdirs)
if err != nil {
return err
}
for _, name := range pcmd.names {
if found := findPlugin(plugins, name); found != nil {
if err := removePlugin(found, pcmd.home); err != nil {
return err
}
fmt.Fprintf(pcmd.out, "Removed plugin: %s\n", name)
}
}
return nil
}
func removePlugin(p *plugin.Plugin, home helmpath.Home) error {
if err := os.Remove(p.Dir); err != nil {
return err
}
return runHook(p, plugin.Delete, home)
}
func findPlugin(plugins []*plugin.Plugin, name string) *plugin.Plugin {
for _, p := range plugins {
if p.Metadata.Name == name {
return p
}
}
return nil
}

@ -17,6 +17,7 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io" "io"
"text/template" "text/template"
"time" "time"
@ -72,3 +73,10 @@ func tpl(t string, vals map[string]interface{}, out io.Writer) error {
} }
return tt.Execute(out, vals) return tt.Execute(out, vals)
} }
func debug(format string, args ...interface{}) {
if flagDebug {
format = fmt.Sprintf("[debug] %s\n", format)
fmt.Printf(format, args...)
}
}

8
glide.lock generated

@ -1,5 +1,5 @@
hash: df0fa621e6a6f80dbfeb815d9d8aa308c50346a9821e401b19b6f10782da3774 hash: 6a39d319e98b1b4305c48e9b718604b723184f27a1366efcedc42d95bcbeb0c8
updated: 2017-04-03T17:00:07.670429885-06:00 updated: 2017-04-06T10:04:41.822904395-07:00
imports: imports:
- name: cloud.google.com/go - name: cloud.google.com/go
version: 3b1ae45394a234c385be014e9a488f2bb6eef821 version: 3b1ae45394a234c385be014e9a488f2bb6eef821
@ -185,6 +185,8 @@ imports:
version: 3f0ab6d4ab4bed1c61caf056b63a6e62190c7801 version: 3f0ab6d4ab4bed1c61caf056b63a6e62190c7801
- name: github.com/Masterminds/sprig - name: github.com/Masterminds/sprig
version: 23597e5f6ad0e4d590e71314bfd0251a4a3cf849 version: 23597e5f6ad0e4d590e71314bfd0251a4a3cf849
- name: github.com/Masterminds/vcs
version: 795e20f901c3d561de52811fb3488a2cb2c8588b
- name: github.com/mattn/go-runewidth - name: github.com/mattn/go-runewidth
version: d6bea18f789704b5f83375793155289da36a3c7f version: d6bea18f789704b5f83375793155289da36a3c7f
- name: github.com/mitchellh/go-wordwrap - name: github.com/mitchellh/go-wordwrap
@ -300,7 +302,7 @@ imports:
- name: gopkg.in/yaml.v2 - name: gopkg.in/yaml.v2
version: a83829b6f1293c91addabc89d0571c246397bbf4 version: a83829b6f1293c91addabc89d0571c246397bbf4
- name: k8s.io/kubernetes - name: k8s.io/kubernetes
version: ea8f6637b639246faa14a8d5c6f864100fcb77a9 version: 114f8911f9597be669a747ab72787e0bd74c9359
subpackages: subpackages:
- cmd/kubeadm/app/apis/kubeadm - cmd/kubeadm/app/apis/kubeadm
- cmd/kubeadm/app/apis/kubeadm/install - cmd/kubeadm/app/apis/kubeadm/install

@ -8,6 +8,8 @@ import:
version: f62e98d28ab7ad31d707ba837a966378465c7b57 version: f62e98d28ab7ad31d707ba837a966378465c7b57
- package: github.com/spf13/pflag - package: github.com/spf13/pflag
version: 5ccb023bc27df288a957c5e994cd44fd19619465 version: 5ccb023bc27df288a957c5e994cd44fd19619465
- package: github.com/Masterminds/vcs
version: ~1.11.0
- package: github.com/Masterminds/sprig - package: github.com/Masterminds/sprig
version: ^2.10 version: ^2.10
- package: github.com/ghodss/yaml - package: github.com/ghodss/yaml

@ -0,0 +1,74 @@
/*
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 cache provides a key generator for vcs urls.
package cache // import "k8s.io/helm/pkg/plugin/cache"
import (
"net/url"
"regexp"
"strings"
)
// Thanks glide!
// scpSyntaxRe matches the SCP-like addresses used to access repos over SSH.
var scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`)
// Key generates a cache key based on a url or scp string. The key is file
// system safe.
func Key(repo string) (string, error) {
var u *url.URL
var err error
var strip bool
if m := scpSyntaxRe.FindStringSubmatch(repo); m != nil {
// Match SCP-like syntax and convert it to a URL.
// Eg, "git@github.com:user/repo" becomes
// "ssh://git@github.com/user/repo".
u = &url.URL{
Scheme: "ssh",
User: url.User(m[1]),
Host: m[2],
Path: "/" + m[3],
}
strip = true
} else {
u, err = url.Parse(repo)
if err != nil {
return "", err
}
}
if strip {
u.Scheme = ""
}
var key string
if u.Scheme != "" {
key = u.Scheme + "-"
}
if u.User != nil && u.User.Username() != "" {
key = key + u.User.Username() + "-"
}
key = key + u.Host
if u.Path != "" {
key = key + strings.Replace(u.Path, "/", "-", -1)
}
key = strings.Replace(key, ":", "-", -1)
return key, nil
}

@ -0,0 +1,33 @@
/*
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 plugin // import "k8s.io/helm/pkg/plugin"
// Types of hooks
const (
// Install is executed after the plugin is added.
Install = "install"
// Delete is executed after the plugin is removed.
Delete = "delete"
)
// Hooks is a map of events to commands.
type Hooks map[string]string
// Get returns a hook for an event.
func (hooks Hooks) Get(event string) string {
h, _ := hooks[event]
return h
}

@ -0,0 +1,48 @@
/*
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 (
"os"
"path/filepath"
"k8s.io/helm/pkg/helm/helmpath"
)
type base struct {
// Source is the reference to a plugin
Source string
// HelmHome is the $HELM_HOME directory
HelmHome helmpath.Home
}
func newBase(source string, home helmpath.Home) base {
return base{source, home}
}
// link creates a symlink from the plugin source to $HELM_HOME.
func (b *base) link(from string) error {
debug("symlinking %s to %s", from, b.Path())
return os.Symlink(from, b.Path())
}
// Path is where the plugin will be symlinked to.
func (b *base) Path() string {
if b.Source == "" {
return ""
}
return filepath.Join(b.HelmHome.Plugins(), filepath.Base(b.Source))
}

@ -0,0 +1,17 @@
/*
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 provides an interface for installing Helm plugins.
package installer // import "k8s.io/helm/pkg/plugin/installer"

@ -0,0 +1,72 @@
/*
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 (
"errors"
"fmt"
"os"
"path/filepath"
"k8s.io/helm/pkg/helm/helmpath"
)
// ErrMissingMetadata indicates that plugin.yaml is missing.
var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing")
// Debug enables verbose output.
var Debug bool
// Installer provides an interface for installing helm client plugins.
type Installer interface {
// Install adds a plugin to $HELM_HOME.
Install() error
// Path is the directory of the installed plugin.
Path() string
}
// Install installs a plugin to $HELM_HOME.
func Install(i Installer) error {
return i.Install()
}
// NewForSource determines the correct Installer for the given source.
func NewForSource(source, version string, home helmpath.Home) (Installer, error) {
// Check if source is a local directory
if isLocalReference(source) {
return NewLocalInstaller(source, home)
}
return NewVCSInstaller(source, version, home)
}
// isLocalReference checks if the source exists on the filesystem.
func isLocalReference(source string) bool {
_, err := os.Stat(source)
return err == nil
}
// isPlugin checks if the directory contains a plugin.yaml file.
func isPlugin(dirname string) bool {
_, err := os.Stat(filepath.Join(dirname, "plugin.yaml"))
return err == nil
}
func debug(format string, args ...interface{}) {
if Debug {
format = fmt.Sprintf("[debug] %s\n", format)
fmt.Printf(format, args...)
}
}

@ -0,0 +1,49 @@
/*
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 (
"path/filepath"
"k8s.io/helm/pkg/helm/helmpath"
)
// LocalInstaller installs plugins from the filesystem.
type LocalInstaller struct {
base
}
// NewLocalInstaller creates a new LocalInstaller.
func NewLocalInstaller(source string, home helmpath.Home) (*LocalInstaller, error) {
i := &LocalInstaller{
base: newBase(source, home),
}
return i, nil
}
// Install creates a symlink to the plugin directory in $HELM_HOME.
//
// Implements Installer.
func (i *LocalInstaller) Install() error {
if !isPlugin(i.Source) {
return ErrMissingMetadata
}
src, err := filepath.Abs(i.Source)
if err != nil {
return err
}
return i.link(src)
}

@ -0,0 +1,64 @@
/*
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 (
"io/ioutil"
"os"
"path/filepath"
"testing"
"k8s.io/helm/pkg/helm/helmpath"
)
var _ Installer = new(LocalInstaller)
func TestLocalInstaller(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-home-")
if err != nil {
t.Fatal(err)
}
defer os.Remove(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.Remove(tdir)
if err := ioutil.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil {
t.Fatal(err)
}
source := "../testdata/plugdir/echo"
i, err := NewForSource(source, "", home)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if err := Install(i); err != nil {
t.Error(err)
}
if i.Path() != home.Path("plugins", "echo") {
t.Errorf("expected path '$HELM_HOME/plugins/helm-env', got %q", i.Path())
}
}

@ -0,0 +1,145 @@
/*
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 (
"os"
"sort"
"github.com/Masterminds/semver"
"github.com/Masterminds/vcs"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/plugin/cache"
)
// VCSInstaller installs plugins from remote a repository.
type VCSInstaller struct {
Repo vcs.Repo
Version string
base
}
// NewVCSInstaller creates a new VCSInstaller.
func NewVCSInstaller(source, version string, home helmpath.Home) (*VCSInstaller, error) {
key, err := cache.Key(source)
if err != nil {
return nil, err
}
cachedpath := home.Path("cache", "plugins", key)
repo, err := vcs.NewRepo(source, cachedpath)
if err != nil {
return nil, err
}
i := &VCSInstaller{
Repo: repo,
Version: version,
base: newBase(source, home),
}
return i, err
}
// Install clones a remote repository and creates a symlink to the plugin directory in HELM_HOME.
//
// Implements Installer.
func (i *VCSInstaller) Install() error {
if err := i.sync(i.Repo); err != nil {
return err
}
ref, err := i.solveVersion(i.Repo)
if err != nil {
return err
}
if err := i.setVersion(i.Repo, ref); err != nil {
return err
}
if !isPlugin(i.Repo.LocalPath()) {
return ErrMissingMetadata
}
return i.link(i.Repo.LocalPath())
}
func (i *VCSInstaller) solveVersion(repo vcs.Repo) (string, error) {
if i.Version == "" {
return "", nil
}
if repo.IsReference(i.Version) {
return i.Version, nil
}
// Create the constraint first to make sure it's valid before
// working on the repo.
constraint, err := semver.NewConstraint(i.Version)
if err != nil {
return "", err
}
// Get the tags and branches (in that order)
refs, err := repo.Tags()
if err != nil {
return "", err
}
debug("found refs: %s", refs)
// Convert and filter the list to semver.Version instances
semvers := getSemVers(refs)
// Sort semver list
sort.Sort(sort.Reverse(semver.Collection(semvers)))
for _, v := range semvers {
if constraint.Check(v) {
// If the constrint passes get the original reference
ver := v.Original()
debug("setting to %s", ver)
return ver, nil
}
}
return "", nil
}
// setVersion attempts to checkout the version
func (i *VCSInstaller) setVersion(repo vcs.Repo, ref string) error {
debug("setting version to %q", i.Version)
return repo.UpdateVersion(ref)
}
// sync will clone or update a remote repo.
func (i *VCSInstaller) sync(repo vcs.Repo) error {
if _, err := os.Stat(repo.LocalPath()); os.IsNotExist(err) {
debug("cloning %s to %s", repo.Remote(), repo.LocalPath())
return repo.Get()
}
debug("updating %s", repo.Remote())
return repo.Update()
}
// Filter a list of versions to only included semantic versions. The response
// is a mapping of the original version to the semantic version.
func getSemVers(refs []string) []*semver.Version {
var sv []*semver.Version
for _, r := range refs {
v, err := semver.NewVersion(r)
if err == nil {
sv = append(sv, v)
}
}
return sv
}

@ -0,0 +1,90 @@
/*
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 (
"io/ioutil"
"os"
"testing"
"k8s.io/helm/pkg/helm/helmpath"
"github.com/Masterminds/vcs"
)
var _ Installer = new(VCSInstaller)
type testRepo struct {
local, remote, current string
tags, branches []string
err error
vcs.Repo
}
func (r *testRepo) LocalPath() string { return r.local }
func (r *testRepo) Remote() string { return r.remote }
func (r *testRepo) Update() error { return r.err }
func (r *testRepo) Get() error { return r.err }
func (r *testRepo) IsReference(string) bool { return false }
func (r *testRepo) Tags() ([]string, error) { return r.tags, r.err }
func (r *testRepo) Branches() ([]string, error) { return r.branches, r.err }
func (r *testRepo) UpdateVersion(version string) error {
r.current = version
return r.err
}
func TestVCSInstaller(t *testing.T) {
hh, err := ioutil.TempDir("", "helm-home-")
if err != nil {
t.Fatal(err)
}
defer os.Remove(hh)
home := helmpath.Home(hh)
if err := os.MkdirAll(home.Plugins(), 0755); err != nil {
t.Fatalf("Could not create %s: %s", home.Plugins(), err)
}
source := "https://github.com/adamreese/helm-env"
repo := &testRepo{
local: "../testdata/plugdir/echo",
tags: []string{"0.1.0", "0.1.1"},
}
i, err := NewForSource(source, "~0.1.0", home)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
// ensure a VCSInstaller was returned
vcsInstaller, ok := i.(*VCSInstaller)
if !ok {
t.Error("expected a VCSInstaller")
}
// set the testRepo in the VCSInstaller
vcsInstaller.Repo = repo
if err := Install(i); err != nil {
t.Error(err)
}
if repo.current != "0.1.1" {
t.Errorf("expected version '0.1.1', got %q", repo.current)
}
if i.Path() != home.Path("plugins", "helm-env") {
t.Errorf("expected path '$HELM_HOME/plugins/helm-env', got %q", i.Path())
}
}

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package plugin package plugin // import "k8s.io/helm/pkg/plugin"
import ( import (
"io/ioutil" "io/ioutil"
@ -21,6 +21,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/Masterminds/vcs"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
) )
@ -64,6 +65,9 @@ type Metadata struct {
// Setting this will cause a number of side effects, such as the // Setting this will cause a number of side effects, such as the
// automatic setting of HELM_HOST. // automatic setting of HELM_HOST.
UseTunnel bool `json:"useTunnel"` UseTunnel bool `json:"useTunnel"`
// Hooks are commands that will run on events.
Hooks Hooks
} }
// Plugin represents a plugin. // Plugin represents a plugin.
@ -72,6 +76,17 @@ type Plugin struct {
Metadata *Metadata Metadata *Metadata
// Dir is the string path to the directory that holds the plugin. // Dir is the string path to the directory that holds the plugin.
Dir string Dir string
// Remote is the remote repo location.
Remote string
}
func detectSource(dirname string) (string, error) {
if repo, err := vcs.NewRepo("", dirname); err == nil {
if repo.CheckLocal() {
return repo.Remote(), nil
}
}
return os.Readlink(dirname)
} }
// PrepareCommand takes a Plugin.Command and prepares it for execution. // PrepareCommand takes a Plugin.Command and prepares it for execution.
@ -101,6 +116,10 @@ func LoadDir(dirname string) (*Plugin, error) {
} }
plug := &Plugin{Dir: dirname} plug := &Plugin{Dir: dirname}
if src, err := detectSource(dirname); err == nil {
plug.Remote = src
}
if err := yaml.Unmarshal(data, &plug.Metadata); err != nil { if err := yaml.Unmarshal(data, &plug.Metadata); err != nil {
return nil, err return nil, err
} }

@ -13,7 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package plugin package plugin // import "k8s.io/helm/pkg/plugin"
import ( import (
"reflect" "reflect"
@ -82,6 +82,9 @@ func TestLoadDir(t *testing.T) {
Command: "$HELM_PLUGIN_SELF/hello.sh", Command: "$HELM_PLUGIN_SELF/hello.sh",
UseTunnel: true, UseTunnel: true,
IgnoreFlags: true, IgnoreFlags: true,
Hooks: map[string]string{
Install: "echo installing...",
},
} }
if !reflect.DeepEqual(expect, plug.Metadata) { if !reflect.DeepEqual(expect, plug.Metadata) {

@ -4,3 +4,5 @@ usage: "echo something"
description: |- description: |-
This is a testing fixture. This is a testing fixture.
command: "echo Hello" command: "echo Hello"
hooks:
install: "echo Installing"

@ -6,3 +6,6 @@ description: |-
command: "$HELM_PLUGIN_SELF/hello.sh" command: "$HELM_PLUGIN_SELF/hello.sh"
useTunnel: true useTunnel: true
ignoreFlags: true ignoreFlags: true
install: "echo installing..."
hooks:
install: "echo installing..."

Loading…
Cancel
Save