Merge branch 'master' into github-metafiles

This commit is contained in:
Erin Call 2019-12-26 13:02:02 -08:00
commit c0964ae178
No known key found for this signature in database
GPG key ID: 4071FF6C15B8DAD1
18 changed files with 584 additions and 136 deletions

View file

@ -2,7 +2,7 @@ FROM alpine/helm
MAINTAINER Erin Call <erin@liffft.com>
COPY build/drone-helm /bin/drone-helm
COPY kubeconfig /root/.kube/config.tpl
COPY assets/kubeconfig.tpl /root/.kube/config.tpl
LABEL description="Helm 3 plugin for Drone 3"
LABEL base="alpine/helm"

View file

@ -1,3 +1,71 @@
# Drone plugin for Helm 3
Dissatisfied with this empty README? Consider grabbing [the "put stuff in the README" issue](https://github.com/pelotech/drone-helm3/issues/8)!
This plugin provides an interface between [Drone](https://drone.io/) and [Helm 3](https://github.com/kubernetes/helm):
* Lint your charts
* Deploy your service
* Delete your service
The plugin is inpsired by [drone-helm](https://github.com/ipedrazas/drone-helm), which fills the same role for Helm 2. It provides a comparable feature-set and the configuration settings are backwards-compatible.
## Example configuration
The examples below give a minimal and sufficient configuration for each use-case. For a full description of each command's settings, see [docs/parameter_reference.md](docs/parameter_reference.md).
### Linting
```yaml
steps:
- name: lint
image: pelotech/drone-helm3
settings:
helm_command: lint
chart: ./
```
### Installation
```yaml
steps:
- name: deploy
image: pelotech/drone-helm3
settings:
helm_command: upgrade
chart: ./
release: my-project
environment:
API_SERVER: https://my.kubernetes.installation/clusters/a-1234
KUBERNETES_TOKEN:
from_secret: kubernetes_token
```
### Uninstallation
```yaml
steps:
- name: uninstall
image: pelotech/drone-helm3
settings:
helm_command: uninstall
release: my-project
environment:
API_SERVER: https://my.kubernetes.installation/clusters/a-1234
KUBERNETES_TOKEN:
from_secret: kubernetes_token
```
## Upgrading from drone-helm
drone-helm3 is largely backwards-compatible with drone-helm. There are some known differences:
* `prefix` must be supplied via the `settings` block, not `environment`.
* Several settings no longer have any effect:
* `purge` -- this is the default behavior in Helm 3
* `recreate_pods`
* `tiller_ns`
* `upgrade`
* `canary_image`
* `client_only`
* `stable_repo_url`
Since helm 3 does not require Tiller, we also recommend switching to a service account with less-expansive permissions.

View file

@ -3,7 +3,7 @@ clusters:
- cluster:
{{- if eq .SkipTLSVerify true }}
insecure-skip-tls-verify: true
{{- else }}
{{- else if .Certificate }}
certificate-authority-data: {{ .Certificate }}
{{- end}}
server: {{ .APIServer }}

View file

@ -2,22 +2,21 @@ package main
import (
"fmt"
"github.com/kelseyhightower/envconfig"
"os"
"github.com/pelotech/drone-helm3/internal/helm"
)
func main() {
var c helm.Config
cfg, err := helm.NewConfig(os.Stdout, os.Stderr)
if err := envconfig.Process("plugin", &c); err != nil {
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
return
}
// Make the plan
plan, err := helm.NewPlan(c)
plan, err := helm.NewPlan(*cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "%w\n", err)
os.Exit(1)

129
docs/parameter_reference.md Normal file
View file

@ -0,0 +1,129 @@
# Parameter reference
## Global
| Param name | Type | Purpose |
|---------------------|-----------------|---------|
| helm_command | string | Indicates the operation to perform. Recommended, but not required. Valid options are `upgrade`, `uninstall`, `lint`, and `help`. |
| update_dependencies | boolean | Calls `helm dependency update` before running the main command. **Not currently implemented**; see [#25](https://github.com/pelotech/drone-helm3/issues/25).|
| helm_repos | list\<string\> | Calls `helm repo add $repo` before running the main command. Each string should be formatted as `repo_name=https://repo.url/`. **Not currently implemented**; see [#26](https://github.com/pelotech/drone-helm3/issues/26). |
| namespace | string | Kubernetes namespace to use for this operation. |
| prefix | string | Expect environment variables to be prefixed with the given string. For more details, see "Using the prefix setting" below. **Not currently implemented**; see [#19](https://github.com/pelotech/drone-helm3/issues/19). |
| debug | boolean | Generate debug output within drone-helm3 and pass `--debug` to all helm commands. Use with care, since the debug output may include secrets. |
## Linting
Linting is only triggered when the `helm_command` setting is "lint".
| Param name | Type | Required | Purpose |
|---------------|----------------|----------|---------|
| chart | string | yes | The chart to be linted. Must be a local path. |
| values | list\<string\> | | Chart values to use as the `--set` argument to `helm lint`. |
| string_values | list\<string\> | | Chart values to use as the `--set-string` argument to `helm lint`. |
| values_files | list\<string\> | | Values to use as `--values` arguments to `helm lint`. |
## Installation
Installations are triggered when the `helm_command` setting is "upgrade." They can also be triggered when the build was triggered by a `push`, `tag`, `deployment`, `pull_request`, `promote`, or `rollback` Drone event.
| Param name | Type | Required | Purpose |
|------------------------|----------------|----------|---------|
| chart | string | yes | The chart to use for this installation. |
| release | string | yes | The release name for helm to use. |
| api_server | string | yes | API endpoint for the Kubernetes cluster. |
| kubernetes_token | string | yes | Token for authenticating to Kubernetes. |
| service_account | string | | Service account for authenticating to Kubernetes. Default is `helm`. |
| kubernetes_certificate | string | | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. |
| chart_version | string | | Specific chart version to install. |
| dry_run | boolean | | Pass `--dry-run` to `helm upgrade`. |
| wait | boolean | | Wait until kubernetes resources are in a ready state before marking the installation successful. |
| timeout | duration | | Timeout for any *individual* Kubernetes operation. The installation's full runtime may exceed this duration. |
| force | boolean | | Pass `--force` to `helm upgrade`. |
| values | list\<string\> | | Chart values to use as the `--set` argument to `helm upgrade`. |
| string_values | list\<string\> | | Chart values to use as the `--set-string` argument to `helm upgrade`. |
| values_files | list\<string\> | | Values to use as `--values` arguments to `helm upgrade`. |
| reuse_values | boolean | | Reuse the values from a previous release. |
| skip_tls_verify | boolean | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. |
## Uninstallation
Uninstallations are triggered when the `helm_command` setting is "uninstall" or "delete." They can also be triggered when the build was triggered by a `delete` Drone event.
| Param name | Type | Required | Purpose |
|------------------------|----------|----------|---------|
| release | string | yes | The release name for helm to use. |
| api_server | string | yes | API endpoint for the Kubernetes cluster. |
| kubernetes_token | string | yes | Token for authenticating to Kubernetes. |
| service_account | string | | Service account for authenticating to Kubernetes. Default is `helm`. |
| kubernetes_certificate | string | | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. |
| dry_run | boolean | | Pass `--dry-run` to `helm uninstall`. |
| timeout | duration | | Timeout for any *individual* Kubernetes operation. The uninstallation's full runtime may exceed this duration. |
| skip_tls_verify | boolean | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. |
### Where to put settings
Any setting (with the exception of `prefix`; [see below](#user-content-using-the-prefix-setting)), can go in either the `settings` or `environment` section.
### Formatting non-string values
* Booleans can be yaml's `true` and `false` literals or the strings `"true"` and `"false"`.
* Durations are strings formatted with the syntax accepted by [golang's ParseDuration function](https://golang.org/pkg/time/#ParseDuration) (e.g. 5m30s)
* List\<string\>s can be a yaml sequence or a comma-separated string.
All of the following are equivalent:
```yaml
values: "foo=1,bar=2"
values: ["foo=1", "bar=2"]
values:
- foo=1
- bar=2
```
Note that **list members must not contain commas**. Both of the following are equivalent:
```yaml
values_files: [ "./over_9,000.yml" ]
values_files: [ "./over_9", "000.yml" ]
```
### Using the `prefix` setting
Because the prefix setting is meta-configuration, it has some inherent edge-cases. Here is what it does in the cases we've thought of:
Unlike the other settings, it must be declared in the `settings` block, not `environment`:
```yaml
settings:
prefix: helm # drone-helm3 will look for environment variables called HELM_VARNAME
environment:
prefix: armet # no effect
```
It does not apply to configuration in the `settings` block, only in `environment`:
```yaml
settings:
prefix: helm
helm_timeout: 5m # no effect
environment:
helm_timeout: 2m # timeout will be 2 minutes
```
If the environment contains a variable in non-prefixed form, it will still be applied:
```yaml
settings:
prefix: helm
environment:
timeout: 2m # timeout will be 2 minutes
```
If the environment contains both the prefixed and non-prefixed forms, drone-helm3 will use the prefixed form:
```yaml
settings:
prefix: helm
environment:
timeout: 5m # overridden
helm_timeout: 2m # timeout will be 2 minutes
```

1
go.mod
View file

@ -8,4 +8,5 @@ require (
github.com/stretchr/testify v1.4.0
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 // indirect
gopkg.in/yaml.v2 v2.2.2
)

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 {
@ -27,13 +30,12 @@ func NewPlan(cfg Config) (*Plan, error) {
cfg: cfg,
runCfg: run.Config{
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,
},
}
@ -67,6 +69,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":
@ -81,11 +84,11 @@ 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 {
return fmt.Errorf("in execution step %d: %w", i, err)
return fmt.Errorf("while executing %T step: %w", step, err)
}
}
@ -141,6 +144,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"
@ -20,6 +20,7 @@ func TestPlanTestSuite(t *testing.T) {
func (suite *PlanTestSuite) TestNewPlan() {
ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl)
@ -29,25 +30,27 @@ 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{
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().
@ -63,6 +66,7 @@ func (suite *PlanTestSuite) TestNewPlan() {
func (suite *PlanTestSuite) TestNewPlanAbortsOnError() {
ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl)
@ -85,6 +89,51 @@ func (suite *PlanTestSuite) TestNewPlanAbortsOnError() {
suite.EqualError(err, "while preparing *helm.MockStep step: I'm starry Dave, aye, cat blew that")
}
func (suite *PlanTestSuite) TestExecute() {
ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl)
runCfg := run.Config{}
plan := Plan{
steps: []Step{stepOne, stepTwo},
runCfg: runCfg,
}
stepOne.EXPECT().
Execute(runCfg).
Times(1)
stepTwo.EXPECT().
Execute(runCfg).
Times(1)
suite.NoError(plan.Execute())
}
func (suite *PlanTestSuite) TestExecuteAbortsOnError() {
ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl)
runCfg := run.Config{}
plan := Plan{
steps: []Step{stepOne, stepTwo},
runCfg: runCfg,
}
stepOne.EXPECT().
Execute(runCfg).
Times(1).
Return(fmt.Errorf("oh, he'll gnaw"))
err := plan.Execute()
suite.EqualError(err, "while executing *helm.MockStep step: oh, he'll gnaw")
}
func (suite *PlanTestSuite) TestUpgrade() {
cfg := Config{
ChartVersion: "seventeen",
@ -142,6 +191,7 @@ func (suite *PlanTestSuite) TestDel() {
ServiceAccount: "greathelm",
Token: "b2YgbXkgYWZmZWN0aW9u",
TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
}
suite.Equal(expected, init)
@ -176,6 +226,7 @@ func (suite *PlanTestSuite) TestInitKube() {
ServiceAccount: "helmet",
Token: "cXVlZXIgY2hhcmFjdGVyCg==",
TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
}
suite.Equal(expected, init)
}

View file

@ -7,7 +7,6 @@ import (
// Config contains configuration applicable to all helm commands
type Config struct {
Debug bool
KubeConfig string
Values string
StringValues string
ValuesFiles []string

View file

@ -16,6 +16,7 @@ type InitKube struct {
ServiceAccount string
Token string
TemplateFile string
ConfigFile string
template *template.Template
configFile io.WriteCloser
@ -34,7 +35,7 @@ type kubeValues struct {
// Execute generates a kubernetes config file from drone-helm3's template.
func (i *InitKube) Execute(cfg Config) error {
if cfg.Debug {
fmt.Fprintf(cfg.Stderr, "writing kubeconfig file to %s\n", cfg.KubeConfig)
fmt.Fprintf(cfg.Stderr, "writing kubeconfig file to %s\n", i.ConfigFile)
}
defer i.configFile.Close()
return i.template.Execute(i.configFile, i.values)
@ -50,9 +51,6 @@ func (i *InitKube) Prepare(cfg Config) error {
if i.Token == "" {
return errors.New("token is needed to deploy")
}
if i.Certificate == "" && !i.SkipTLSVerify {
return errors.New("certificate is needed to deploy")
}
if i.ServiceAccount == "" {
i.ServiceAccount = "helm"
@ -76,16 +74,16 @@ func (i *InitKube) Prepare(cfg Config) error {
}
if cfg.Debug {
if _, err := os.Stat(cfg.KubeConfig); err != nil {
if _, err := os.Stat(i.ConfigFile); err != nil {
// non-nil err here isn't an actual error state; the kubeconfig just doesn't exist
fmt.Fprint(cfg.Stderr, "creating ")
} else {
fmt.Fprint(cfg.Stderr, "truncating ")
}
fmt.Fprintf(cfg.Stderr, "kubeconfig file at %s\n", cfg.KubeConfig)
fmt.Fprintf(cfg.Stderr, "kubeconfig file at %s\n", i.ConfigFile)
}
i.configFile, err = os.Create(cfg.KubeConfig)
i.configFile, err = os.Create(i.ConfigFile)
if err != nil {
return fmt.Errorf("could not open kubeconfig file for writing: %w", err)
}

View file

@ -1,12 +1,12 @@
package run
import (
"github.com/stretchr/testify/suite"
yaml "gopkg.in/yaml.v2"
"io/ioutil"
"os"
"text/template"
// "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"testing"
"text/template"
)
type InitKubeTestSuite struct {
@ -34,10 +34,10 @@ namespace: {{ .Namespace }}
Certificate: "CCNA",
Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
}
cfg := Config{
Namespace: "Cisco",
KubeConfig: configFile.Name(),
Namespace: "Cisco",
}
err = init.Prepare(cfg)
suite.Require().Nil(err)
@ -58,6 +58,59 @@ namespace: Cisco
suite.Equal(want, string(conf))
}
func (suite *InitKubeTestSuite) TestExecuteGeneratesConfig() {
configFile, err := tempfile("kubeconfig********.yml", "")
defer os.Remove(configFile.Name())
suite.Require().NoError(err)
cfg := Config{
Namespace: "marshmallow",
}
init := InitKube{
ConfigFile: configFile.Name(),
TemplateFile: "../../assets/kubeconfig.tpl", // the actual kubeconfig template
APIServer: "https://kube.cluster/peanut",
ServiceAccount: "chef",
Token: "eWVhaCB3ZSB0b2tpbic=",
Certificate: "d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE",
}
suite.Require().NoError(init.Prepare(cfg))
suite.Require().NoError(init.Execute(cfg))
contents, err := ioutil.ReadFile(configFile.Name())
suite.Require().NoError(err)
// each setting should be reflected in the generated file
expectations := []string{
"namespace: marshmallow",
"server: https://kube.cluster/peanut",
"user: chef",
"name: chef",
"token: eWVhaCB3ZSB0b2tpbic",
"certificate-authority-data: d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE",
}
for _, expected := range expectations {
suite.Contains(string(contents), expected)
}
// the generated config should be valid yaml, with no repeated keys
conf := map[string]interface{}{}
suite.NoError(yaml.UnmarshalStrict(contents, &conf))
// test the other branch of the certificate/SkipTLSVerify conditional
init.SkipTLSVerify = true
init.Certificate = ""
suite.Require().NoError(init.Prepare(cfg))
suite.Require().NoError(init.Execute(cfg))
contents, err = ioutil.ReadFile(configFile.Name())
suite.Require().NoError(err)
suite.Contains(string(contents), "insecure-skip-tls-verify: true")
conf = map[string]interface{}{}
suite.NoError(yaml.UnmarshalStrict(contents, &conf))
}
func (suite *InitKubeTestSuite) TestPrepareParseError() {
templateFile, err := tempfile("kubeconfig********.yml.tpl", `{{ NonexistentFunction }}`)
defer os.Remove(templateFile.Name())
@ -95,11 +148,10 @@ func (suite *InitKubeTestSuite) TestPrepareCannotOpenDestinationFile() {
Certificate: "CCNA",
Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: "/usr/foreign/exclude/kubeprofig",
}
cfg := Config{
KubeConfig: "/usr/foreign/exclude/kubeprofig",
}
cfg := Config{}
err = init.Prepare(cfg)
suite.Error(err)
suite.Regexp("could not open .* for writing: .* no such file or directory", err)
@ -120,11 +172,10 @@ func (suite *InitKubeTestSuite) TestPrepareRequiredConfig() {
Certificate: "CCNA",
Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
}
cfg := Config{
KubeConfig: configFile.Name(),
}
cfg := Config{}
suite.NoError(init.Prepare(cfg)) // consistency check; we should be starting in a happy state
@ -134,13 +185,6 @@ func (suite *InitKubeTestSuite) TestPrepareRequiredConfig() {
init.APIServer = "Sysadmin"
init.Token = ""
suite.Error(init.Prepare(cfg), "Token should be required.")
init.Token = "Aspire virtual currency"
init.Certificate = ""
suite.Error(init.Prepare(cfg), "Certificate should be required.")
init.SkipTLSVerify = true
suite.NoError(init.Prepare(cfg), "Certificate should not be required if SkipTLSVerify is true")
}
func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
@ -157,11 +201,10 @@ func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
Certificate: "CCNA",
Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
}
cfg := Config{
KubeConfig: configFile.Name(),
}
cfg := Config{}
init.Prepare(cfg)
suite.Equal("helm", init.ServiceAccount)

View file

@ -16,6 +16,8 @@ func (l *Lint) Execute(_ Config) error {
}
// Prepare gets the Lint ready to execute.
// Note: mandatory settings are documented in README.md, and the full list of settings is in docs/lint_settings.yml.
// Any additions or deletions here should be reflected there.
func (l *Lint) Prepare(cfg Config) error {
if l.Chart == "" {
return fmt.Errorf("chart is required")

View file

@ -22,7 +22,7 @@ func (u *Uninstall) Prepare(cfg Config) error {
return fmt.Errorf("release is required")
}
args := []string{"--kubeconfig", cfg.KubeConfig}
args := make([]string, 0)
if cfg.Namespace != "" {
args = append(args, "--namespace", cfg.Namespace)

View file

@ -58,11 +58,9 @@ func (suite *UninstallTestSuite) TestPrepareAndExecute() {
Run().
Times(1)
cfg := Config{
KubeConfig: "/root/.kube/config",
}
cfg := Config{}
suite.NoError(u.Prepare(cfg))
expected := []string{"--kubeconfig", "/root/.kube/config", "uninstall", "zayde_wølf_king"}
expected := []string{"uninstall", "zayde_wølf_king"}
suite.Equal(expected, actual)
u.Execute(cfg)
@ -73,15 +71,13 @@ func (suite *UninstallTestSuite) TestPrepareDryRunFlag() {
Release: "firefox_ak_wildfire",
DryRun: true,
}
cfg := Config{
KubeConfig: "/root/.kube/config",
}
cfg := Config{}
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
suite.NoError(u.Prepare(cfg))
expected := []string{"--kubeconfig", "/root/.kube/config", "uninstall", "--dry-run", "firefox_ak_wildfire"}
expected := []string{"uninstall", "--dry-run", "firefox_ak_wildfire"}
suite.Equal(expected, suite.actualArgs)
}
@ -90,16 +86,14 @@ func (suite *UninstallTestSuite) TestPrepareNamespaceFlag() {
Release: "carly_simon_run_away_with_me",
}
cfg := Config{
KubeConfig: "/root/.kube/config",
Namespace: "emotion",
Namespace: "emotion",
}
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
suite.NoError(u.Prepare(cfg))
expected := []string{"--kubeconfig", "/root/.kube/config",
"--namespace", "emotion", "uninstall", "carly_simon_run_away_with_me"}
expected := []string{"--namespace", "emotion", "uninstall", "carly_simon_run_away_with_me"}
suite.Equal(expected, suite.actualArgs)
}
@ -109,9 +103,8 @@ func (suite *UninstallTestSuite) TestPrepareDebugFlag() {
}
stderr := strings.Builder{}
cfg := Config{
KubeConfig: "/root/.kube/config",
Debug: true,
Stderr: &stderr,
Debug: true,
Stderr: &stderr,
}
command = func(path string, args ...string) cmd {
@ -126,8 +119,8 @@ func (suite *UninstallTestSuite) TestPrepareDebugFlag() {
suite.mockCmd.EXPECT().Stderr(&stderr).AnyTimes()
suite.NoError(u.Prepare(cfg))
suite.Equal(fmt.Sprintf("Generated command: '%s --kubeconfig /root/.kube/config "+
"--debug uninstall just_a_band_huff_and_puff'\n", helmBin), stderr.String())
suite.Equal(fmt.Sprintf("Generated command: '%s --debug "+
"uninstall just_a_band_huff_and_puff'\n", helmBin), stderr.String())
}
func (suite *UninstallTestSuite) TestPrepareRequiresRelease() {

View file

@ -25,6 +25,8 @@ func (u *Upgrade) Execute(_ Config) error {
}
// Prepare gets the Upgrade ready to execute.
// Note: mandatory settings are documented in README.md, and the full list of settings is in docs/upgrade_settings.yml.
// Any additions or deletions here should be reflected there.
func (u *Upgrade) Prepare(cfg Config) error {
if u.Chart == "" {
return fmt.Errorf("chart is required")
@ -33,7 +35,7 @@ func (u *Upgrade) Prepare(cfg Config) error {
return fmt.Errorf("release is required")
}
args := []string{"--kubeconfig", cfg.KubeConfig}
args := make([]string, 0)
if cfg.Namespace != "" {
args = append(args, "--namespace", cfg.Namespace)

View file

@ -41,8 +41,7 @@ func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path)
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "upgrade", "--install",
"jonas_brothers_only_human", "at40"}, args)
suite.Equal([]string{"upgrade", "--install", "jonas_brothers_only_human", "at40"}, args)
return suite.mockCmd
}
@ -55,9 +54,7 @@ func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
Run().
Times(1)
cfg := Config{
KubeConfig: "/root/.kube/config",
}
cfg := Config{}
err := u.Prepare(cfg)
suite.Require().Nil(err)
u.Execute(cfg)
@ -73,8 +70,7 @@ func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path)
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "--namespace", "melt", "upgrade",
"--install", "shaed_trampoline", "at40"}, args)
suite.Equal([]string{"--namespace", "melt", "upgrade", "--install", "shaed_trampoline", "at40"}, args)
return suite.mockCmd
}
@ -83,8 +79,7 @@ func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
suite.mockCmd.EXPECT().Stderr(gomock.Any())
cfg := Config{
Namespace: "melt",
KubeConfig: "/root/.kube/config",
Namespace: "melt",
}
err := u.Prepare(cfg)
suite.Require().Nil(err)
@ -105,7 +100,6 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
}
cfg := Config{
KubeConfig: "/root/.kube/config",
Values: "age=35",
StringValues: "height=5ft10in",
ValuesFiles: []string{"/usr/local/stats", "/usr/local/grades"},
@ -113,7 +107,7 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path)
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "upgrade", "--install",
suite.Equal([]string{"upgrade", "--install",
"--version", "radio_edit",
"--dry-run",
"--wait",
@ -165,10 +159,9 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := Config{
Debug: true,
KubeConfig: "/root/.kube/config",
Stdout: &stdout,
Stderr: &stderr,
Debug: true,
Stdout: &stdout,
Stderr: &stderr,
}
command = func(path string, args ...string) cmd {
@ -186,7 +179,7 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
u.Prepare(cfg)
want := fmt.Sprintf("Generated command: '%s --kubeconfig /root/.kube/config --debug upgrade "+
want := fmt.Sprintf("Generated command: '%s --debug upgrade "+
"--install lewis_capaldi_someone_you_loved at40'\n", helmBin)
suite.Equal(want, stderr.String())
suite.Equal("", stdout.String())