Add support for uploader plugins

Signed-off-by: Josh Dolitsky <josh@dolit.ski>
pull/10430/head
Josh Dolitsky 4 years ago
parent 0e6e936ab8
commit 29bf8c09ea
No known key found for this signature in database
GPG Key ID: B2B93673243A65FB

@ -30,6 +30,8 @@ import (
// //
// It provides the implementation of 'helm push'. // It provides the implementation of 'helm push'.
type Push struct { type Push struct {
action.ChartPathOptions
Settings *cli.EnvSettings Settings *cli.EnvSettings
cfg *action.Configuration cfg *action.Configuration
} }
@ -60,7 +62,11 @@ func (p *Push) Run(chartRef string, remote string) (string, error) {
c := uploader.ChartUploader{ c := uploader.ChartUploader{
Out: &out, Out: &out,
Pushers: pusher.All(p.Settings), Pushers: pusher.All(p.Settings),
Options: []pusher.Option{}, Options: []pusher.Option{
pusher.WithBasicAuth(p.Username, p.Password),
pusher.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile),
pusher.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify),
},
} }
if registry.IsOCI(remote) { if registry.IsOCI(remote) {

@ -0,0 +1,102 @@
/*
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 pusher
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/plugin"
)
// collectPlugins scans for pusher plugins.
// This will load plugins according to the cli.
func collectPlugins(settings *cli.EnvSettings) (Providers, error) {
plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
if err != nil {
return nil, err
}
var result Providers
for _, plugin := range plugins {
for _, uploader := range plugin.Metadata.Uploaders {
result = append(result, Provider{
Schemes: uploader.Protocols,
New: NewPluginPusher(
uploader.Command,
settings,
plugin.Metadata.Name,
plugin.Dir,
),
})
}
}
return result, nil
}
// pluginPusher is a generic type to invoke custom uploaders,
// implemented in plugins.
type pluginPusher struct {
command string
settings *cli.EnvSettings
name string
base string
opts options
}
// Push runs uploader plugin command
func (p *pluginPusher) Push(chartRef, href string, options ...Option) error {
for _, opt := range options {
opt(&p.opts)
}
commands := strings.Split(p.command, " ")
argv := append(commands[1:], p.opts.certFile, p.opts.keyFile, p.opts.caFile, href, chartRef)
prog := exec.Command(filepath.Join(p.base, commands[0]), argv...)
plugin.SetupPluginEnv(p.settings, p.name, p.base)
prog.Env = os.Environ()
buf := bytes.NewBuffer(nil)
prog.Stdout = buf
prog.Stderr = os.Stderr
if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
os.Stderr.Write(eerr.Stderr)
return errors.Errorf("plugin %q exited with error", p.command)
}
return err
}
return nil
}
// NewPluginPusher constructs a valid plugin pusher
func NewPluginPusher(command string, settings *cli.EnvSettings, name, base string) Constructor {
return func(options ...Option) (Pusher, error) {
result := &pluginPusher{
command: command,
settings: settings,
name: name,
base: base,
}
for _, opt := range options {
opt(&result.opts)
}
return result, nil
}
}

@ -27,13 +27,43 @@ import (
// //
// Pushers may or may not ignore these parameters as they are passed in. // Pushers may or may not ignore these parameters as they are passed in.
type options struct { type options struct {
registryClient *registry.Client certFile string
keyFile string
caFile string
insecureSkipVerifyTLS bool
username string
password string
registryClient *registry.Client
} }
// Option allows specifying various settings configurable by the user for overriding the defaults // Option allows specifying various settings configurable by the user for overriding the defaults
// used when performing Push operations with the Pusher. // used when performing Push operations with the Pusher.
type Option func(*options) type Option func(*options)
// WithBasicAuth sets the request's Authorization header to use the provided credentials
func WithBasicAuth(username, password string) Option {
return func(opts *options) {
opts.username = username
opts.password = password
}
}
// WithInsecureSkipVerifyTLS determines if a TLS Certificate will be checked
func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) Option {
return func(opts *options) {
opts.insecureSkipVerifyTLS = insecureSkipVerifyTLS
}
}
// WithTLSClientConfig sets the client auth with the provided credentials.
func WithTLSClientConfig(certFile, keyFile, caFile string) Option {
return func(opts *options) {
opts.certFile = certFile
opts.keyFile = keyFile
opts.caFile = caFile
}
}
// WithRegistryClient sets the registryClient option. // WithRegistryClient sets the registryClient option.
func WithRegistryClient(client *registry.Client) Option { func WithRegistryClient(client *registry.Client) Option {
return func(opts *options) { return func(opts *options) {
@ -88,8 +118,11 @@ var ociProvider = Provider{
} }
// All finds all of the registered pushers as a list of Provider instances. // All finds all of the registered pushers as a list of Provider instances.
// Currently, just the built-in pushers are collected. // Currently, the built-in pushers and the discovered plugins with uploader
// notations are collected.
func All(settings *cli.EnvSettings) Providers { func All(settings *cli.EnvSettings) Providers {
result := Providers{ociProvider} result := Providers{ociProvider}
pluginDownloaders, _ := collectPlugins(settings)
result = append(result, pluginDownloaders...)
return result return result
} }

@ -22,6 +22,8 @@ import (
"helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli"
) )
const pluginDir = "testdata/plugins"
func TestProvider(t *testing.T) { func TestProvider(t *testing.T) {
p := Provider{ p := Provider{
[]string{"one", "three"}, []string{"one", "three"},
@ -53,15 +55,26 @@ func TestProviders(t *testing.T) {
func TestAll(t *testing.T) { func TestAll(t *testing.T) {
env := cli.New() env := cli.New()
env.PluginsDirectory = pluginDir
all := All(env) all := All(env)
if len(all) != 1 { if len(all) != 3 {
t.Errorf("expected 1 provider (OCI), got %d", len(all)) t.Errorf("expected 3 providers (OCI plus two plugins), got %d", len(all))
}
if _, err := all.ByScheme("test2"); err != nil {
t.Error(err)
} }
} }
func TestByScheme(t *testing.T) { func TestByScheme(t *testing.T) {
env := cli.New() env := cli.New()
env.PluginsDirectory = pluginDir
g := All(env) g := All(env)
if _, err := g.ByScheme("test"); err != nil {
t.Error(err)
}
if _, err := g.ByScheme(registry.OCIScheme); err != nil { if _, err := g.ByScheme(registry.OCIScheme); err != nil {
t.Error(err) t.Error(err)
} }

@ -0,0 +1,15 @@
name: "testpusher"
version: "0.1.0"
usage: "Push a package to a test:// remote"
description: |-
Print the environment that the plugin was given, then exit.
This registers the test:// protocol.
command: "$HELM_PLUGIN_DIR/push.sh"
ignoreFlags: true
uploaders:
#- command: "$HELM_PLUGIN_DIR/push.sh"
- command: "echo"
protocols:
- "test"

@ -0,0 +1,8 @@
#!/bin/bash
echo ENVIRONMENT
env
echo ""
echo ARGUMENTS
echo $@

@ -0,0 +1,10 @@
name: "testpusher2"
version: "0.1.0"
usage: "Push a different package to a test2:// remote"
description: "Handle test2 scheme"
command: "$HELM_PLUGIN_DIR/push.sh"
ignoreFlags: true
uploaders:
- command: "echo"
protocols:
- "test2"

@ -0,0 +1,8 @@
#!/bin/bash
echo ENVIRONMENT
env
echo ""
echo ARGUMENTS
echo $@

@ -43,6 +43,16 @@ type Downloaders struct {
Command string `json:"command"` Command string `json:"command"`
} }
// Uploaders represents the plugins capability if it can publish
// charts to special sources
type Uploaders struct {
// Protocols are the list of schemes from the charts URL.
Protocols []string `json:"protocols"`
// Command is the executable path with which the plugin performs
// the actual upload for the corresponding Protocols
Command string `json:"command"`
}
// PlatformCommand represents a command for a particular operating system and architecture // PlatformCommand represents a command for a particular operating system and architecture
type PlatformCommand struct { type PlatformCommand struct {
OperatingSystem string `json:"os"` OperatingSystem string `json:"os"`
@ -98,6 +108,10 @@ type Metadata struct {
// for special protocols. // for special protocols.
Downloaders []Downloaders `json:"downloaders"` Downloaders []Downloaders `json:"downloaders"`
// Uploaders field is used if the plugin supply uploader mechanism
// for special protocols.
Uploaders []Uploaders `json:"uploaders"`
// UseTunnelDeprecated indicates that this command needs a tunnel. // UseTunnelDeprecated indicates that this command needs a tunnel.
// 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.

@ -242,6 +242,36 @@ func TestDownloader(t *testing.T) {
} }
} }
func TestUploader(t *testing.T) {
dirname := "testdata/plugdir/good/uploader"
plug, err := LoadDir(dirname)
if err != nil {
t.Fatalf("error loading uploader plugin: %s", err)
}
if plug.Dir != dirname {
t.Fatalf("Expected dir %q, got %q", dirname, plug.Dir)
}
expect := &Metadata{
Name: "uploader",
Version: "1.2.3",
Usage: "usage",
Description: "upload something",
Command: "echo Hello",
Uploaders: []Uploaders{
{
Protocols: []string{"myprotocol", "myprotocols"},
Command: "echo Upload",
},
},
}
if !reflect.DeepEqual(expect, plug.Metadata) {
t.Fatalf("Expected metadata %v, got %v", expect, plug.Metadata)
}
}
func TestLoadAll(t *testing.T) { func TestLoadAll(t *testing.T) {
// Verify that empty dir loads: // Verify that empty dir loads:
@ -257,8 +287,8 @@ func TestLoadAll(t *testing.T) {
t.Fatalf("Could not load %q: %s", basedir, err) t.Fatalf("Could not load %q: %s", basedir, err)
} }
if l := len(plugs); l != 3 { if l := len(plugs); l != 4 {
t.Fatalf("expected 3 plugins, found %d", l) t.Fatalf("expected 4 plugins, found %d", l)
} }
if plugs[0].Metadata.Name != "downloader" { if plugs[0].Metadata.Name != "downloader" {
@ -270,6 +300,9 @@ func TestLoadAll(t *testing.T) {
if plugs[2].Metadata.Name != "hello" { if plugs[2].Metadata.Name != "hello" {
t.Errorf("Expected second plugin to be hello, got %q", plugs[1].Metadata.Name) t.Errorf("Expected second plugin to be hello, got %q", plugs[1].Metadata.Name)
} }
if plugs[3].Metadata.Name != "uploader" {
t.Errorf("Expected second plugin to be hello, got %q", plugs[1].Metadata.Name)
}
} }
func TestFindPlugins(t *testing.T) { func TestFindPlugins(t *testing.T) {
@ -296,7 +329,7 @@ func TestFindPlugins(t *testing.T) {
{ {
name: "normal", name: "normal",
plugdirs: "./testdata/plugdir/good", plugdirs: "./testdata/plugdir/good",
expected: 3, expected: 4,
}, },
} }
for _, c := range cases { for _, c := range cases {

@ -0,0 +1,11 @@
name: "uploader"
version: "1.2.3"
usage: "usage"
description: |-
upload something
command: "echo Hello"
uploaders:
- protocols:
- "myprotocol"
- "myprotocols"
command: "echo Upload"
Loading…
Cancel
Save