Merge remote-tracking branch 'origin/master' into help-by-default

This commit is contained in:
Erin Call
2019-12-26 12:23:14 -08:00
16 changed files with 478 additions and 131 deletions

View File

@@ -2,62 +2,76 @@ package helm
import (
"fmt"
"strings"
"github.com/kelseyhightower/envconfig"
"io"
)
// The Config struct captures the `settings` and `environment` blocks inthe application's drone
// The Config struct captures the `settings` and `environment` blocks in the application's drone
// config. Configuration in drone's `settings` block arrives as uppercase env vars matching the
// config key, prefixed with `PLUGIN_`. Config from the `environment` block is *not* prefixed; any
// keys that are likely to be in that block (i.e. things that use `from_secret` need an explicit
// `envconfig:` tag so that envconfig will look for a non-prefixed env var.
// config key, prefixed with `PLUGIN_`. Config from the `environment` block is uppercased, but does
// not have the `PLUGIN_` prefix. It may, however, be prefixed with the value in `$PLUGIN_PREFIX`.
type Config struct {
// Configuration for drone-helm itself
Command helmCommand `envconfig:"HELM_COMMAND"` // Helm command to run
DroneEvent string `envconfig:"DRONE_BUILD_EVENT"` // Drone event that invoked this plugin.
UpdateDependencies bool `split_words:"true"` // call `helm dependency update` before the main command
Repos []string `envconfig:"HELM_REPOS"` // call `helm repo add` before the main command
Prefix string `` // Prefix to use when looking up secret env vars
Command string `envconfig:"HELM_COMMAND"` // Helm command to run
DroneEvent string `envconfig:"DRONE_BUILD_EVENT"` // Drone event that invoked this plugin.
UpdateDependencies bool `split_words:"true"` // Call `helm dependency update` before the main command
Repos []string `envconfig:"HELM_REPOS"` // Call `helm repo add` before the main command
Prefix string `` // Prefix to use when looking up secret env vars
Debug bool `` // Generate debug output and pass --debug to all helm commands
Values string `` // Argument to pass to --set in applicable helm commands
StringValues string `split_words:"true"` // Argument to pass to --set-string in applicable helm commands
ValuesFiles []string `split_words:"true"` // Arguments to pass to --values in applicable helm commands
Namespace string `` // Kubernetes namespace for all helm commands
KubeToken string `envconfig:"KUBERNETES_TOKEN"` // Kubernetes authentication token to put in .kube/config
SkipTLSVerify bool `envconfig:"SKIP_TLS_VERIFY"` // Put insecure-skip-tls-verify in .kube/config
Certificate string `envconfig:"KUBERNETES_CERTIFICATE"` // The Kubernetes cluster CA's self-signed certificate (must be base64-encoded)
APIServer string `envconfig:"API_SERVER"` // The Kubernetes cluster's API endpoint
ServiceAccount string `split_words:"true"` // Account to use for connecting to the Kubernetes cluster
ChartVersion string `split_words:"true"` // Specific chart version to use in `helm upgrade`
DryRun bool `split_words:"true"` // Pass --dry-run to applicable helm commands
Wait bool `` // Pass --wait to applicable helm commands
ReuseValues bool `split_words:"true"` // Pass --reuse-values to `helm upgrade`
Timeout string `` // Argument to pass to --timeout in applicable helm commands
Chart string `` // Chart argument to use in applicable helm commands
Release string `` // Release argument to use in applicable helm commands
Force bool `` // Pass --force to applicable helm commands
// Global helm config
Debug bool `` // global helm flag (also applies to drone-helm itself)
KubeConfig string `split_words:"true" default:"/root/.kube/config"` // path to the kube config file
Values string ``
StringValues string `split_words:"true"`
ValuesFiles []string `split_words:"true"`
Namespace string ``
KubeToken string `envconfig:"KUBERNETES_TOKEN"`
SkipTLSVerify bool `envconfig:"SKIP_TLS_VERIFY"`
Certificate string `envconfig:"KUBERNETES_CERTIFICATE"`
APIServer string `envconfig:"API_SERVER"`
ServiceAccount string `envconfig:"SERVICE_ACCOUNT"` // Can't just use split_words; need envconfig to find the non-prefixed form
// Config specifically for `helm upgrade`
ChartVersion string `split_words:"true"` //
DryRun bool `split_words:"true"` // also available for `delete`
Wait bool `` //
ReuseValues bool `split_words:"true"` //
Timeout string `` //
Chart string `` // Also available for `lint`, in which case it must be a path to a chart directory
Release string ``
Force bool `` //
Stdout io.Writer `ignored:"true"`
Stderr io.Writer `ignored:"true"`
}
type helmCommand string
// NewConfig creates a Config and reads environment variables into it, accounting for several possible formats.
func NewConfig(stdout, stderr io.Writer) (*Config, error) {
cfg := Config{
Stdout: stdout,
Stderr: stderr,
}
if err := envconfig.Process("plugin", &cfg); err != nil {
return nil, err
}
// helmCommand.Decode checks the given value against the list of known commands and generates a helpful error if the command is unknown.
func (cmd *helmCommand) Decode(value string) error {
known := []string{"upgrade", "delete", "lint", "help"}
for _, c := range known {
if value == c {
*cmd = helmCommand(value)
return nil
prefix := cfg.Prefix
if err := envconfig.Process("", &cfg); err != nil {
return nil, err
}
if prefix != "" {
if err := envconfig.Process(cfg.Prefix, &cfg); err != nil {
return nil, err
}
}
if value == "" {
return nil
if cfg.Debug && cfg.Stderr != nil {
cfg.logDebug()
}
known[len(known)-1] = fmt.Sprintf("or %s", known[len(known)-1])
return fmt.Errorf("unknown command '%s'. If specified, command must be %s",
value, strings.Join(known, ", "))
return &cfg, nil
}
func (cfg Config) logDebug() {
if cfg.KubeToken != "" {
cfg.KubeToken = "(redacted)"
}
fmt.Fprintf(cfg.Stderr, "Generated config: %+v\n", cfg)
}

View File

@@ -2,27 +2,179 @@ package helm
import (
"github.com/stretchr/testify/suite"
"os"
"strings"
"testing"
)
type ConfigTestSuite struct {
suite.Suite
// These tests need to mutate the environment, so the suite.setenv and .unsetenv functions store the original contents of the
// relevant variable in this map. Its use of *string is so they can distinguish between "not set" and "set to empty string"
envBackup map[string]*string
}
func TestConfigTestSuite(t *testing.T) {
suite.Run(t, new(ConfigTestSuite))
}
func (suite *ConfigTestSuite) TestHelmCommandDecodeSuccess() {
cmd := helmCommand("")
err := cmd.Decode("upgrade")
suite.Require().Nil(err)
func (suite *ConfigTestSuite) TestNewConfigWithPluginPrefix() {
suite.unsetenv("PLUGIN_PREFIX")
suite.unsetenv("HELM_COMMAND")
suite.unsetenv("UPDATE_DEPENDENCIES")
suite.unsetenv("DEBUG")
suite.EqualValues(cmd, "upgrade")
suite.setenv("PLUGIN_HELM_COMMAND", "execute order 66")
suite.setenv("PLUGIN_UPDATE_DEPENDENCIES", "true")
suite.setenv("PLUGIN_DEBUG", "true")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("execute order 66", cfg.Command)
suite.True(cfg.UpdateDependencies)
suite.True(cfg.Debug)
}
func (suite *ConfigTestSuite) TestHelmCommandDecodeFailure() {
cmd := helmCommand("")
err := cmd.Decode("execute order 66")
suite.EqualError(err, "unknown command 'execute order 66'. If specified, command must be upgrade, delete, lint, or help")
func (suite *ConfigTestSuite) TestNewConfigWithNoPrefix() {
suite.unsetenv("PLUGIN_PREFIX")
suite.unsetenv("PLUGIN_HELM_COMMAND")
suite.unsetenv("PLUGIN_UPDATE_DEPENDENCIES")
suite.unsetenv("PLUGIN_DEBUG")
suite.setenv("HELM_COMMAND", "execute order 66")
suite.setenv("UPDATE_DEPENDENCIES", "true")
suite.setenv("DEBUG", "true")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("execute order 66", cfg.Command)
suite.True(cfg.UpdateDependencies)
suite.True(cfg.Debug)
}
func (suite *ConfigTestSuite) TestNewConfigWithConfigurablePrefix() {
suite.unsetenv("API_SERVER")
suite.unsetenv("PLUGIN_API_SERVER")
suite.setenv("PLUGIN_PREFIX", "prix_fixe")
suite.setenv("PRIX_FIXE_API_SERVER", "your waiter this evening")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("prix_fixe", cfg.Prefix)
suite.Equal("your waiter this evening", cfg.APIServer)
}
func (suite *ConfigTestSuite) TestPrefixSettingDoesNotAffectPluginPrefix() {
suite.setenv("PLUGIN_PREFIX", "IXFREP")
suite.setenv("PLUGIN_HELM_COMMAND", "wake me up")
suite.setenv("IXFREP_PLUGIN_HELM_COMMAND", "send me to sleep inside")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("wake me up", cfg.Command)
}
func (suite *ConfigTestSuite) TestPrefixSettingMustHavePluginPrefix() {
suite.unsetenv("PLUGIN_PREFIX")
suite.setenv("PREFIX", "refpix")
suite.setenv("HELM_COMMAND", "gimme more")
suite.setenv("REFPIX_HELM_COMMAND", "gimme less")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("gimme more", cfg.Command)
}
func (suite *ConfigTestSuite) TestNewConfigWithConflictingVariables() {
suite.setenv("PLUGIN_HELM_COMMAND", "execute order 66")
suite.setenv("HELM_COMMAND", "defend the jedi") // values from the `environment` block override those from `settings`
suite.setenv("PLUGIN_PREFIX", "prod")
suite.setenv("TIMEOUT", "5m0s")
suite.setenv("PROD_TIMEOUT", "2m30s") // values from prefixed env vars override those from non-prefixed ones
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("defend the jedi", cfg.Command)
suite.Equal("2m30s", cfg.Timeout)
}
func (suite *ConfigTestSuite) TestNewConfigSetsWriters() {
stdout := &strings.Builder{}
stderr := &strings.Builder{}
cfg, err := NewConfig(stdout, stderr)
suite.Require().NoError(err)
suite.Equal(stdout, cfg.Stdout)
suite.Equal(stderr, cfg.Stderr)
}
func (suite *ConfigTestSuite) TestLogDebug() {
suite.setenv("DEBUG", "true")
suite.setenv("HELM_COMMAND", "upgrade")
stderr := strings.Builder{}
stdout := strings.Builder{}
_, err := NewConfig(&stdout, &stderr)
suite.Require().NoError(err)
suite.Equal("", stdout.String())
suite.Regexp(`^Generated config: \{Command:upgrade.*\}`, stderr.String())
}
func (suite *ConfigTestSuite) TestLogDebugCensorsKubeToken() {
stderr := &strings.Builder{}
kubeToken := "I'm shy! Don't put me in your build logs!"
cfg := Config{
Debug: true,
KubeToken: kubeToken,
Stderr: stderr,
}
cfg.logDebug()
suite.Contains(stderr.String(), "KubeToken:(redacted)")
suite.Equal(kubeToken, cfg.KubeToken) // The actual config value should be left unchanged
}
func (suite *ConfigTestSuite) setenv(key, val string) {
orig, ok := os.LookupEnv(key)
if ok {
suite.envBackup[key] = &orig
} else {
suite.envBackup[key] = nil
}
os.Setenv(key, val)
}
func (suite *ConfigTestSuite) unsetenv(key string) {
orig, ok := os.LookupEnv(key)
if ok {
suite.envBackup[key] = &orig
} else {
suite.envBackup[key] = nil
}
os.Unsetenv(key)
}
func (suite *ConfigTestSuite) BeforeTest(_, _ string) {
suite.envBackup = make(map[string]*string)
}
func (suite *ConfigTestSuite) AfterTest(_, _ string) {
for key, val := range suite.envBackup {
if val == nil {
os.Unsetenv(key)
} else {
os.Setenv(key, *val)
}
}
}

View File

@@ -6,7 +6,10 @@ import (
"os"
)
const kubeConfigTemplate = "/root/.kube/config.tpl"
const (
kubeConfigTemplate = "/root/.kube/config.tpl"
kubeConfigFile = "/root/.kube/config"
)
// A Step is one step in the plan.
type Step interface {
@@ -28,13 +31,12 @@ func NewPlan(cfg Config) (*Plan, error) {
runCfg: run.Config{
HelmCommand: string(cfg.Command),
Debug: cfg.Debug,
KubeConfig: cfg.KubeConfig,
Values: cfg.Values,
StringValues: cfg.StringValues,
ValuesFiles: cfg.ValuesFiles,
Namespace: cfg.Namespace,
Stdout: os.Stdout,
Stderr: os.Stderr,
Stdout: cfg.Stdout,
Stderr: cfg.Stderr,
},
}
@@ -68,6 +70,7 @@ func determineSteps(cfg Config) *func(Config) []Step {
return &help
default:
switch cfg.DroneEvent {
// Note: These events are documented in docs/upgrade_settings.yml. Any changes here should be reflected there.
case "push", "tag", "deployment", "pull_request", "promote", "rollback":
return &upgrade
case "delete":
@@ -82,7 +85,7 @@ func determineSteps(cfg Config) *func(Config) []Step {
func (p *Plan) Execute() error {
for i, step := range p.steps {
if p.cfg.Debug {
fmt.Fprintf(os.Stderr, "calling %T.Execute (step %d)\n", step, i)
fmt.Fprintf(p.cfg.Stderr, "calling %T.Execute (step %d)\n", step, i)
}
if err := step.Execute(p.runCfg); err != nil {
@@ -142,6 +145,7 @@ func initKube(cfg Config) []Step {
ServiceAccount: cfg.ServiceAccount,
Token: cfg.KubeToken,
TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
},
}
}

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/suite"
"os"
"strings"
"testing"
"github.com/pelotech/drone-helm3/internal/run"
@@ -29,26 +29,28 @@ func (suite *PlanTestSuite) TestNewPlan() {
}
defer func() { help = origHelp }()
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := Config{
Command: "help",
Debug: false,
KubeConfig: "/branch/.sfere/profig",
Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
Namespace: "outer",
Stdout: &stdout,
Stderr: &stderr,
}
runCfg := run.Config{
HelmCommand: "help",
Debug: false,
KubeConfig: "/branch/.sfere/profig",
Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
Namespace: "outer",
Stdout: os.Stdout,
Stderr: os.Stderr,
Stdout: &stdout,
Stderr: &stderr,
}
stepOne.EXPECT().
@@ -143,6 +145,7 @@ func (suite *PlanTestSuite) TestDel() {
ServiceAccount: "greathelm",
Token: "b2YgbXkgYWZmZWN0aW9u",
TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
}
suite.Equal(expected, init)
@@ -177,6 +180,7 @@ func (suite *PlanTestSuite) TestInitKube() {
ServiceAccount: "helmet",
Token: "cXVlZXIgY2hhcmFjdGVyCg==",
TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
}
suite.Equal(expected, init)
}