48 Commits

Author SHA1 Message Date
Erin Call
9f9e83da99 Merge pull request #35 from pelotech/testplan
Add omitted plan tests
2019-12-26 12:59:54 -08:00
Erin Call
39aea4c8dd Merge branch 'master' into testplan 2019-12-26 12:58:46 -08:00
Erin Call
7e25218945 Merge pull request #36 from pelotech/kubeconfig-tests
Validation tests for the kubeconfig template
2019-12-26 12:58:27 -08:00
Erin Call
3b85c38714 Test yaml validity without a new dependency [#15]
It turns out testify already depends on yaml, so we aren't adding
anything new by using it here.
2019-12-26 12:53:36 -08:00
Erin Call
1422ec77a4 Merge branch 'master' into kubeconfig-tests 2019-12-26 12:50:00 -08:00
Erin Call
2a13fff548 Don't check the generated config's yaml syntax [#13]
See discussion on https://github.com/pelotech/drone-helm3/pull/36 --it
doesn't really make sense to add a dependency on yaml just for testing.
2019-12-26 12:39:02 -08:00
Erin Call
d53a1ed942 Merge branch 'master' into testplan 2019-12-26 12:26:07 -08:00
Erin Call
fc3bfc466a Merge pull request #32 from pelotech/config-fixup
Clarify and improve internal/helm.Config
2019-12-26 12:23:00 -08:00
Erin Call
8f2d4bec49 Merge branch 'master' into testplan 2019-12-26 12:08:30 -08:00
Erin Call
b2066961e1 Merge branch 'master' into kubeconfig-tests 2019-12-26 11:55:32 -08:00
Joachim Hill-Grannec
253a4465f8 Merge branch 'master' into config-fixup 2019-12-26 11:36:55 -08:00
Joachim Hill-Grannec
8857782826 Merge pull request #30 from pelotech/noncompulsory-certificate
Make the Certificate setting optional
2019-12-26 11:36:38 -08:00
Joachim Hill-Grannec
81c4877a92 Merge branch 'master' into noncompulsory-certificate 2019-12-26 11:27:43 -08:00
Joachim Hill-Grannec
fbfb69f063 Merge pull request #27 from pelotech/useful-readme
Put some useful information in the README
2019-12-26 11:26:59 -08:00
Erin Call
568f613401 Associate lines of text with their yaml blocks [#8]
As I skimmed through that section I noticed it wasn't immediately clear
whether a line of text was referring to the example above it or the one
below it.
2019-12-26 09:44:46 -08:00
Erin Call
dc05855aa5 Mention the settings/environment equivalency [#8]
It seems like this needs more information, like why you'd want to put
something in one stanza or the other, but I don't really know enough
about drone to give useful advice.
2019-12-26 09:41:10 -08:00
Erin Call
6b331fdf03 Check the validity of the kubeconfig template [#13]
It's a little tricky to find a balance between "brittle" and "thorough"
in this test--I'd like to verify that e.g. the certificate is in
clusters[0].cluster.certificate-authority-data, not at the root. On the
other hand, we can't actually show that it's a valid kubeconfig file
without actually *using* it, so there's a hard upper limit on the
strength of the assertions. I've settled on verifying that all the
settings make it into the file and the file is syntactically-valid yaml.
2019-12-25 10:11:14 -08:00
Erin Call
801598e1c5 Use a clearer filepath for the kubeconfig template [#13] 2019-12-24 16:16:22 -08:00
Erin Call
cb58b5a021 Phrase errors in Execute the same as in Prepare [#33] 2019-12-24 15:49:47 -08:00
Erin Call
d86ac72529 Test Plan.Execute [#33] 2019-12-24 15:47:26 -08:00
Erin Call
52c9fb552c Ensure the plan test mocks' expectations are met [#33] 2019-12-24 15:33:50 -08:00
Erin Call
d4506608d7 Note a backwards-incompatibility in the README [#8]
This probably isn't going to bite anyone, but it's technically possible,
and it doesn't hurt to mention it.
2019-12-24 15:25:44 -08:00
Erin Call
ff8e988122 Use "installation" rather than "deployment" [#8]
"deploy" matches my mental model of what helm does, but "install"
matches helm's own terminology more closely.
2019-12-24 15:22:25 -08:00
Erin Call
ef66bc0f92 Document parameters in a single markdown file [#8]
I was unhappy with the comments-in-yaml approach; it required
duplicating a lot of information and it was hard to find a balance
between "usefully thorough" and "readably concise.""
2019-12-24 14:36:39 -08:00
Erin Call
08ddf5e27a Log debug output in helm.Config [#9]
Redacting KubeToken may not be sufficient, since it's possible that
someone would put secrets in Values or StringValues. Unilaterally
redacting those seems unhelpful, though, since they may be the very
thing the user is trying to debug. I've settled on redacting the obvious
field without trying to promise that all sensitive data will be hidden.
2019-12-24 11:08:09 -08:00
Erin Call
4ba1e694d9 Use a go-idiomatic constructor for helm.Config [#9] 2019-12-24 10:41:01 -08:00
Erin Call
10e7e7fee5 Document the Config struct's behavior correctly [#19] [#9] 2019-12-23 16:45:09 -08:00
Erin Call
285e9d98a4 Allow a configurable env var prefix [#19]
I'd like to keep Prefix's scope fairly limited, because it has potential
to spiral into something magnificently complex. You get one prefix
setting, it goes in `settings` not `environment`, end of feature.
2019-12-23 16:36:37 -08:00
Erin Call
db87bd0507 Require no-error in config tests [#9] 2019-12-23 15:52:01 -08:00
Erin Call
e2f53f3b08 Process non-prefixed forms of all config settings [#9]
Trying to guess in advance which part of the config a user will put in
the `settings` section and which they'll put in `environment` is a
fool's errand. Just let everything go in either place.

The ServiceAccount field only had an `envconfig` tag (as opposed to
`split_words`) because that triggered envconfig to look for the non-
prefixed form. Now that we're finding non-prefixed forms of everything,
we can use the clearer/more concise tag.

Note that TestPopulateWithConflictingVariables isn't meant to say
"here's what behavior we *want*" so much as "here's what the behavior
*is*." I don't think one thing is any better than the other, but we
should know which one we're getting.
2019-12-23 15:34:08 -08:00
Erin Call
c4c136b021 Do envconfig-loading in config.go (and test it!) [#9] 2019-12-23 15:10:00 -08:00
Erin Call
ef4db923cd Use a plain string for helm.Config.Command [#9]
I'm leaving the no-op test file in place because my next step is to add
new behavior that will require testing.
2019-12-23 14:06:05 -08:00
Erin Call
ae9cb59a1f No typo inthe helm.Config docs [#9] 2019-12-23 14:03:51 -08:00
Erin Call
ad5baea3e6 Document helm.Config's struct fields more clearly [#9] 2019-12-23 14:02:46 -08:00
Erin Call
4755f502b5 Always use the default kubeconfig file path [#20] 2019-12-23 12:47:16 -08:00
Erin Call
59a591eda5 Recommend removing tiller when upgrading [#8] 2019-12-23 09:57:05 -08:00
Erin Call
3d1c849e75 Don't document the kube_config setting [#8]
See #30--there's no known use-case and no drone-helm users are using the
setting, so it's on the chopping block.
2019-12-23 09:49:29 -08:00
Erin Call
3eb90651d1 Rough-draft upgrade settings documentation [#8] 2019-12-23 09:49:01 -08:00
Erin Call
044caebafd Omit empty CA data from the kubeconfig [#29] 2019-12-20 16:14:17 -08:00
Erin Call
dc4ecb6b91 Allow an empty Certificate setting [#29]
I just plain misunderstood how kubernetes CAs worked!
2019-12-20 16:11:20 -08:00
Erin Call
cab3a8ae95 Advise that some settings aren't yet functional [#8] 2019-12-20 10:37:32 -08:00
Erin Call
197a377a82 Prod maintainers to keep the docs and code in sync [#8]
Offhand I don't see a way to ensure it programmatically, but I feel like
I should at least make an attempt.
2019-12-20 10:05:50 -08:00
Erin Call
aed59c251e Namespace is relevant in helm lint [#8]
...Or at least, the namespace is passed around in helm's linting code. I
haven't proven that there's a case where omitting the namespace can
cause a linting problem, but I've seen enough to go ahead and document
the setting.
2019-12-20 09:56:51 -08:00
Erin Call
2d4688f99b Merge branch 'master' into useful-readme 2019-12-20 09:42:28 -08:00
Erin Call
420014f9e5 Rename the setting description files to _settings [#8] 2019-12-20 09:41:36 -08:00
Erin Call
285af8a317 Rough draft of an example lint stanza [#8] 2019-12-19 16:37:04 -08:00
Erin Call
485eb4375c Rename "delete" to "uninstall" [#8]
Helm 3 renamed the command, and I didn't realize it until just now.

See also 161960e, where it was renamed in the code.
2019-12-19 15:24:11 -08:00
Erin Call
5e2f2f3dc6 First draft of a useful README [#8] 2019-12-19 14:53:53 -08:00
18 changed files with 584 additions and 136 deletions

View File

@@ -2,7 +2,7 @@ FROM alpine/helm
MAINTAINER Erin Call <erin@liffft.com> MAINTAINER Erin Call <erin@liffft.com>
COPY build/drone-helm /bin/drone-helm 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 description="Helm 3 plugin for Drone 3"
LABEL base="alpine/helm" LABEL base="alpine/helm"

View File

@@ -1,3 +1,71 @@
# Drone plugin for Helm 3 # 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: - cluster:
{{- if eq .SkipTLSVerify true }} {{- if eq .SkipTLSVerify true }}
insecure-skip-tls-verify: true insecure-skip-tls-verify: true
{{- else }} {{- else if .Certificate }}
certificate-authority-data: {{ .Certificate }} certificate-authority-data: {{ .Certificate }}
{{- end}} {{- end}}
server: {{ .APIServer }} server: {{ .APIServer }}

View File

@@ -2,22 +2,21 @@ package main
import ( import (
"fmt" "fmt"
"github.com/kelseyhightower/envconfig"
"os" "os"
"github.com/pelotech/drone-helm3/internal/helm" "github.com/pelotech/drone-helm3/internal/helm"
) )
func main() { 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()) fmt.Fprintf(os.Stderr, "%s\n", err.Error())
return return
} }
// Make the plan // Make the plan
plan, err := helm.NewPlan(c) plan, err := helm.NewPlan(*cfg)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%w\n", err) fmt.Fprintf(os.Stderr, "%w\n", err)
os.Exit(1) 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 github.com/stretchr/testify v1.4.0
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 // 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 ( import (
"fmt" "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. 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 // config key, prefixed with `PLUGIN_`. Config from the `environment` block is uppercased, but does
// keys that are likely to be in that block (i.e. things that use `from_secret` need an explicit // not have the `PLUGIN_` prefix. It may, however, be prefixed with the value in `$PLUGIN_PREFIX`.
// `envconfig:` tag so that envconfig will look for a non-prefixed env var.
type Config struct { type Config struct {
// Configuration for drone-helm itself // Configuration for drone-helm itself
Command helmCommand `envconfig:"HELM_COMMAND"` // Helm command to run Command string `envconfig:"HELM_COMMAND"` // Helm command to run
DroneEvent string `envconfig:"DRONE_BUILD_EVENT"` // Drone event that invoked this plugin. 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 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 Repos []string `envconfig:"HELM_REPOS"` // Call `helm repo add` before the main command
Prefix string `` // Prefix to use when looking up secret env vars 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 Stdout io.Writer `ignored:"true"`
Debug bool `` // global helm flag (also applies to drone-helm itself) Stderr io.Writer `ignored:"true"`
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 `` //
} }
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. prefix := cfg.Prefix
func (cmd *helmCommand) Decode(value string) error {
known := []string{"upgrade", "delete", "lint", "help"} if err := envconfig.Process("", &cfg); err != nil {
for _, c := range known { return nil, err
if value == c { }
*cmd = helmCommand(value)
return nil if prefix != "" {
if err := envconfig.Process(cfg.Prefix, &cfg); err != nil {
return nil, err
} }
} }
if value == "" { if cfg.Debug && cfg.Stderr != nil {
return 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", return &cfg, nil
value, strings.Join(known, ", ")) }
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 ( import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"os"
"strings"
"testing" "testing"
) )
type ConfigTestSuite struct { type ConfigTestSuite struct {
suite.Suite 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) { func TestConfigTestSuite(t *testing.T) {
suite.Run(t, new(ConfigTestSuite)) suite.Run(t, new(ConfigTestSuite))
} }
func (suite *ConfigTestSuite) TestHelmCommandDecodeSuccess() { func (suite *ConfigTestSuite) TestNewConfigWithPluginPrefix() {
cmd := helmCommand("") suite.unsetenv("PLUGIN_PREFIX")
err := cmd.Decode("upgrade") suite.unsetenv("HELM_COMMAND")
suite.Require().Nil(err) 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() { func (suite *ConfigTestSuite) TestNewConfigWithNoPrefix() {
cmd := helmCommand("") suite.unsetenv("PLUGIN_PREFIX")
err := cmd.Decode("execute order 66") suite.unsetenv("PLUGIN_HELM_COMMAND")
suite.EqualError(err, "unknown command 'execute order 66'. If specified, command must be upgrade, delete, lint, or help") 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" "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. // A Step is one step in the plan.
type Step interface { type Step interface {
@@ -27,13 +30,12 @@ func NewPlan(cfg Config) (*Plan, error) {
cfg: cfg, cfg: cfg,
runCfg: run.Config{ runCfg: run.Config{
Debug: cfg.Debug, Debug: cfg.Debug,
KubeConfig: cfg.KubeConfig,
Values: cfg.Values, Values: cfg.Values,
StringValues: cfg.StringValues, StringValues: cfg.StringValues,
ValuesFiles: cfg.ValuesFiles, ValuesFiles: cfg.ValuesFiles,
Namespace: cfg.Namespace, Namespace: cfg.Namespace,
Stdout: os.Stdout, Stdout: cfg.Stdout,
Stderr: os.Stderr, Stderr: cfg.Stderr,
}, },
} }
@@ -67,6 +69,7 @@ func determineSteps(cfg Config) *func(Config) []Step {
return &help return &help
default: default:
switch cfg.DroneEvent { 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": case "push", "tag", "deployment", "pull_request", "promote", "rollback":
return &upgrade return &upgrade
case "delete": case "delete":
@@ -81,11 +84,11 @@ func determineSteps(cfg Config) *func(Config) []Step {
func (p *Plan) Execute() error { func (p *Plan) Execute() error {
for i, step := range p.steps { for i, step := range p.steps {
if p.cfg.Debug { 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 { 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, ServiceAccount: cfg.ServiceAccount,
Token: cfg.KubeToken, Token: cfg.KubeToken,
TemplateFile: kubeConfigTemplate, TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
}, },
} }
} }

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"os" "strings"
"testing" "testing"
"github.com/pelotech/drone-helm3/internal/run" "github.com/pelotech/drone-helm3/internal/run"
@@ -20,6 +20,7 @@ func TestPlanTestSuite(t *testing.T) {
func (suite *PlanTestSuite) TestNewPlan() { func (suite *PlanTestSuite) TestNewPlan() {
ctrl := gomock.NewController(suite.T()) ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
stepOne := NewMockStep(ctrl) stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl) stepTwo := NewMockStep(ctrl)
@@ -29,25 +30,27 @@ func (suite *PlanTestSuite) TestNewPlan() {
} }
defer func() { help = origHelp }() defer func() { help = origHelp }()
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := Config{ cfg := Config{
Command: "help", Command: "help",
Debug: false, Debug: false,
KubeConfig: "/branch/.sfere/profig",
Values: "steadfastness,forthrightness", Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility", StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"}, ValuesFiles: []string{"/root/price_inventory.yml"},
Namespace: "outer", Namespace: "outer",
Stdout: &stdout,
Stderr: &stderr,
} }
runCfg := run.Config{ runCfg := run.Config{
Debug: false, Debug: false,
KubeConfig: "/branch/.sfere/profig",
Values: "steadfastness,forthrightness", Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility", StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"}, ValuesFiles: []string{"/root/price_inventory.yml"},
Namespace: "outer", Namespace: "outer",
Stdout: os.Stdout, Stdout: &stdout,
Stderr: os.Stderr, Stderr: &stderr,
} }
stepOne.EXPECT(). stepOne.EXPECT().
@@ -63,6 +66,7 @@ func (suite *PlanTestSuite) TestNewPlan() {
func (suite *PlanTestSuite) TestNewPlanAbortsOnError() { func (suite *PlanTestSuite) TestNewPlanAbortsOnError() {
ctrl := gomock.NewController(suite.T()) ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
stepOne := NewMockStep(ctrl) stepOne := NewMockStep(ctrl)
stepTwo := 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") 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() { func (suite *PlanTestSuite) TestUpgrade() {
cfg := Config{ cfg := Config{
ChartVersion: "seventeen", ChartVersion: "seventeen",
@@ -142,6 +191,7 @@ func (suite *PlanTestSuite) TestDel() {
ServiceAccount: "greathelm", ServiceAccount: "greathelm",
Token: "b2YgbXkgYWZmZWN0aW9u", Token: "b2YgbXkgYWZmZWN0aW9u",
TemplateFile: kubeConfigTemplate, TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
} }
suite.Equal(expected, init) suite.Equal(expected, init)
@@ -176,6 +226,7 @@ func (suite *PlanTestSuite) TestInitKube() {
ServiceAccount: "helmet", ServiceAccount: "helmet",
Token: "cXVlZXIgY2hhcmFjdGVyCg==", Token: "cXVlZXIgY2hhcmFjdGVyCg==",
TemplateFile: kubeConfigTemplate, TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
} }
suite.Equal(expected, init) suite.Equal(expected, init)
} }

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
package run package run
import ( import (
"github.com/stretchr/testify/suite"
yaml "gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"os" "os"
"text/template"
// "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"testing" "testing"
"text/template"
) )
type InitKubeTestSuite struct { type InitKubeTestSuite struct {
@@ -34,10 +34,10 @@ namespace: {{ .Namespace }}
Certificate: "CCNA", Certificate: "CCNA",
Token: "Aspire virtual currency", Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(), TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
} }
cfg := Config{ cfg := Config{
Namespace: "Cisco", Namespace: "Cisco",
KubeConfig: configFile.Name(),
} }
err = init.Prepare(cfg) err = init.Prepare(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
@@ -58,6 +58,59 @@ namespace: Cisco
suite.Equal(want, string(conf)) 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() { func (suite *InitKubeTestSuite) TestPrepareParseError() {
templateFile, err := tempfile("kubeconfig********.yml.tpl", `{{ NonexistentFunction }}`) templateFile, err := tempfile("kubeconfig********.yml.tpl", `{{ NonexistentFunction }}`)
defer os.Remove(templateFile.Name()) defer os.Remove(templateFile.Name())
@@ -95,11 +148,10 @@ func (suite *InitKubeTestSuite) TestPrepareCannotOpenDestinationFile() {
Certificate: "CCNA", Certificate: "CCNA",
Token: "Aspire virtual currency", Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(), TemplateFile: templateFile.Name(),
ConfigFile: "/usr/foreign/exclude/kubeprofig",
} }
cfg := Config{ cfg := Config{}
KubeConfig: "/usr/foreign/exclude/kubeprofig",
}
err = init.Prepare(cfg) err = init.Prepare(cfg)
suite.Error(err) suite.Error(err)
suite.Regexp("could not open .* for writing: .* no such file or directory", err) suite.Regexp("could not open .* for writing: .* no such file or directory", err)
@@ -120,11 +172,10 @@ func (suite *InitKubeTestSuite) TestPrepareRequiredConfig() {
Certificate: "CCNA", Certificate: "CCNA",
Token: "Aspire virtual currency", Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(), TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
} }
cfg := Config{ cfg := Config{}
KubeConfig: configFile.Name(),
}
suite.NoError(init.Prepare(cfg)) // consistency check; we should be starting in a happy state 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.APIServer = "Sysadmin"
init.Token = "" init.Token = ""
suite.Error(init.Prepare(cfg), "Token should be required.") 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() { func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
@@ -157,11 +201,10 @@ func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
Certificate: "CCNA", Certificate: "CCNA",
Token: "Aspire virtual currency", Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(), TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
} }
cfg := Config{ cfg := Config{}
KubeConfig: configFile.Name(),
}
init.Prepare(cfg) init.Prepare(cfg)
suite.Equal("helm", init.ServiceAccount) suite.Equal("helm", init.ServiceAccount)

View File

@@ -16,6 +16,8 @@ func (l *Lint) Execute(_ Config) error {
} }
// Prepare gets the Lint ready to execute. // 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 { func (l *Lint) Prepare(cfg Config) error {
if l.Chart == "" { if l.Chart == "" {
return fmt.Errorf("chart is required") 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") return fmt.Errorf("release is required")
} }
args := []string{"--kubeconfig", cfg.KubeConfig} args := make([]string, 0)
if cfg.Namespace != "" { if cfg.Namespace != "" {
args = append(args, "--namespace", cfg.Namespace) args = append(args, "--namespace", cfg.Namespace)

View File

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

View File

@@ -25,6 +25,8 @@ func (u *Upgrade) Execute(_ Config) error {
} }
// Prepare gets the Upgrade ready to execute. // 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 { func (u *Upgrade) Prepare(cfg Config) error {
if u.Chart == "" { if u.Chart == "" {
return fmt.Errorf("chart is required") return fmt.Errorf("chart is required")
@@ -33,7 +35,7 @@ func (u *Upgrade) Prepare(cfg Config) error {
return fmt.Errorf("release is required") return fmt.Errorf("release is required")
} }
args := []string{"--kubeconfig", cfg.KubeConfig} args := make([]string, 0)
if cfg.Namespace != "" { if cfg.Namespace != "" {
args = append(args, "--namespace", 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 { command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path) suite.Equal(helmBin, path)
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "upgrade", "--install", suite.Equal([]string{"upgrade", "--install", "jonas_brothers_only_human", "at40"}, args)
"jonas_brothers_only_human", "at40"}, args)
return suite.mockCmd return suite.mockCmd
} }
@@ -55,9 +54,7 @@ func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
cfg := Config{ cfg := Config{}
KubeConfig: "/root/.kube/config",
}
err := u.Prepare(cfg) err := u.Prepare(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
u.Execute(cfg) u.Execute(cfg)
@@ -73,8 +70,7 @@ func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
command = func(path string, args ...string) cmd { command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path) suite.Equal(helmBin, path)
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "--namespace", "melt", "upgrade", suite.Equal([]string{"--namespace", "melt", "upgrade", "--install", "shaed_trampoline", "at40"}, args)
"--install", "shaed_trampoline", "at40"}, args)
return suite.mockCmd return suite.mockCmd
} }
@@ -83,8 +79,7 @@ func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
suite.mockCmd.EXPECT().Stderr(gomock.Any()) suite.mockCmd.EXPECT().Stderr(gomock.Any())
cfg := Config{ cfg := Config{
Namespace: "melt", Namespace: "melt",
KubeConfig: "/root/.kube/config",
} }
err := u.Prepare(cfg) err := u.Prepare(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
@@ -105,7 +100,6 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
} }
cfg := Config{ cfg := Config{
KubeConfig: "/root/.kube/config",
Values: "age=35", Values: "age=35",
StringValues: "height=5ft10in", StringValues: "height=5ft10in",
ValuesFiles: []string{"/usr/local/stats", "/usr/local/grades"}, ValuesFiles: []string{"/usr/local/stats", "/usr/local/grades"},
@@ -113,7 +107,7 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
command = func(path string, args ...string) cmd { command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path) suite.Equal(helmBin, path)
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "upgrade", "--install", suite.Equal([]string{"upgrade", "--install",
"--version", "radio_edit", "--version", "radio_edit",
"--dry-run", "--dry-run",
"--wait", "--wait",
@@ -165,10 +159,9 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
stdout := strings.Builder{} stdout := strings.Builder{}
stderr := strings.Builder{} stderr := strings.Builder{}
cfg := Config{ cfg := Config{
Debug: true, Debug: true,
KubeConfig: "/root/.kube/config", Stdout: &stdout,
Stdout: &stdout, Stderr: &stderr,
Stderr: &stderr,
} }
command = func(path string, args ...string) cmd { command = func(path string, args ...string) cmd {
@@ -186,7 +179,7 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
u.Prepare(cfg) 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) "--install lewis_capaldi_someone_you_loved at40'\n", helmBin)
suite.Equal(want, stderr.String()) suite.Equal(want, stderr.String())
suite.Equal("", stdout.String()) suite.Equal("", stdout.String())