2 Commits

Author SHA1 Message Date
Erin Call
7892c9c0a6 Get env vars from urfave/cli instead of envconfig [#66] 2020-01-09 09:37:32 -08:00
Erin Call
b4b1f7882d Verify that every config field gets populated [#66]
I'm about to radically alter the way NewConfig populates its fields, so
I want assurance that it's still working correctly afterward.
2020-01-08 14:19:54 -08:00
37 changed files with 1634 additions and 1741 deletions

View File

@@ -1,36 +1,32 @@
---
kind: pipeline kind: pipeline
type: kubernetes type: docker
name: build name: default
trigger:
branch:
- master
clone:
# disable: true
depth: 1
image_pull_secrets:
- dockerconfig
steps: steps:
- name: test
image: golang:1.13
commands:
- go test ./cmd/... ./internal/...
- go vet ./cmd/... ./internal/...
- name: lint
image: cytopia/golint
commands:
- golint -set_exit_status ./cmd/... ./internal/...
- name: build
image: golang:1.13
commands:
- go build -o build/drone-helm cmd/drone-helm/main.go
- name: publish_linux_amd64 - name: publish_linux_amd64
image: harbor.1sept.ru/drone/drone-kaniko image: plugins/docker
settings: settings:
registry: harbor.1sept.ru auto_tag: true
repo: drone/drone-helm3 username:
cache: false from_secret: docker_username
tags: password:
- git-${DRONE_COMMIT_SHA:0:7} from_secret: docker_password
- latest repo:
username: from_secret: plugin_repo
from_secret: harbor-username dockerfile: Dockerfile
password: when:
from_secret: harbor-password event: [ tag, push ]
---
kind: signature
hmac: ab275b41f1621072152d6e85b5f94289be8dda38debd2a177e0f666960491c6e
...

View File

@@ -7,9 +7,6 @@ assignees: ''
--- ---
**My drone-helm3 and drone versions:**
<!-- e.g. drone-helm3 0.9.0, drone 1.6.0-->
**What I tried to do:** **What I tried to do:**
<!-- e.g. run a helm installation --> <!-- e.g. run a helm installation -->

4
.gitignore vendored
View File

@@ -4,7 +4,6 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
*.swp
.idea .idea
@@ -18,6 +17,3 @@
# vendor/ # vendor/
.env .env
.secrets .secrets
build/*
**/.DS_Store

View File

@@ -1,19 +1,10 @@
FROM golang:1.24-alpine3.21 AS builder FROM alpine/helm:3.0.2
MAINTAINER Erin Call <erin@liffft.com>
ENV GO111MODULE=on COPY build/drone-helm /bin/drone-helm
WORKDIR /app COPY assets/kubeconfig.tpl /root/.kube/config.tpl
COPY --link go.mod . LABEL description="Helm 3 plugin for Drone 3"
COPY --link go.sum . LABEL base="alpine/helm"
RUN go mod download
COPY --link . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /go/bin/drone-helm ./cmd/drone-helm ENTRYPOINT [ "/bin/drone-helm" ]
# --- Copy the cli to an image with helm already installed ---
FROM alpine/helm:3.18.5
COPY --link --from=builder /go/bin/drone-helm /bin/drone-helm
COPY --link ./assets/kubeconfig.tpl /root/.kube/config.tpl
ENTRYPOINT [ "/bin/drone-helm" ]

View File

@@ -1,9 +0,0 @@
help:
@echo "make check - Check Dockerfile syntax"
@echo "make drone-sign - Sign Drone config file"
check:
docker build --check .
drone-sign: ## Envs DRONE_SERVER & DRONE_TOKEN needed
drone sign --save 1sept/drone-helm3

View File

@@ -5,12 +5,11 @@ import (
"os" "os"
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/pelotech/drone-helm3/internal/helm" "github.com/pelotech/drone-helm3/internal/helm"
) )
func main() { func main() {
cfg, err := env.NewConfig(os.Stdout, os.Stderr) cfg, err := helm.NewConfig(os.Stdout, os.Stderr, os.Args...)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error()) fmt.Fprintf(os.Stderr, "%s\n", err.Error())

View File

@@ -1,15 +1,13 @@
# Parameter reference # Parameter reference
## Global ## Global
| Param name | Type | Alias | Purpose | | Param name | Type | Purpose |
|---------------------|-----------------|--------------|---------| |---------------------|-----------------|---------|
| mode | string | helm_command | Indicates the operation to perform. Recommended, but not required. Valid options are `upgrade`, `uninstall`, `lint`, and `help`. | | mode | 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.| | update_dependencies | boolean | Calls `helm dependency update` before running the main command.|
| add_repos | list\<string\> | helm_repos | Calls `helm repo add $repo` before running the main command. Each string should be formatted as `repo_name=https://repo.url/`. | | add_repos | list\<string\> | Calls `helm repo add $repo` before running the main command. Each string should be formatted as `repo_name=https://repo.url/`. |
| repo_certificate | string | | Base64 encoded TLS certificate for a chart repository. | | namespace | string | Kubernetes namespace to use for this operation. |
| repo_ca_certificate | string | | Base64 encoded TLS certificate for a chart repository certificate authority. | | 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. |
| namespace | string | | Kubernetes namespace to use for this operation. |
| 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
@@ -27,49 +25,43 @@ Linting is only triggered when the `mode` setting is "lint".
Installations are triggered when the `mode` 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. Installations are triggered when the `mode` 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 | Alias | Purpose | | Param name | Type | Required | Purpose |
|------------------------|----------------|----------|------------------------|---------| |------------------------|----------------|----------|---------|
| chart | string | yes | | The chart to use for this installation. | | chart | string | yes | The chart to use for this installation. |
| release | string | yes | | The release name for helm to use. | | release | string | yes | The release name for helm to use. |
| skip_kubeconfig | boolean | | | Whether to skip kubeconfig file creation. | | kube_api_server | string | yes | API endpoint for the Kubernetes cluster. |
| kube_api_server | string | yes | api_server | API endpoint for the Kubernetes cluster. This is ignored if `skip_kubeconfig` is `true`. | | kube_token | string | yes | Token for authenticating to Kubernetes. |
| kube_token | string | yes | kubernetes_token | Token for authenticating to Kubernetes. This is ignored if `skip_kubeconfig` is `true`. | | kube_service_account | string | | Service account for authenticating to Kubernetes. Default is `helm`. |
| kube_service_account | string | | service_account | Service account for authenticating to Kubernetes. Default is `helm`. This is ignored if `skip_kubeconfig` is `true`. | | kube_certificate | string | | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. |
| kube_certificate | string | | kubernetes_certificate | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. This is ignored if `skip_kubeconfig` is `true`. | | chart_version | string | | Specific chart version to install. |
| chart_version | string | | | Specific chart version to install. | | dry_run | boolean | | Pass `--dry-run` to `helm upgrade`. |
| dry_run | boolean | | | Pass `--dry-run` to `helm upgrade`. | | wait_for_upgrade | boolean | | Wait until kubernetes resources are in a ready state before marking the installation successful. |
| dependencies_action | string | | | Calls `helm dependency build` OR `helm dependency update` before running the main command. Possible values: `build`, `update`. | | timeout | duration | | Timeout for any *individual* Kubernetes operation. The installation's full runtime may exceed this duration. |
| wait_for_upgrade | boolean | | wait | Wait until kubernetes resources are in a ready state before marking the installation successful. | | force_upgrade | boolean | | Pass `--force` to `helm upgrade`. |
| timeout | duration | | | Timeout for any *individual* Kubernetes operation. The installation's full runtime may exceed this duration. | | atomic_upgrade | boolean | | Pass `--atomic` to `helm upgrade`. |
| force_upgrade | boolean | | force | Pass `--force` to `helm upgrade`. | | cleanup_failed_upgrade | boolean | | Pass `--cleanup-on-fail` to `helm upgrade`. |
| atomic_upgrade | boolean | | | Pass `--atomic` to `helm upgrade`. | | values | list\<string\> | | Chart values to use as the `--set` argument to `helm upgrade`. |
| cleanup_failed_upgrade | boolean | | | Pass `--cleanup-on-fail` to `helm upgrade`. | | string_values | list\<string\> | | Chart values to use as the `--set-string` argument to `helm upgrade`. |
| history_max | int | | | Pass `--history-max` to `helm upgrade`. | | values_files | list\<string\> | | Values to use as `--values` arguments to `helm upgrade`. |
| values | list\<string\> | | | Chart values to use as the `--set` argument to `helm upgrade`. | | reuse_values | boolean | | Reuse the values from a previous release. |
| string_values | list\<string\> | | | Chart values to use as the `--set-string` argument to `helm upgrade`. | | skip_tls_verify | boolean | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. |
| 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. This is ignored if `skip_kubeconfig` is `true`. |
| create_namespace | boolean | | | Pass --create-namespace to `helm upgrade`. |
| skip_crds | boolean | | | Pass --skip-crds to `helm upgrade`. |
## Uninstallation ## Uninstallation
Uninstallations are triggered when the `mode` setting is "uninstall" or "delete." They can also be triggered when the build was triggered by a `delete` Drone event. Uninstallations are triggered when the `mode` setting is "uninstall" or "delete." They can also be triggered when the build was triggered by a `delete` Drone event.
| Param name | Type | Required | Alias | Purpose | | Param name | Type | Required | Purpose |
|------------------------|----------|----------|------------------------|---------| |------------------------|----------|----------|---------|
| release | string | yes | | The release name for helm to use. | | release | string | yes | The release name for helm to use. |
| skip_kubeconfig | boolean | | | Whether to skip kubeconfig file creation. | | kube_api_server | string | yes | API endpoint for the Kubernetes cluster. |
| kube_api_server | string | yes | api_server | API endpoint for the Kubernetes cluster. This is ignored if `skip_kubeconfig` is `true`. | | kube_token | string | yes | Token for authenticating to Kubernetes. |
| kube_token | string | yes | kubernetes_token | Token for authenticating to Kubernetes. This is ignored if `skip_kubeconfig` is `true`. | | kube_service_account | string | | Service account for authenticating to Kubernetes. Default is `helm`. |
| kube_service_account | string | | service_account | Service account for authenticating to Kubernetes. Default is `helm`. This is ignored if `skip_kubeconfig` is `true`. | | kube_certificate | string | | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. |
| kube_certificate | string | | kubernetes_certificate | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. This is ignored if `skip_kubeconfig` is `true`. | | keep_history | boolean | | Pass `--keep-history` to `helm uninstall`, to retain the release history. |
| keep_history | boolean | | | Pass `--keep-history` to `helm uninstall`, to retain the release history. | | dry_run | boolean | | Pass `--dry-run` to `helm uninstall`. |
| 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. |
| 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. |
| skip_tls_verify | boolean | | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. This is ignored if `skip_kubeconfig` is `true`. | | chart | string | | Required when the global `update_dependencies` parameter is true. No effect otherwise. |
| chart | string | | | Required when the global `update_dependencies` parameter is true. No effect otherwise. |
### Where to put settings ### Where to put settings
@@ -100,38 +92,3 @@ Note that **list members must not contain commas**. Both of the following are eq
values_files: [ "./over_9,000.yml" ] values_files: [ "./over_9,000.yml" ]
values_files: [ "./over_9", "000.yml" ] values_files: [ "./over_9", "000.yml" ]
``` ```
### Interpolating secrets into the `values`, `string_values` and `add_repos` settings
If you want to send secrets to your charts, you can use syntax similar to shell variable interpolation--either `$VARNAME` or `$${VARNAME}`. The double dollar-sign is necessary when using curly brackets; using curly brackets with a single dollar-sign will trigger Drone's string substitution (which can't use arbitrary environment variables). If an environment variable is not set, it will be treated as if it were set to the empty string.
```yaml
environment:
DB_PASSWORD:
from_secret: db_password
SESSION_KEY:
from_secret: session_key
settings:
values:
- db_password=$DB_PASSWORD # db_password will be set to the contents of the db_password secret
- db_pass=$DB_PASS # db_pass will be set to "" since $DB_PASS is not set
- session_key=$${SESSION_KEY} # session_key will be set to the contents of the session_key secret
- sess_key=${SESSION_KEY} # sess_key will be set to "" by Drone's variable substitution
```
Variables intended for interpolation must be set in the `environment` section, not `settings`.
### Backward-compatibility aliases
Some settings have alternate names, for backward-compatibility with drone-helm. We recommend using the canonical name unless you require the backward-compatible form.
| Canonical name | Alias |
|----------------------|-------|
| mode | helm_command |
| add_repos | helm_repos |
| kube_api_server | api_server |
| kube_service_account | service_account |
| kube_token | kubernetes_token |
| kube_certificate | kubernetes_certificate |
| wait_for_upgrade | wait |
| force_upgrade | force |

4
go.mod
View File

@@ -5,9 +5,7 @@ go 1.13
require ( require (
github.com/golang/mock v1.3.1 github.com/golang/mock v1.3.1
github.com/joho/godotenv v1.3.0 github.com/joho/godotenv v1.3.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect github.com/urfave/cli/v2 v2.1.1
golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 // indirect
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 v2.2.2
) )

21
go.sum
View File

@@ -1,33 +1,30 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262 h1:qsl9y/CJx34tuA7QCPNp86JNJe4spst6Ff8MjvPUdPg= golang.org/x/tools v0.0.0-20190425150028-36563e24a262 h1:qsl9y/CJx34tuA7QCPNp86JNJe4spst6Ff8MjvPUdPg=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f h1:kDxGY2VmgABOe55qheT/TFqUMtcTHnomIPS1iv3G4Ms=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd h1:Zc7EU2PqpsNeIfOoVA7hvQX4cS3YDJEs5KlfatT3hLo=
golang.org/x/tools v0.0.0-20191206204035-259af5ff87bd/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 h1:NfPq5mgc5ArFgVLCpeS4z07IoxSAqVfV/gQ5vxdgaxI=
golang.org/x/tools v0.0.0-20191209225234-22774f7dae43/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=

177
internal/env/config.go vendored
View File

@@ -1,177 +0,0 @@
package env
import (
"fmt"
"io"
"os"
"regexp"
"strings"
"github.com/kelseyhightower/envconfig"
)
const (
defaultHistoryMax = 10
)
var (
justNumbers = regexp.MustCompile(`^\d+$`)
deprecatedVars = []string{"PURGE", "RECREATE_PODS", "TILLER_NS", "UPGRADE", "CANARY_IMAGE", "CLIENT_ONLY", "STABLE_REPO_URL"}
)
// 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 uppercased, but does
// not have the `PLUGIN_` prefix.
type Config struct {
// Configuration for drone-helm itself
Command string `envconfig:"mode"` // Helm command to run
DroneEvent string `envconfig:"drone_build_event"` // Drone event that invoked this plugin.
UpdateDependencies bool `split_words:"true"` // [Deprecated] Call `helm dependency update` before the main command (deprecated, use dependencies_action: update instead)
DependenciesAction string `split_words:"true"` // Call `helm dependency build` or `helm dependency update` before the main command
AddRepos []string `split_words:"true"` // Call `helm repo add` before the main command
RepoCertificate string `envconfig:"repo_certificate"` // The Helm chart repository's self-signed certificate (must be base64-encoded)
RepoCACertificate string `envconfig:"repo_ca_certificate"` // The Helm chart repository CA's self-signed certificate (must be base64-encoded)
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
CreateNamespace bool `split_words:"true"` // Pass --create-namespace to `helm upgrade`
KubeToken string `split_words:"true"` // Kubernetes authentication token to put in .kube/config
SkipKubeconfig bool `envconfig:"skip_kubeconfig"` // Skip kubeconfig creation
SkipTLSVerify bool `envconfig:"skip_tls_verify"` // Put insecure-skip-tls-verify in .kube/config
Certificate string `envconfig:"kube_certificate"` // The Kubernetes cluster CA's self-signed certificate (must be base64-encoded)
APIServer string `envconfig:"kube_api_server"` // The Kubernetes cluster's API endpoint
ServiceAccount string `envconfig:"kube_service_account"` // 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 `envconfig:"wait_for_upgrade"` // Pass --wait to applicable helm commands
ReuseValues bool `split_words:"true"` // Pass --reuse-values to `helm upgrade`
KeepHistory bool `split_words:"true"` // Pass --keep-history to `helm uninstall`
HistoryMax int `split_words:"true"` // Pass --history-max option
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 `envconfig:"force_upgrade"` // Pass --force to applicable helm commands
AtomicUpgrade bool `split_words:"true"` // Pass --atomic to `helm upgrade`
CleanupOnFail bool `envconfig:"cleanup_failed_upgrade"` // Pass --cleanup-on-fail to `helm upgrade`
LintStrictly bool `split_words:"true"` // Pass --strict to `helm lint`
SkipCrds bool `split_words:"true"` // Pass --skip-crds to `helm upgrade`
Stdout io.Writer `ignored:"true"`
Stderr io.Writer `ignored:"true"`
}
// NewConfig creates a Config and reads environment variables into it, accounting for several possible formats.
func NewConfig(stdout, stderr io.Writer) (*Config, error) {
var aliases settingAliases
if err := envconfig.Process("plugin", &aliases); err != nil {
return nil, err
}
if err := envconfig.Process("", &aliases); err != nil {
return nil, err
}
cfg := Config{
Command: aliases.Command,
AddRepos: aliases.AddRepos,
APIServer: aliases.APIServer,
ServiceAccount: aliases.ServiceAccount,
Wait: aliases.Wait,
Force: aliases.Force,
KubeToken: aliases.KubeToken,
Certificate: aliases.Certificate,
// set to same default as helm CLI
HistoryMax: defaultHistoryMax,
Stdout: stdout,
Stderr: stderr,
}
if err := envconfig.Process("plugin", &cfg); err != nil {
return nil, err
}
if err := envconfig.Process("", &cfg); err != nil {
return nil, err
}
if cfg.SkipKubeconfig {
if cfg.KubeToken != "" || cfg.Certificate != "" || cfg.APIServer != "" || cfg.ServiceAccount != "" || cfg.SkipTLSVerify {
fmt.Fprintf(cfg.Stderr, "Warning: skip_kubeconfig is set. The following kubeconfig-related settings will be ignored: kube_config, kube_certificate, kube_api_server, kube_service_account, skip_tls_verify.")
}
}
if justNumbers.MatchString(cfg.Timeout) {
cfg.Timeout = fmt.Sprintf("%ss", cfg.Timeout)
}
cfg.loadValuesSecrets()
if cfg.Debug && cfg.Stderr != nil {
cfg.logDebug()
}
cfg.deprecationWarn()
return &cfg, nil
}
func (cfg *Config) loadValuesSecrets() {
findVar := regexp.MustCompile(`\$\{?(\w+)\}?`)
replacer := func(varName string) string {
sigils := regexp.MustCompile(`[${}]`)
varName = sigils.ReplaceAllString(varName, "")
if value, ok := os.LookupEnv(varName); ok {
return value
}
if cfg.Debug {
fmt.Fprintf(cfg.Stderr, "$%s not present in environment, replaced with \"\"\n", varName)
}
return ""
}
cfg.Values = findVar.ReplaceAllStringFunc(cfg.Values, replacer)
cfg.StringValues = findVar.ReplaceAllStringFunc(cfg.StringValues, replacer)
for i := 0; i < len(cfg.AddRepos); i++ {
cfg.AddRepos[i] = findVar.ReplaceAllStringFunc(cfg.AddRepos[i], replacer)
}
}
func (cfg Config) logDebug() {
if cfg.KubeToken != "" {
cfg.KubeToken = "(redacted)"
}
fmt.Fprintf(cfg.Stderr, "Generated config: %+v\n", cfg)
}
func (cfg *Config) deprecationWarn() {
for _, varname := range deprecatedVars {
_, barePresent := os.LookupEnv(varname)
_, prefixedPresent := os.LookupEnv("PLUGIN_" + varname)
if barePresent || prefixedPresent {
fmt.Fprintf(cfg.Stderr, "Warning: ignoring deprecated '%s' setting\n", strings.ToLower(varname))
}
}
}
// settingAliases provides alternate environment variable names for certain settings, either because
// they were renamed during drone-helm3's lifetime or for backward-compatibility with the original
// drone-helm. Most config options don't need to be included here; adding them to the main Config
// struct is sufficient.
type settingAliases struct {
Command string `envconfig:"helm_command"`
AddRepos []string `envconfig:"helm_repos"`
APIServer string `envconfig:"api_server"`
ServiceAccount string `split_words:"true"`
Wait bool ``
Force bool ``
KubeToken string `envconfig:"kubernetes_token"`
Certificate string `envconfig:"kubernetes_certificate"`
}

View File

@@ -1,261 +0,0 @@
package env
import (
"fmt"
"os"
"strings"
"testing"
"github.com/stretchr/testify/suite"
)
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) TestNewConfigWithPluginPrefix() {
suite.unsetenv("MODE")
suite.unsetenv("UPDATE_DEPENDENCIES")
suite.unsetenv("DEBUG")
suite.setenv("PLUGIN_MODE", "iambic")
suite.setenv("PLUGIN_UPDATE_DEPENDENCIES", "true")
suite.setenv("PLUGIN_DEBUG", "true")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("iambic", cfg.Command)
suite.True(cfg.UpdateDependencies)
suite.True(cfg.Debug)
}
func (suite *ConfigTestSuite) TestNewConfigWithNoPrefix() {
suite.unsetenv("PLUGIN_MODE")
suite.unsetenv("PLUGIN_UPDATE_DEPENDENCIES")
suite.unsetenv("PLUGIN_DEBUG")
suite.setenv("MODE", "iambic")
suite.setenv("UPDATE_DEPENDENCIES", "true")
suite.setenv("DEBUG", "true")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("iambic", cfg.Command)
suite.True(cfg.UpdateDependencies)
suite.True(cfg.Debug)
}
func (suite *ConfigTestSuite) TestNewConfigWithConflictingVariables() {
suite.setenv("PLUGIN_MODE", "iambic")
suite.setenv("MODE", "haiku") // values from the `environment` block override those from `settings`
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("haiku", cfg.Command)
}
func (suite *ConfigTestSuite) TestNewConfigInfersNumbersAreSeconds() {
suite.setenv("PLUGIN_TIMEOUT", "42")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("42s", cfg.Timeout)
}
func (suite *ConfigTestSuite) TestNewConfigWithAliases() {
for _, varname := range []string{
"MODE",
"ADD_REPOS",
"KUBE_API_SERVER",
"KUBE_SERVICE_ACCOUNT",
"WAIT_FOR_UPGRADE",
"FORCE_UPGRADE",
"KUBE_TOKEN",
"KUBE_CERTIFICATE",
} {
suite.unsetenv(varname)
suite.unsetenv("PLUGIN_" + varname)
}
suite.setenv("PLUGIN_HELM_COMMAND", "beware the jabberwock")
suite.setenv("PLUGIN_HELM_REPOS", "chortle=http://calloo.callay/frabjous/day")
suite.setenv("PLUGIN_API_SERVER", "http://tumtum.tree")
suite.setenv("PLUGIN_SERVICE_ACCOUNT", "tulgey")
suite.setenv("PLUGIN_WAIT", "true")
suite.setenv("PLUGIN_FORCE", "true")
suite.setenv("PLUGIN_KUBERNETES_TOKEN", "Y29tZSB0byBteSBhcm1z")
suite.setenv("PLUGIN_KUBERNETES_CERTIFICATE", "d2l0aCBpdHMgaGVhZA==")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("beware the jabberwock", cfg.Command)
suite.Equal([]string{"chortle=http://calloo.callay/frabjous/day"}, cfg.AddRepos)
suite.Equal("http://tumtum.tree", cfg.APIServer)
suite.Equal("tulgey", cfg.ServiceAccount)
suite.True(cfg.Wait, "Wait should be aliased")
suite.True(cfg.Force, "Force should be aliased")
suite.Equal("Y29tZSB0byBteSBhcm1z", cfg.KubeToken, "KubeToken should be aliased")
suite.Equal("d2l0aCBpdHMgaGVhZA==", cfg.Certificate, "Certificate should be aliased")
}
func (suite *ConfigTestSuite) TestAliasedSettingWithoutPluginPrefix() {
suite.unsetenv("FORCE_UPGRADE")
suite.unsetenv("PLUGIN_FORCE_UPGRADE")
suite.unsetenv("PLUGIN_FORCE")
suite.setenv("FORCE", "true")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.True(cfg.Force)
}
func (suite *ConfigTestSuite) TestNewConfigWithAliasConflicts() {
suite.unsetenv("FORCE_UPGRADE")
suite.setenv("PLUGIN_FORCE", "true")
suite.setenv("PLUGIN_FORCE_UPGRADE", "false") // should override even when set to the zero value
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.NoError(err)
suite.False(cfg.Force, "official names should override alias names")
}
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) TestDeprecatedSettingWarnings() {
for _, varname := range deprecatedVars {
suite.setenv(varname, "deprecoat") // environment-block entries should cause warnings
}
suite.unsetenv("PURGE")
suite.setenv("PLUGIN_PURGE", "true") // settings-block entries should cause warnings
suite.setenv("UPGRADE", "") // entries should cause warnings even when set to empty string
stderr := &strings.Builder{}
_, err := NewConfig(&strings.Builder{}, stderr)
suite.NoError(err)
for _, varname := range deprecatedVars {
suite.Contains(stderr.String(), fmt.Sprintf("Warning: ignoring deprecated '%s' setting\n", strings.ToLower(varname)))
}
}
func (suite *ConfigTestSuite) TestLogDebug() {
suite.setenv("DEBUG", "true")
suite.setenv("MODE", "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) TestNewConfigWithValuesSecrets() {
suite.unsetenv("VALUES")
suite.unsetenv("STRING_VALUES")
suite.unsetenv("SECRET_WATER")
suite.setenv("SECRET_FIRE", "Eru_Ilúvatar")
suite.setenv("SECRET_RINGS", "1")
suite.setenv("PLUGIN_VALUES", "fire=$SECRET_FIRE,water=${SECRET_WATER}")
suite.setenv("PLUGIN_STRING_VALUES", "rings=${SECRET_RINGS}")
suite.setenv("PLUGIN_ADD_REPOS", "testrepo=https://user:${SECRET_FIRE}@testrepo.test")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("fire=Eru_Ilúvatar,water=", cfg.Values)
suite.Equal("rings=1", cfg.StringValues)
suite.Equal(fmt.Sprintf("testrepo=https://user:%s@testrepo.test", os.Getenv("SECRET_FIRE")), cfg.AddRepos[0])
}
func (suite *ConfigTestSuite) TestValuesSecretsWithDebugLogging() {
suite.unsetenv("VALUES")
suite.unsetenv("SECRET_WATER")
suite.setenv("SECRET_FIRE", "Eru_Ilúvatar")
suite.setenv("PLUGIN_DEBUG", "true")
suite.setenv("PLUGIN_STRING_VALUES", "fire=$SECRET_FIRE")
suite.setenv("PLUGIN_VALUES", "fire=$SECRET_FIRE,water=$SECRET_WATER")
stderr := strings.Builder{}
_, err := NewConfig(&strings.Builder{}, &stderr)
suite.Require().NoError(err)
suite.Contains(stderr.String(), "Values:fire=Eru_Ilúvatar,water=")
suite.Contains(stderr.String(), `$SECRET_WATER not present in environment, replaced with ""`)
}
func (suite *ConfigTestSuite) TestHistoryMax() {
conf := NewTestConfig(suite.T())
suite.Assert().Equal(10, conf.HistoryMax)
suite.setenv("PLUGIN_HISTORY_MAX", "0")
conf = NewTestConfig(suite.T())
suite.Assert().Equal(0, conf.HistoryMax)
}
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

@@ -1,16 +0,0 @@
package env
import (
"os"
"testing"
"github.com/stretchr/testify/require"
)
// NewTestConfig is a helper for setting up a Config object and error checking
func NewTestConfig(t *testing.T) *Config {
conf, err := NewConfig(os.Stdout, os.Stderr)
require.NoError(t, err)
return conf
}

237
internal/helm/config.go Normal file
View File

@@ -0,0 +1,237 @@
package helm
import (
"fmt"
"github.com/urfave/cli/v2"
"io"
"os"
"regexp"
"strings"
)
var (
justNumbers = regexp.MustCompile(`^\d+$`)
deprecatedVars = []string{"PURGE", "RECREATE_PODS", "TILLER_NS", "UPGRADE", "CANARY_IMAGE", "CLIENT_ONLY", "STABLE_REPO_URL"}
)
// 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 uppercased, but does
// not have the `PLUGIN_` prefix.
type Config struct {
// Configuration for drone-helm itself
Command string // Helm command to run
DroneEvent string // Drone event that invoked this plugin.
UpdateDependencies bool // Call `helm dependency update` before the main command
AddRepos []string // Call `helm repo add` before the main command
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 // Argument to pass to --set-string in applicable helm commands
ValuesFiles []string // Arguments to pass to --values in applicable helm commands
Namespace string // Kubernetes namespace for all helm commands
KubeToken string // Kubernetes authentication token to put in .kube/config
SkipTLSVerify bool // Put insecure-skip-tls-verify in .kube/config
Certificate string // The Kubernetes cluster CA's self-signed certificate (must be base64-encoded)
APIServer string // The Kubernetes cluster's API endpoint
ServiceAccount string // Account to use for connecting to the Kubernetes cluster
ChartVersion string // Specific chart version to use in `helm upgrade`
DryRun bool // Pass --dry-run to applicable helm commands
Wait bool // Pass --wait to applicable helm commands
ReuseValues bool // Pass --reuse-values to `helm upgrade`
KeepHistory bool // Pass --keep-history to `helm uninstall`
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
AtomicUpgrade bool // Pass --atomic to `helm upgrade`
CleanupOnFail bool // Pass --cleanup-on-fail to `helm upgrade`
LintStrictly bool // Pass --strict to `helm lint`
Stdout io.Writer
Stderr io.Writer
}
// NewConfig creates a Config and reads environment variables into it, accounting for several possible formats.
func NewConfig(stdout, stderr io.Writer, argv ...string) (*Config, error) {
cfg := Config{
Stdout: stdout,
Stderr: stderr,
}
// cli doesn't support Destination for string slices, so we'll use bare
// strings as an intermediate value and split them on commas ourselves.
var addRepos, valuesFiles string
app := &cli.App{
Name: "drone-helm3",
Action: func(*cli.Context) error { return nil },
Flags: []cli.Flag{
&cli.StringFlag{
Name: "mode",
Destination: &cfg.Command,
EnvVars: []string{"MODE", "PLUGIN_MODE", "HELM_COMMAND", "PLUGIN_HELM_COMMAND"},
},
&cli.StringFlag{
Name: "drone-event",
Destination: &cfg.DroneEvent,
EnvVars: []string{"DRONE_BUILD_EVENT"},
},
&cli.BoolFlag{
Name: "update-dependencies",
Destination: &cfg.UpdateDependencies,
EnvVars: []string{"UPDATE_DEPENDENCIES", "PLUGIN_UPDATE_DEPENDENCIES"},
},
&cli.StringFlag{
Name: "add-repos",
Destination: &addRepos,
EnvVars: []string{"ADD_REPOS", "PLUGIN_ADD_REPOS", "HELM_REPOS", "PLUGIN_HELM_REPOS"},
},
&cli.BoolFlag{
Name: "debug",
Destination: &cfg.Debug,
EnvVars: []string{"DEBUG", "PLUGIN_DEBUG"},
},
&cli.StringFlag{
Name: "values",
Destination: &cfg.Values,
EnvVars: []string{"VALUES", "PLUGIN_VALUES"},
},
&cli.StringFlag{
Name: "string-values",
Destination: &cfg.StringValues,
EnvVars: []string{"STRING_VALUES", "PLUGIN_STRING_VALUES"},
},
&cli.StringFlag{
Name: "values-files",
Destination: &valuesFiles,
EnvVars: []string{"VALUES_FILES", "PLUGIN_VALUES_FILES"},
},
&cli.StringFlag{
Name: "namespace",
Destination: &cfg.Namespace,
EnvVars: []string{"NAMESPACE", "PLUGIN_NAMESPACE"},
},
&cli.StringFlag{
Name: "kube-token",
Destination: &cfg.KubeToken,
EnvVars: []string{"KUBE_TOKEN", "PLUGIN_KUBE_TOKEN", "KUBERNETES_TOKEN", "PLUGIN_KUBERNETES_TOKEN"},
},
&cli.BoolFlag{
Name: "skip-tls-verify",
Destination: &cfg.SkipTLSVerify,
EnvVars: []string{"SKIP_TLS_VERIFY", "PLUGIN_SKIP_TLS_VERIFY"},
},
&cli.StringFlag{
Name: "kube-certificate",
Destination: &cfg.Certificate,
EnvVars: []string{"KUBE_CERTIFICATE", "PLUGIN_KUBE_CERTIFICATE", "KUBERNETES_CERTIFICATE", "PLUGIN_KUBERNETES_CERTIFICATE"},
},
&cli.StringFlag{
Name: "kube-api-server",
Destination: &cfg.APIServer,
EnvVars: []string{"KUBE_API_SERVER", "PLUGIN_KUBE_API_SERVER", "API_SERVER", "PLUGIN_API_SERVER"},
},
&cli.StringFlag{
Name: "service-account",
Destination: &cfg.ServiceAccount,
EnvVars: []string{"KUBE_SERVICE_ACCOUNT", "PLUGIN_KUBE_SERVICE_ACCOUNT", "SERVICE_ACCOUNT", "PLUGIN_SERVICE_ACCOUNT"},
},
&cli.StringFlag{
Name: "chart-version",
Destination: &cfg.ChartVersion,
EnvVars: []string{"CHART_VERSION", "PLUGIN_CHART_VERSION"},
},
&cli.BoolFlag{
Name: "dry-run",
Destination: &cfg.DryRun,
EnvVars: []string{"DRY_RUN", "PLUGIN_DRY_RUN"},
},
&cli.BoolFlag{
Name: "wait-for-upgrade",
Destination: &cfg.Wait,
EnvVars: []string{"WAIT_FOR_UPGRADE", "PLUGIN_WAIT_FOR_UPGRADE", "WAIT", "PLUGIN_WAIT"},
},
&cli.BoolFlag{
Name: "reuse-values",
Destination: &cfg.ReuseValues,
EnvVars: []string{"REUSE_VALUES", "PLUGIN_REUSE_VALUES"},
},
&cli.BoolFlag{
Name: "keep-history",
Destination: &cfg.KeepHistory,
EnvVars: []string{"KEEP_HISTORY", "PLUGIN_KEEP_HISTORY"},
},
&cli.StringFlag{
Name: "timeout",
Destination: &cfg.Timeout,
EnvVars: []string{"TIMEOUT", "PLUGIN_TIMEOUT"},
},
&cli.StringFlag{
Name: "chart",
Destination: &cfg.Chart,
EnvVars: []string{"CHART", "PLUGIN_CHART"},
},
&cli.StringFlag{
Name: "release",
Destination: &cfg.Release,
EnvVars: []string{"RELEASE", "PLUGIN_RELEASE"},
},
&cli.BoolFlag{
Name: "force-upgrade",
Destination: &cfg.Force,
EnvVars: []string{"FORCE_UPGRADE", "PLUGIN_FORCE_UPGRADE", "FORCE", "PLUGIN_FORCE"},
},
&cli.BoolFlag{
Name: "atomic-upgrade",
Destination: &cfg.AtomicUpgrade,
EnvVars: []string{"ATOMIC_UPGRADE", "PLUGIN_ATOMIC_UPGRADE"},
},
&cli.BoolFlag{
Name: "cleanup-failed-upgrade",
Destination: &cfg.CleanupOnFail,
EnvVars: []string{"CLEANUP_FAILED_UPGRADE", "PLUGIN_CLEANUP_FAILED_UPGRADE"},
},
&cli.BoolFlag{
Name: "lint-strictly",
Destination: &cfg.LintStrictly,
EnvVars: []string{"LINT_STRICTLY", "PLUGIN_LINT_STRICTLY"},
},
},
}
if err := app.Run(argv); err != nil {
return nil, err
}
if addRepos != "" {
cfg.AddRepos = strings.Split(addRepos, ",")
}
if valuesFiles != "" {
cfg.ValuesFiles = strings.Split(valuesFiles, ",")
}
if justNumbers.MatchString(cfg.Timeout) {
cfg.Timeout = fmt.Sprintf("%ss", cfg.Timeout)
}
if cfg.Debug && cfg.Stderr != nil {
cfg.logDebug()
}
cfg.deprecationWarn()
return &cfg, nil
}
func (cfg Config) logDebug() {
if cfg.KubeToken != "" {
cfg.KubeToken = "(redacted)"
}
fmt.Fprintf(cfg.Stderr, "Generated config: %+v\n", cfg)
}
func (cfg *Config) deprecationWarn() {
for _, varname := range deprecatedVars {
_, barePresent := os.LookupEnv(varname)
_, prefixedPresent := os.LookupEnv("PLUGIN_" + varname)
if barePresent || prefixedPresent {
fmt.Fprintf(cfg.Stderr, "Warning: ignoring deprecated '%s' setting\n", strings.ToLower(varname))
}
}
}

View File

@@ -0,0 +1,410 @@
package helm
import (
"fmt"
"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) TestNewConfigWithPluginPrefix() {
stdout := strings.Builder{}
stderr := strings.Builder{}
for _, varname := range []string{
"MODE",
"DRONE_BUILD_EVENT",
"HELM_COMMAND",
"PLUGIN_HELM_COMMAND",
"UPDATE_DEPENDENCIES",
"ADD_REPOS",
"HELM_REPOS",
"PLUGIN_HELM_REPOS",
"DEBUG",
"VALUES",
"STRING_VALUES",
"VALUES_FILES",
"NAMESPACE",
"KUBE_TOKEN",
"KUBERNETES_TOKEN",
"PLUGIN_KUBERNETES_TOKEN",
"SKIP_TLS_VERIFY",
"KUBE_CERTIFICATE",
"KUBERNETES_CERTIFICATE",
"PLUGIN_KUBERNETES_CERTIFICATE",
"KUBE_API_SERVER",
"API_SERVER",
"PLUGIN_API_SERVER",
"KUBE_SERVICE_ACCOUNT",
"SERVICE_ACCOUNT",
"PLUGIN_SERVICE_ACCOUNT",
"CHART_VERSION",
"DRY_RUN",
"WAIT_FOR_UPGRADE",
"WAIT",
"PLUGIN_WAIT",
"REUSE_VALUES",
"KEEP_HISTORY",
"TIMEOUT",
"CHART",
"RELEASE",
"FORCE",
"FORCE_UPGRADE",
"PLUGIN_FORCE_UPGRADE",
"ATOMIC_UPGRADE",
"CLEANUP_FAILED_UPGRADE",
"LINT_STRICTLY",
} {
suite.unsetenv(varname)
}
suite.setenv("PLUGIN_MODE", "upgrade")
suite.setenv("PLUGIN_UPDATE_DEPENDENCIES", "true")
suite.setenv("PLUGIN_ADD_REPOS", "foo=http://bar,goo=http://baz")
suite.setenv("PLUGIN_DEBUG", "true")
suite.setenv("PLUGIN_VALUES", "dog=husky")
suite.setenv("PLUGIN_STRING_VALUES", "version=1.0")
suite.setenv("PLUGIN_VALUES_FILES", "underrides.yml,overrides.yml")
suite.setenv("PLUGIN_NAMESPACE", "myapp")
suite.setenv("PLUGIN_KUBE_TOKEN", "cGxlYXNlIHNpciwgbGV0IG1lIGlu")
suite.setenv("PLUGIN_SKIP_TLS_VERIFY", "true")
suite.setenv("PLUGIN_KUBE_CERTIFICATE", "SSBhbSB0b3RhbGx5IHRoZSBzZXJ2ZXIgeW91IHdhbnQ=")
suite.setenv("PLUGIN_KUBE_API_SERVER", "http://my.kube/cluster")
suite.setenv("PLUGIN_KUBE_SERVICE_ACCOUNT", "deploybot")
suite.setenv("PLUGIN_CHART_VERSION", "six")
suite.setenv("PLUGIN_DRY_RUN", "true")
suite.setenv("PLUGIN_WAIT_FOR_UPGRADE", "true")
suite.setenv("PLUGIN_REUSE_VALUES", "true")
suite.setenv("PLUGIN_KEEP_HISTORY", "true")
suite.setenv("PLUGIN_TIMEOUT", "5m20s")
suite.setenv("PLUGIN_CHART", "./helm/myapp/")
suite.setenv("PLUGIN_RELEASE", "my_app")
suite.setenv("PLUGIN_FORCE_UPGRADE", "true")
suite.setenv("PLUGIN_ATOMIC_UPGRADE", "true")
suite.setenv("PLUGIN_CLEANUP_FAILED_UPGRADE", "true")
suite.setenv("PLUGIN_LINT_STRICTLY", "true")
cfg, err := NewConfig(&stdout, &stderr, "test")
suite.Require().NoError(err)
want := Config{
Command: "upgrade",
DroneEvent: "",
UpdateDependencies: true,
AddRepos: []string{"foo=http://bar", "goo=http://baz"},
Debug: true,
Values: "dog=husky",
StringValues: "version=1.0",
ValuesFiles: []string{"underrides.yml", "overrides.yml"},
Namespace: "myapp",
KubeToken: "cGxlYXNlIHNpciwgbGV0IG1lIGlu",
SkipTLSVerify: true,
Certificate: "SSBhbSB0b3RhbGx5IHRoZSBzZXJ2ZXIgeW91IHdhbnQ=",
APIServer: "http://my.kube/cluster",
ServiceAccount: "deploybot",
ChartVersion: "six",
DryRun: true,
Wait: true,
ReuseValues: true,
KeepHistory: true,
Timeout: "5m20s",
Chart: "./helm/myapp/",
Release: "my_app",
Force: true,
AtomicUpgrade: true,
CleanupOnFail: true,
LintStrictly: true,
Stdout: &stdout,
Stderr: &stderr,
}
suite.Equal(&want, cfg)
}
func (suite *ConfigTestSuite) TestNewConfigWithNoPrefix() {
stdout := strings.Builder{}
stderr := strings.Builder{}
for _, varname := range []string{
"PLUGIN_MODE",
"PLUGIN_HELM_COMMAND",
"HELM_COMMAND",
"PLUGIN_UPDATE_DEPENDENCIES",
"PLUGIN_ADD_REPOS",
"PLUGIN_HELM_REPOS",
"HELM_REPOS",
"PLUGIN_DEBUG",
"PLUGIN_VALUES",
"PLUGIN_STRING_VALUES",
"PLUGIN_VALUES_FILES",
"PLUGIN_NAMESPACE",
"PLUGIN_KUBE_TOKEN",
"PLUGIN_KUBERNETES_TOKEN",
"KUBERNETES_TOKEN",
"PLUGIN_SKIP_TLS_VERIFY",
"PLUGIN_KUBE_CERTIFICATE",
"PLUGIN_KUBERNETES_CERTIFICATE",
"KUBERNETES_CERTIFICATE",
"PLUGIN_KUBE_API_SERVER",
"PLUGIN_API_SERVER",
"API_SERVER",
"PLUGIN_KUBE_SERVICE_ACCOUNT",
"PLUGIN_SERVICE_ACCOUNT",
"SERVICE_ACCOUNT",
"PLUGIN_CHART_VERSION",
"PLUGIN_DRY_RUN",
"PLUGIN_WAIT_FOR_UPGRADE",
"PLUGIN_WAIT",
"WAIT",
"PLUGIN_REUSE_VALUES",
"PLUGIN_KEEP_HISTORY",
"PLUGIN_TIMEOUT",
"PLUGIN_CHART",
"PLUGIN_RELEASE",
"PLUGIN_FORCE",
"PLUGIN_FORCE_UPGRADE",
"FORCE_UPGRADE",
"PLUGIN_ATOMIC_UPGRADE",
"PLUGIN_CLEANUP_FAILED_UPGRADE",
"PLUGIN_LINT_STRICTLY",
} {
suite.unsetenv(varname)
}
suite.setenv("MODE", "upgrade")
suite.setenv("DRONE_BUILD_EVENT", "tag")
suite.setenv("UPDATE_DEPENDENCIES", "true")
suite.setenv("ADD_REPOS", "foo=http://bar,goo=http://baz")
suite.setenv("DEBUG", "true")
suite.setenv("VALUES", "dog=husky")
suite.setenv("STRING_VALUES", "version=1.0")
suite.setenv("VALUES_FILES", "underrides.yml,overrides.yml")
suite.setenv("NAMESPACE", "myapp")
suite.setenv("KUBE_TOKEN", "cGxlYXNlIHNpciwgbGV0IG1lIGlu")
suite.setenv("SKIP_TLS_VERIFY", "true")
suite.setenv("KUBE_CERTIFICATE", "SSBhbSB0b3RhbGx5IHRoZSBzZXJ2ZXIgeW91IHdhbnQ=")
suite.setenv("KUBE_API_SERVER", "http://my.kube/cluster")
suite.setenv("KUBE_SERVICE_ACCOUNT", "deploybot")
suite.setenv("CHART_VERSION", "six")
suite.setenv("DRY_RUN", "true")
suite.setenv("WAIT_FOR_UPGRADE", "true")
suite.setenv("REUSE_VALUES", "true")
suite.setenv("KEEP_HISTORY", "true")
suite.setenv("TIMEOUT", "5m20s")
suite.setenv("CHART", "./helm/myapp/")
suite.setenv("RELEASE", "my_app")
suite.setenv("FORCE_UPGRADE", "true")
suite.setenv("ATOMIC_UPGRADE", "true")
suite.setenv("CLEANUP_FAILED_UPGRADE", "true")
suite.setenv("LINT_STRICTLY", "true")
cfg, err := NewConfig(&stdout, &stderr, "test")
suite.Require().NoError(err)
want := Config{
Command: "upgrade",
DroneEvent: "tag",
UpdateDependencies: true,
AddRepos: []string{"foo=http://bar", "goo=http://baz"},
Debug: true,
Values: "dog=husky",
StringValues: "version=1.0",
ValuesFiles: []string{"underrides.yml", "overrides.yml"},
Namespace: "myapp",
KubeToken: "cGxlYXNlIHNpciwgbGV0IG1lIGlu",
SkipTLSVerify: true,
Certificate: "SSBhbSB0b3RhbGx5IHRoZSBzZXJ2ZXIgeW91IHdhbnQ=",
APIServer: "http://my.kube/cluster",
ServiceAccount: "deploybot",
ChartVersion: "six",
DryRun: true,
Wait: true,
ReuseValues: true,
KeepHistory: true,
Timeout: "5m20s",
Chart: "./helm/myapp/",
Release: "my_app",
Force: true,
AtomicUpgrade: true,
CleanupOnFail: true,
LintStrictly: true,
Stdout: &stdout,
Stderr: &stderr,
}
suite.Equal(&want, cfg)
}
func (suite *ConfigTestSuite) TestNewConfigWithConflictingVariables() {
suite.setenv("PLUGIN_MODE", "iambic")
suite.setenv("MODE", "haiku") // values from the `environment` block override those from `settings`
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{}, "test")
suite.Require().NoError(err)
suite.Equal("haiku", cfg.Command)
}
func (suite *ConfigTestSuite) TestNewConfigInfersNumbersAreSeconds() {
suite.setenv("PLUGIN_TIMEOUT", "42")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{}, "test")
suite.Require().NoError(err)
suite.Equal("42s", cfg.Timeout)
}
func (suite *ConfigTestSuite) TestNewConfigWithAliases() {
for _, varname := range []string{
"MODE",
"ADD_REPOS",
"KUBE_API_SERVER",
"KUBE_SERVICE_ACCOUNT",
"WAIT_FOR_UPGRADE",
"FORCE_UPGRADE",
"KUBE_TOKEN",
"KUBE_CERTIFICATE",
} {
suite.unsetenv(varname)
suite.unsetenv("PLUGIN_" + varname)
}
suite.setenv("PLUGIN_HELM_COMMAND", "beware the jabberwock")
suite.setenv("PLUGIN_HELM_REPOS", "chortle=http://calloo.callay/frabjous/day")
suite.setenv("PLUGIN_API_SERVER", "http://tumtum.tree")
suite.setenv("PLUGIN_SERVICE_ACCOUNT", "tulgey")
suite.setenv("PLUGIN_WAIT", "true")
suite.setenv("PLUGIN_FORCE", "true")
suite.setenv("PLUGIN_KUBERNETES_TOKEN", "Y29tZSB0byBteSBhcm1z")
suite.setenv("PLUGIN_KUBERNETES_CERTIFICATE", "d2l0aCBpdHMgaGVhZA==")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{}, "test")
suite.Require().NoError(err)
suite.Equal("beware the jabberwock", cfg.Command)
suite.Equal([]string{"chortle=http://calloo.callay/frabjous/day"}, cfg.AddRepos)
suite.Equal("http://tumtum.tree", cfg.APIServer)
suite.Equal("tulgey", cfg.ServiceAccount)
suite.True(cfg.Wait, "Wait should be aliased")
suite.True(cfg.Force, "Force should be aliased")
suite.Equal("Y29tZSB0byBteSBhcm1z", cfg.KubeToken, "KubeToken should be aliased")
suite.Equal("d2l0aCBpdHMgaGVhZA==", cfg.Certificate, "Certificate should be aliased")
}
func (suite *ConfigTestSuite) TestAliasedSettingWithoutPluginPrefix() {
suite.unsetenv("FORCE_UPGRADE")
suite.unsetenv("PLUGIN_FORCE_UPGRADE")
suite.unsetenv("PLUGIN_FORCE")
suite.setenv("FORCE", "true")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{}, "test")
suite.Require().NoError(err)
suite.True(cfg.Force)
}
func (suite *ConfigTestSuite) TestNewConfigWithAliasConflicts() {
suite.unsetenv("FORCE_UPGRADE")
suite.setenv("PLUGIN_FORCE", "true")
suite.setenv("PLUGIN_FORCE_UPGRADE", "false") // should override even when set to the zero value
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{}, "test")
suite.NoError(err)
suite.False(cfg.Force, "official names should override alias names")
}
func (suite *ConfigTestSuite) TestNewConfigSetsWriters() {
stdout := &strings.Builder{}
stderr := &strings.Builder{}
cfg, err := NewConfig(stdout, stderr, "test")
suite.Require().NoError(err)
suite.Equal(stdout, cfg.Stdout)
suite.Equal(stderr, cfg.Stderr)
}
func (suite *ConfigTestSuite) TestDeprecatedSettingWarnings() {
for _, varname := range deprecatedVars {
suite.setenv(varname, "deprecoat") // environment-block entries should cause warnings
}
suite.unsetenv("PURGE")
suite.setenv("PLUGIN_PURGE", "true") // settings-block entries should cause warnings
suite.setenv("UPGRADE", "") // entries should cause warnings even when set to empty string
stderr := &strings.Builder{}
_, err := NewConfig(&strings.Builder{}, stderr, "test")
suite.NoError(err)
for _, varname := range deprecatedVars {
suite.Contains(stderr.String(), fmt.Sprintf("Warning: ignoring deprecated '%s' setting\n", strings.ToLower(varname)))
}
}
func (suite *ConfigTestSuite) TestLogDebug() {
suite.setenv("DEBUG", "true")
suite.setenv("MODE", "upgrade")
stderr := strings.Builder{}
stdout := strings.Builder{}
_, err := NewConfig(&stdout, &stderr, "test")
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,6 +6,7 @@ package helm
import ( import (
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
run "github.com/pelotech/drone-helm3/internal/run"
reflect "reflect" reflect "reflect"
) )
@@ -33,29 +34,29 @@ func (m *MockStep) EXPECT() *MockStepMockRecorder {
} }
// Prepare mocks base method // Prepare mocks base method
func (m *MockStep) Prepare() error { func (m *MockStep) Prepare(arg0 run.Config) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Prepare") ret := m.ctrl.Call(m, "Prepare", arg0)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// Prepare indicates an expected call of Prepare // Prepare indicates an expected call of Prepare
func (mr *MockStepMockRecorder) Prepare() *gomock.Call { func (mr *MockStepMockRecorder) Prepare(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockStep)(nil).Prepare)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockStep)(nil).Prepare), arg0)
} }
// Execute mocks base method // Execute mocks base method
func (m *MockStep) Execute() error { func (m *MockStep) Execute(arg0 run.Config) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Execute") ret := m.ctrl.Call(m, "Execute", arg0)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// Execute indicates an expected call of Execute // Execute indicates an expected call of Execute
func (mr *MockStepMockRecorder) Execute() *gomock.Call { func (mr *MockStepMockRecorder) Execute(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockStep)(nil).Execute)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockStep)(nil).Execute), arg0)
} }

View File

@@ -1,9 +1,7 @@
package helm package helm
import ( import (
"errors"
"fmt" "fmt"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/pelotech/drone-helm3/internal/run" "github.com/pelotech/drone-helm3/internal/run"
"os" "os"
) )
@@ -15,24 +13,27 @@ const (
// A Step is one step in the plan. // A Step is one step in the plan.
type Step interface { type Step interface {
Prepare() error Prepare(run.Config) error
Execute() error Execute(run.Config) error
} }
// A Plan is a series of steps to perform. // A Plan is a series of steps to perform.
type Plan struct { type Plan struct {
steps []Step steps []Step
cfg env.Config cfg Config
runCfg run.Config
} }
// NewPlan makes a plan for running a helm operation. // NewPlan makes a plan for running a helm operation.
func NewPlan(cfg env.Config) (*Plan, error) { func NewPlan(cfg Config) (*Plan, error) {
p := Plan{ p := Plan{
cfg: cfg, cfg: cfg,
} runCfg: run.Config{
Debug: cfg.Debug,
if cfg.UpdateDependencies && cfg.DependenciesAction != "" { Namespace: cfg.Namespace,
return nil, errors.New("update_dependencies is deprecated and cannot be provided together with dependencies_action") Stdout: cfg.Stdout,
Stderr: cfg.Stderr,
},
} }
p.steps = (*determineSteps(cfg))(cfg) p.steps = (*determineSteps(cfg))(cfg)
@@ -42,7 +43,7 @@ func NewPlan(cfg env.Config) (*Plan, error) {
fmt.Fprintf(os.Stderr, "calling %T.Prepare (step %d)\n", step, i) fmt.Fprintf(os.Stderr, "calling %T.Prepare (step %d)\n", step, i)
} }
if err := step.Prepare(); err != nil { if err := step.Prepare(p.runCfg); err != nil {
err = fmt.Errorf("while preparing %T step: %w", step, err) err = fmt.Errorf("while preparing %T step: %w", step, err)
return nil, err return nil, err
} }
@@ -53,7 +54,7 @@ func NewPlan(cfg env.Config) (*Plan, error) {
// determineSteps is primarily for the tests' convenience: it allows testing the "which stuff should // determineSteps is primarily for the tests' convenience: it allows testing the "which stuff should
// we do" logic without building a config that meets all the steps' requirements. // we do" logic without building a config that meets all the steps' requirements.
func determineSteps(cfg env.Config) *func(env.Config) []Step { func determineSteps(cfg Config) *func(Config) []Step {
switch cfg.Command { switch cfg.Command {
case "upgrade": case "upgrade":
return &upgrade return &upgrade
@@ -82,7 +83,7 @@ func (p *Plan) Execute() error {
fmt.Fprintf(p.cfg.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(); err != nil { if err := step.Execute(p.runCfg); err != nil {
return fmt.Errorf("while executing %T step: %w", step, err) return fmt.Errorf("while executing %T step: %w", step, err)
} }
} }
@@ -90,53 +91,97 @@ func (p *Plan) Execute() error {
return nil return nil
} }
var upgrade = func(cfg env.Config) []Step { var upgrade = func(cfg Config) []Step {
var steps []Step steps := initKube(cfg)
if !cfg.SkipKubeconfig { steps = append(steps, addRepos(cfg)...)
steps = append(steps, run.NewInitKube(cfg, kubeConfigTemplate, kubeConfigFile)) if cfg.UpdateDependencies {
steps = append(steps, depUpdate(cfg)...)
} }
steps = append(steps, &run.Upgrade{
Chart: cfg.Chart,
Release: cfg.Release,
ChartVersion: cfg.ChartVersion,
DryRun: cfg.DryRun,
Wait: cfg.Wait,
Values: cfg.Values,
StringValues: cfg.StringValues,
ValuesFiles: cfg.ValuesFiles,
ReuseValues: cfg.ReuseValues,
Timeout: cfg.Timeout,
Force: cfg.Force,
Atomic: cfg.AtomicUpgrade,
CleanupOnFail: cfg.CleanupOnFail,
})
return steps
}
var uninstall = func(cfg Config) []Step {
steps := initKube(cfg)
if cfg.UpdateDependencies {
steps = append(steps, depUpdate(cfg)...)
}
steps = append(steps, &run.Uninstall{
Release: cfg.Release,
DryRun: cfg.DryRun,
KeepHistory: cfg.KeepHistory,
})
return steps
}
var lint = func(cfg Config) []Step {
steps := addRepos(cfg)
if cfg.UpdateDependencies {
steps = append(steps, depUpdate(cfg)...)
}
steps = append(steps, &run.Lint{
Chart: cfg.Chart,
Values: cfg.Values,
StringValues: cfg.StringValues,
ValuesFiles: cfg.ValuesFiles,
Strict: cfg.LintStrictly,
})
return steps
}
var help = func(cfg Config) []Step {
help := &run.Help{
HelmCommand: cfg.Command,
}
return []Step{help}
}
func initKube(cfg Config) []Step {
return []Step{
&run.InitKube{
SkipTLSVerify: cfg.SkipTLSVerify,
Certificate: cfg.Certificate,
APIServer: cfg.APIServer,
ServiceAccount: cfg.ServiceAccount,
Token: cfg.KubeToken,
TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
},
}
}
func addRepos(cfg Config) []Step {
steps := make([]Step, 0)
for _, repo := range cfg.AddRepos { for _, repo := range cfg.AddRepos {
steps = append(steps, run.NewAddRepo(cfg, repo)) steps = append(steps, &run.AddRepo{
Repo: repo,
})
} }
if cfg.DependenciesAction != "" {
steps = append(steps, run.NewDepAction(cfg))
}
if cfg.UpdateDependencies {
steps = append(steps, run.NewDepUpdate(cfg))
}
steps = append(steps, run.NewUpgrade(cfg))
return steps return steps
} }
var uninstall = func(cfg env.Config) []Step { func depUpdate(cfg Config) []Step {
var steps []Step return []Step{
if !cfg.SkipKubeconfig { &run.DepUpdate{
steps = append(steps, run.NewInitKube(cfg, kubeConfigTemplate, kubeConfigFile)) Chart: cfg.Chart,
},
} }
if cfg.UpdateDependencies {
steps = append(steps, run.NewDepUpdate(cfg))
}
steps = append(steps, run.NewUninstall(cfg))
return steps
}
var lint = func(cfg env.Config) []Step {
var steps []Step
for _, repo := range cfg.AddRepos {
steps = append(steps, run.NewAddRepo(cfg, repo))
}
if cfg.UpdateDependencies {
steps = append(steps, run.NewDepUpdate(cfg))
}
steps = append(steps, run.NewLint(cfg))
return steps
}
var help = func(cfg env.Config) []Step {
return []Step{run.NewHelp(cfg)}
} }

View File

@@ -7,7 +7,6 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/pelotech/drone-helm3/internal/run" "github.com/pelotech/drone-helm3/internal/run"
) )
@@ -26,14 +25,14 @@ func (suite *PlanTestSuite) TestNewPlan() {
stepTwo := NewMockStep(ctrl) stepTwo := NewMockStep(ctrl)
origHelp := help origHelp := help
help = func(cfg env.Config) []Step { help = func(cfg Config) []Step {
return []Step{stepOne, stepTwo} return []Step{stepOne, stepTwo}
} }
defer func() { help = origHelp }() defer func() { help = origHelp }()
stdout := strings.Builder{} stdout := strings.Builder{}
stderr := strings.Builder{} stderr := strings.Builder{}
cfg := env.Config{ cfg := Config{
Command: "help", Command: "help",
Debug: false, Debug: false,
Namespace: "outer", Namespace: "outer",
@@ -41,14 +40,22 @@ func (suite *PlanTestSuite) TestNewPlan() {
Stderr: &stderr, Stderr: &stderr,
} }
runCfg := run.Config{
Debug: false,
Namespace: "outer",
Stdout: &stdout,
Stderr: &stderr,
}
stepOne.EXPECT(). stepOne.EXPECT().
Prepare() Prepare(runCfg)
stepTwo.EXPECT(). stepTwo.EXPECT().
Prepare() Prepare(runCfg)
plan, err := NewPlan(cfg) plan, err := NewPlan(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
suite.Equal(cfg, plan.cfg) suite.Equal(cfg, plan.cfg)
suite.Equal(runCfg, plan.runCfg)
} }
func (suite *PlanTestSuite) TestNewPlanAbortsOnError() { func (suite *PlanTestSuite) TestNewPlanAbortsOnError() {
@@ -58,17 +65,17 @@ func (suite *PlanTestSuite) TestNewPlanAbortsOnError() {
stepTwo := NewMockStep(ctrl) stepTwo := NewMockStep(ctrl)
origHelp := help origHelp := help
help = func(cfg env.Config) []Step { help = func(cfg Config) []Step {
return []Step{stepOne, stepTwo} return []Step{stepOne, stepTwo}
} }
defer func() { help = origHelp }() defer func() { help = origHelp }()
cfg := env.Config{ cfg := Config{
Command: "help", Command: "help",
} }
stepOne.EXPECT(). stepOne.EXPECT().
Prepare(). Prepare(gomock.Any()).
Return(fmt.Errorf("I'm starry Dave, aye, cat blew that")) Return(fmt.Errorf("I'm starry Dave, aye, cat blew that"))
_, err := NewPlan(cfg) _, err := NewPlan(cfg)
@@ -82,15 +89,18 @@ func (suite *PlanTestSuite) TestExecute() {
stepOne := NewMockStep(ctrl) stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl) stepTwo := NewMockStep(ctrl)
runCfg := run.Config{}
plan := Plan{ plan := Plan{
steps: []Step{stepOne, stepTwo}, steps: []Step{stepOne, stepTwo},
runCfg: runCfg,
} }
stepOne.EXPECT(). stepOne.EXPECT().
Execute(). Execute(runCfg).
Times(1) Times(1)
stepTwo.EXPECT(). stepTwo.EXPECT().
Execute(). Execute(runCfg).
Times(1) Times(1)
suite.NoError(plan.Execute()) suite.NoError(plan.Execute())
@@ -102,12 +112,15 @@ func (suite *PlanTestSuite) TestExecuteAbortsOnError() {
stepOne := NewMockStep(ctrl) stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl) stepTwo := NewMockStep(ctrl)
runCfg := run.Config{}
plan := Plan{ plan := Plan{
steps: []Step{stepOne, stepTwo}, steps: []Step{stepOne, stepTwo},
runCfg: runCfg,
} }
stepOne.EXPECT(). stepOne.EXPECT().
Execute(). Execute(runCfg).
Times(1). Times(1).
Return(fmt.Errorf("oh, he'll gnaw")) Return(fmt.Errorf("oh, he'll gnaw"))
@@ -116,20 +129,50 @@ func (suite *PlanTestSuite) TestExecuteAbortsOnError() {
} }
func (suite *PlanTestSuite) TestUpgrade() { func (suite *PlanTestSuite) TestUpgrade() {
steps := upgrade(env.Config{}) cfg := Config{
suite.Require().Equal(2, len(steps), "upgrade should return 2 steps") ChartVersion: "seventeen",
suite.IsType(&run.InitKube{}, steps[0]) DryRun: true,
suite.IsType(&run.Upgrade{}, steps[1]) Wait: true,
} Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
ReuseValues: true,
Timeout: "go sit in the corner",
Chart: "billboard_top_100",
Release: "post_malone_circles",
Force: true,
AtomicUpgrade: true,
CleanupOnFail: true,
}
func (suite *PlanTestSuite) TestUpgradeWithSkipKubeconfig() { steps := upgrade(cfg)
steps := upgrade(env.Config{SkipKubeconfig: true}) suite.Require().Equal(2, len(steps), "upgrade should return 2 steps")
suite.Require().Equal(1, len(steps), "upgrade should return 1 step") suite.Require().IsType(&run.InitKube{}, steps[0])
suite.IsType(&run.Upgrade{}, steps[0])
suite.Require().IsType(&run.Upgrade{}, steps[1])
upgrade, _ := steps[1].(*run.Upgrade)
expected := &run.Upgrade{
Chart: cfg.Chart,
Release: cfg.Release,
ChartVersion: cfg.ChartVersion,
DryRun: true,
Wait: cfg.Wait,
Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
ReuseValues: cfg.ReuseValues,
Timeout: cfg.Timeout,
Force: cfg.Force,
Atomic: true,
CleanupOnFail: true,
}
suite.Equal(expected, upgrade)
} }
func (suite *PlanTestSuite) TestUpgradeWithUpdateDependencies() { func (suite *PlanTestSuite) TestUpgradeWithUpdateDependencies() {
cfg := env.Config{ cfg := Config{
UpdateDependencies: true, UpdateDependencies: true,
} }
steps := upgrade(cfg) steps := upgrade(cfg)
@@ -139,7 +182,7 @@ func (suite *PlanTestSuite) TestUpgradeWithUpdateDependencies() {
} }
func (suite *PlanTestSuite) TestUpgradeWithAddRepos() { func (suite *PlanTestSuite) TestUpgradeWithAddRepos() {
cfg := env.Config{ cfg := Config{
AddRepos: []string{ AddRepos: []string{
"machine=https://github.com/harold_finch/themachine", "machine=https://github.com/harold_finch/themachine",
}, },
@@ -150,15 +193,47 @@ func (suite *PlanTestSuite) TestUpgradeWithAddRepos() {
} }
func (suite *PlanTestSuite) TestUninstall() { func (suite *PlanTestSuite) TestUninstall() {
steps := uninstall(env.Config{}) cfg := Config{
KubeToken: "b2YgbXkgYWZmZWN0aW9u",
SkipTLSVerify: true,
Certificate: "cHJvY2xhaW1zIHdvbmRlcmZ1bCBmcmllbmRzaGlw",
APIServer: "98.765.43.21",
ServiceAccount: "greathelm",
DryRun: true,
Timeout: "think about what you did",
Release: "jetta_id_love_to_change_the_world",
KeepHistory: true,
}
steps := uninstall(cfg)
suite.Require().Equal(2, len(steps), "uninstall should return 2 steps") suite.Require().Equal(2, len(steps), "uninstall should return 2 steps")
suite.IsType(&run.InitKube{}, steps[0]) suite.Require().IsType(&run.InitKube{}, steps[0])
suite.IsType(&run.Uninstall{}, steps[1]) init, _ := steps[0].(*run.InitKube)
var expected Step = &run.InitKube{
SkipTLSVerify: true,
Certificate: "cHJvY2xhaW1zIHdvbmRlcmZ1bCBmcmllbmRzaGlw",
APIServer: "98.765.43.21",
ServiceAccount: "greathelm",
Token: "b2YgbXkgYWZmZWN0aW9u",
TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
}
suite.Equal(expected, init)
suite.Require().IsType(&run.Uninstall{}, steps[1])
actual, _ := steps[1].(*run.Uninstall)
expected = &run.Uninstall{
Release: "jetta_id_love_to_change_the_world",
DryRun: true,
KeepHistory: true,
}
suite.Equal(expected, actual)
} }
func (suite *PlanTestSuite) TestUninstallWithUpdateDependencies() { func (suite *PlanTestSuite) TestUninstallWithUpdateDependencies() {
cfg := env.Config{ cfg := Config{
UpdateDependencies: true, UpdateDependencies: true,
} }
steps := uninstall(cfg) steps := uninstall(cfg)
@@ -167,14 +242,91 @@ func (suite *PlanTestSuite) TestUninstallWithUpdateDependencies() {
suite.IsType(&run.DepUpdate{}, steps[1]) suite.IsType(&run.DepUpdate{}, steps[1])
} }
func (suite *PlanTestSuite) TestInitKube() {
cfg := Config{
KubeToken: "cXVlZXIgY2hhcmFjdGVyCg==",
SkipTLSVerify: true,
Certificate: "b2Ygd29rZW5lc3MK",
APIServer: "123.456.78.9",
ServiceAccount: "helmet",
}
steps := initKube(cfg)
suite.Require().Equal(1, len(steps), "initKube should return one step")
suite.Require().IsType(&run.InitKube{}, steps[0])
init, _ := steps[0].(*run.InitKube)
expected := &run.InitKube{
SkipTLSVerify: true,
Certificate: "b2Ygd29rZW5lc3MK",
APIServer: "123.456.78.9",
ServiceAccount: "helmet",
Token: "cXVlZXIgY2hhcmFjdGVyCg==",
TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
}
suite.Equal(expected, init)
}
func (suite *PlanTestSuite) TestDepUpdate() {
cfg := Config{
UpdateDependencies: true,
Chart: "scatterplot",
}
steps := depUpdate(cfg)
suite.Require().Equal(1, len(steps), "depUpdate should return one step")
suite.Require().IsType(&run.DepUpdate{}, steps[0])
update, _ := steps[0].(*run.DepUpdate)
expected := &run.DepUpdate{
Chart: "scatterplot",
}
suite.Equal(expected, update)
}
func (suite *PlanTestSuite) TestAddRepos() {
cfg := Config{
AddRepos: []string{
"first=https://add.repos/one",
"second=https://add.repos/two",
},
}
steps := addRepos(cfg)
suite.Require().Equal(2, len(steps), "addRepos should add one step per repo")
suite.Require().IsType(&run.AddRepo{}, steps[0])
suite.Require().IsType(&run.AddRepo{}, steps[1])
first := steps[0].(*run.AddRepo)
second := steps[1].(*run.AddRepo)
suite.Equal(first.Repo, "first=https://add.repos/one")
suite.Equal(second.Repo, "second=https://add.repos/two")
}
func (suite *PlanTestSuite) TestLint() { func (suite *PlanTestSuite) TestLint() {
steps := lint(env.Config{}) cfg := Config{
suite.Require().Equal(1, len(steps)) Chart: "./flow",
suite.IsType(&run.Lint{}, steps[0]) Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
LintStrictly: true,
}
steps := lint(cfg)
suite.Equal(1, len(steps))
want := &run.Lint{
Chart: "./flow",
Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
Strict: true,
}
suite.Equal(want, steps[0])
} }
func (suite *PlanTestSuite) TestLintWithUpdateDependencies() { func (suite *PlanTestSuite) TestLintWithUpdateDependencies() {
cfg := env.Config{ cfg := Config{
UpdateDependencies: true, UpdateDependencies: true,
} }
steps := lint(cfg) steps := lint(cfg)
@@ -183,7 +335,7 @@ func (suite *PlanTestSuite) TestLintWithUpdateDependencies() {
} }
func (suite *PlanTestSuite) TestLintWithAddRepos() { func (suite *PlanTestSuite) TestLintWithAddRepos() {
cfg := env.Config{ cfg := Config{
AddRepos: []string{"friendczar=https://github.com/logan_pierce/friendczar"}, AddRepos: []string{"friendczar=https://github.com/logan_pierce/friendczar"},
} }
steps := lint(cfg) steps := lint(cfg)
@@ -192,7 +344,7 @@ func (suite *PlanTestSuite) TestLintWithAddRepos() {
} }
func (suite *PlanTestSuite) TestDeterminePlanUpgradeCommand() { func (suite *PlanTestSuite) TestDeterminePlanUpgradeCommand() {
cfg := env.Config{ cfg := Config{
Command: "upgrade", Command: "upgrade",
} }
stepsMaker := determineSteps(cfg) stepsMaker := determineSteps(cfg)
@@ -200,7 +352,7 @@ func (suite *PlanTestSuite) TestDeterminePlanUpgradeCommand() {
} }
func (suite *PlanTestSuite) TestDeterminePlanUpgradeFromDroneEvent() { func (suite *PlanTestSuite) TestDeterminePlanUpgradeFromDroneEvent() {
cfg := env.Config{} cfg := Config{}
upgradeEvents := []string{"push", "tag", "deployment", "pull_request", "promote", "rollback"} upgradeEvents := []string{"push", "tag", "deployment", "pull_request", "promote", "rollback"}
for _, event := range upgradeEvents { for _, event := range upgradeEvents {
@@ -211,7 +363,7 @@ func (suite *PlanTestSuite) TestDeterminePlanUpgradeFromDroneEvent() {
} }
func (suite *PlanTestSuite) TestDeterminePlanUninstallCommand() { func (suite *PlanTestSuite) TestDeterminePlanUninstallCommand() {
cfg := env.Config{ cfg := Config{
Command: "uninstall", Command: "uninstall",
} }
stepsMaker := determineSteps(cfg) stepsMaker := determineSteps(cfg)
@@ -220,7 +372,7 @@ func (suite *PlanTestSuite) TestDeterminePlanUninstallCommand() {
// helm_command = delete is provided as an alias for backward-compatibility with drone-helm // helm_command = delete is provided as an alias for backward-compatibility with drone-helm
func (suite *PlanTestSuite) TestDeterminePlanDeleteCommand() { func (suite *PlanTestSuite) TestDeterminePlanDeleteCommand() {
cfg := env.Config{ cfg := Config{
Command: "delete", Command: "delete",
} }
stepsMaker := determineSteps(cfg) stepsMaker := determineSteps(cfg)
@@ -228,7 +380,7 @@ func (suite *PlanTestSuite) TestDeterminePlanDeleteCommand() {
} }
func (suite *PlanTestSuite) TestDeterminePlanDeleteFromDroneEvent() { func (suite *PlanTestSuite) TestDeterminePlanDeleteFromDroneEvent() {
cfg := env.Config{ cfg := Config{
DroneEvent: "delete", DroneEvent: "delete",
} }
stepsMaker := determineSteps(cfg) stepsMaker := determineSteps(cfg)
@@ -236,7 +388,7 @@ func (suite *PlanTestSuite) TestDeterminePlanDeleteFromDroneEvent() {
} }
func (suite *PlanTestSuite) TestDeterminePlanLintCommand() { func (suite *PlanTestSuite) TestDeterminePlanLintCommand() {
cfg := env.Config{ cfg := Config{
Command: "lint", Command: "lint",
} }
@@ -245,7 +397,7 @@ func (suite *PlanTestSuite) TestDeterminePlanLintCommand() {
} }
func (suite *PlanTestSuite) TestDeterminePlanHelpCommand() { func (suite *PlanTestSuite) TestDeterminePlanHelpCommand() {
cfg := env.Config{ cfg := Config{
Command: "help", Command: "help",
} }

View File

@@ -2,60 +2,50 @@ package run
import ( import (
"fmt" "fmt"
"github.com/pelotech/drone-helm3/internal/env"
"strings" "strings"
) )
// AddRepo is an execution step that calls `helm repo add` when executed. // AddRepo is an execution step that calls `helm repo add` when executed.
type AddRepo struct { type AddRepo struct {
*config Repo string
repo string cmd cmd
certs *repoCerts
cmd cmd
}
// NewAddRepo creates an AddRepo for the given repo-spec. No validation is performed at this time.
func NewAddRepo(cfg env.Config, repo string) *AddRepo {
return &AddRepo{
config: newConfig(cfg),
repo: repo,
certs: newRepoCerts(cfg),
}
} }
// Execute executes the `helm repo add` command. // Execute executes the `helm repo add` command.
func (a *AddRepo) Execute() error { func (a *AddRepo) Execute(_ Config) error {
return a.cmd.Run() return a.cmd.Run()
} }
// Prepare gets the AddRepo ready to execute. // Prepare gets the AddRepo ready to execute.
func (a *AddRepo) Prepare() error { func (a *AddRepo) Prepare(cfg Config) error {
if a.repo == "" { if a.Repo == "" {
return fmt.Errorf("repo is required") return fmt.Errorf("repo is required")
} }
split := strings.SplitN(a.repo, "=", 2) split := strings.SplitN(a.Repo, "=", 2)
if len(split) != 2 { if len(split) != 2 {
return fmt.Errorf("bad repo spec '%s'", a.repo) return fmt.Errorf("bad repo spec '%s'", a.Repo)
}
if err := a.certs.write(); err != nil {
return err
} }
name := split[0] name := split[0]
url := split[1] url := split[1]
args := a.globalFlags() args := make([]string, 0)
args = append(args, "repo", "add")
args = append(args, a.certs.flags()...) if cfg.Namespace != "" {
args = append(args, name, url) args = append(args, "--namespace", cfg.Namespace)
}
if cfg.Debug {
args = append(args, "--debug")
}
args = append(args, "repo", "add", name, url)
a.cmd = command(helmBin, args...) a.cmd = command(helmBin, args...)
a.cmd.Stdout(a.stdout) a.cmd.Stdout(cfg.Stdout)
a.cmd.Stderr(a.stderr) a.cmd.Stderr(cfg.Stderr)
if a.debug { if cfg.Debug {
fmt.Fprintf(a.stderr, "Generated command: '%s'\n", a.cmd.String()) fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", a.cmd.String())
} }
return nil return nil

View File

@@ -1,8 +1,8 @@
package run package run
import ( import (
"fmt"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"strings" "strings"
"testing" "testing"
@@ -38,22 +38,16 @@ func TestAddRepoTestSuite(t *testing.T) {
suite.Run(t, new(AddRepoTestSuite)) suite.Run(t, new(AddRepoTestSuite))
} }
func (suite *AddRepoTestSuite) TestNewAddRepo() {
repo := NewAddRepo(env.Config{}, "picompress=https://github.com/caleb_phipps/picompress")
suite.Require().NotNil(repo)
suite.Equal("picompress=https://github.com/caleb_phipps/picompress", repo.repo)
suite.NotNil(repo.config)
suite.NotNil(repo.certs)
}
func (suite *AddRepoTestSuite) TestPrepareAndExecute() { func (suite *AddRepoTestSuite) TestPrepareAndExecute() {
stdout := strings.Builder{} stdout := strings.Builder{}
stderr := strings.Builder{} stderr := strings.Builder{}
cfg := env.Config{ cfg := Config{
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
} }
a := NewAddRepo(cfg, "edeath=https://github.com/n_marks/e-death") a := AddRepo{
Repo: "edeath=https://github.com/n_marks/e-death",
}
suite.mockCmd.EXPECT(). suite.mockCmd.EXPECT().
Stdout(&stdout). Stdout(&stdout).
@@ -62,7 +56,7 @@ func (suite *AddRepoTestSuite) TestPrepareAndExecute() {
Stderr(&stderr). Stderr(&stderr).
Times(1) Times(1)
suite.Require().NoError(a.Prepare()) suite.Require().NoError(a.Prepare(cfg))
suite.Equal(helmBin, suite.commandPath) suite.Equal(helmBin, suite.commandPath)
suite.Equal([]string{"repo", "add", "edeath", "https://github.com/n_marks/e-death"}, suite.commandArgs) suite.Equal([]string{"repo", "add", "edeath", "https://github.com/n_marks/e-death"}, suite.commandArgs)
@@ -70,7 +64,7 @@ func (suite *AddRepoTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
suite.Require().NoError(a.Execute()) suite.Require().NoError(a.Execute(cfg))
} }
@@ -78,35 +72,70 @@ func (suite *AddRepoTestSuite) TestPrepareRepoIsRequired() {
// These aren't really expected, but allowing them gives clearer test-failure messages // These aren't really expected, but allowing them gives clearer test-failure messages
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()
a := NewAddRepo(env.Config{}, "") cfg := Config{}
a := AddRepo{}
err := a.Prepare() err := a.Prepare(cfg)
suite.EqualError(err, "repo is required") suite.EqualError(err, "repo is required")
} }
func (suite *AddRepoTestSuite) TestPrepareMalformedRepo() { func (suite *AddRepoTestSuite) TestPrepareMalformedRepo() {
a := NewAddRepo(env.Config{}, "dwim") a := AddRepo{
err := a.Prepare() Repo: "dwim",
}
err := a.Prepare(Config{})
suite.EqualError(err, "bad repo spec 'dwim'") suite.EqualError(err, "bad repo spec 'dwim'")
} }
func (suite *AddRepoTestSuite) TestPrepareWithEqualSignInURL() { func (suite *AddRepoTestSuite) TestPrepareWithEqualSignInURL() {
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()
a := NewAddRepo(env.Config{}, "samaritan=https://github.com/arthur_claypool/samaritan?version=2.1") a := AddRepo{
suite.NoError(a.Prepare()) Repo: "samaritan=https://github.com/arthur_claypool/samaritan?version=2.1",
}
suite.NoError(a.Prepare(Config{}))
suite.Contains(suite.commandArgs, "https://github.com/arthur_claypool/samaritan?version=2.1") suite.Contains(suite.commandArgs, "https://github.com/arthur_claypool/samaritan?version=2.1")
} }
func (suite *AddRepoTestSuite) TestRepoAddFlags() { func (suite *AddRepoTestSuite) TestNamespaceFlag() {
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()
cfg := env.Config{} cfg := Config{
a := NewAddRepo(cfg, "machine=https://github.com/harold_finch/themachine") Namespace: "alliteration",
}
a := AddRepo{
Repo: "edeath=https://github.com/theater_guy/e-death",
}
// inject a ca cert filename so repoCerts won't create any files that we'd have to clean up suite.NoError(a.Prepare(cfg))
a.certs.caCertFilename = "./helm/reporepo.cert" suite.Equal(suite.commandPath, helmBin)
suite.NoError(a.Prepare()) suite.Equal(suite.commandArgs, []string{"--namespace", "alliteration",
suite.Equal([]string{"repo", "add", "--ca-file", "./helm/reporepo.cert", "repo", "add", "edeath", "https://github.com/theater_guy/e-death"})
"machine", "https://github.com/harold_finch/themachine"}, suite.commandArgs) }
func (suite *AddRepoTestSuite) TestDebugFlag() {
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
stderr := strings.Builder{}
command = func(path string, args ...string) cmd {
suite.mockCmd.EXPECT().
String().
Return(fmt.Sprintf("%s %s", path, strings.Join(args, " ")))
return suite.mockCmd
}
cfg := Config{
Debug: true,
Stderr: &stderr,
}
a := AddRepo{
Repo: "edeath=https://github.com/the_bug/e-death",
}
suite.Require().NoError(a.Prepare(cfg))
suite.Equal(fmt.Sprintf("Generated command: '%s --debug "+
"repo add edeath https://github.com/the_bug/e-death'\n", helmBin), stderr.String())
} }

View File

@@ -1,33 +1,13 @@
package run package run
import ( import (
"github.com/pelotech/drone-helm3/internal/env"
"io" "io"
) )
type config struct { // Config contains configuration applicable to all helm commands
debug bool type Config struct {
namespace string Debug bool
stdout io.Writer Namespace string
stderr io.Writer Stdout io.Writer
} Stderr io.Writer
func newConfig(cfg env.Config) *config {
return &config{
debug: cfg.Debug,
namespace: cfg.Namespace,
stdout: cfg.Stdout,
stderr: cfg.Stderr,
}
}
func (cfg *config) globalFlags() []string {
flags := []string{}
if cfg.debug {
flags = append(flags, "--debug")
}
if cfg.namespace != "" {
flags = append(flags, "--namespace", cfg.namespace)
}
return flags
} }

View File

@@ -1,48 +0,0 @@
package run
import (
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite"
"strings"
"testing"
)
type ConfigTestSuite struct {
suite.Suite
}
func TestConfigTestSuite(t *testing.T) {
suite.Run(t, new(ConfigTestSuite))
}
func (suite *ConfigTestSuite) TestNewConfig() {
stdout := &strings.Builder{}
stderr := &strings.Builder{}
envCfg := env.Config{
Namespace: "private",
Debug: true,
Stdout: stdout,
Stderr: stderr,
}
cfg := newConfig(envCfg)
suite.Require().NotNil(cfg)
suite.Equal(&config{
namespace: "private",
debug: true,
stdout: stdout,
stderr: stderr,
}, cfg)
}
func (suite *ConfigTestSuite) TestGlobalFlags() {
cfg := config{
debug: true,
namespace: "public",
}
flags := cfg.globalFlags()
suite.Equal([]string{"--debug", "--namespace", "public"}, flags)
cfg = config{}
flags = cfg.globalFlags()
suite.Equal([]string{}, flags)
}

View File

@@ -1,59 +0,0 @@
package run
import (
"errors"
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
)
const (
actionBuild = "build"
actionUpdate = "update"
)
// DepAction is an execution step that calls `helm dependency update` or `helm dependency build` when executed.
type DepAction struct {
*config
chart string
cmd cmd
action string
}
// NewDepAction creates a DepAction using fields from the given Config. No validation is performed at this time.
func NewDepAction(cfg env.Config) *DepAction {
return &DepAction{
config: newConfig(cfg),
chart: cfg.Chart,
action: cfg.DependenciesAction,
}
}
// Execute executes the `helm upgrade` command.
func (d *DepAction) Execute() error {
return d.cmd.Run()
}
// Prepare gets the DepAction ready to execute.
func (d *DepAction) Prepare() error {
if d.chart == "" {
return fmt.Errorf("chart is required")
}
args := d.globalFlags()
if d.action != actionBuild && d.action != actionUpdate {
return errors.New("unknown dependency_action: " + d.action)
}
args = append(args, "dependency", d.action, d.chart)
d.cmd = command(helmBin, args...)
d.cmd.Stdout(d.stdout)
d.cmd.Stderr(d.stderr)
if d.debug {
fmt.Fprintf(d.stderr, "Generated command: '%s'\n", d.cmd.String())
}
return nil
}

View File

@@ -1,131 +0,0 @@
package run
import (
"errors"
"github.com/golang/mock/gomock"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite"
"strings"
"testing"
)
type DepActionTestSuite struct {
suite.Suite
ctrl *gomock.Controller
mockCmd *Mockcmd
originalCommand func(string, ...string) cmd
}
func (suite *DepActionTestSuite) BeforeTest(_, _ string) {
suite.ctrl = gomock.NewController(suite.T())
suite.mockCmd = NewMockcmd(suite.ctrl)
suite.originalCommand = command
command = func(path string, args ...string) cmd { return suite.mockCmd }
}
func (suite *DepActionTestSuite) AfterTest(_, _ string) {
command = suite.originalCommand
}
func TestDepActionTestSuite(t *testing.T) {
suite.Run(t, new(DepActionTestSuite))
}
func (suite *DepActionTestSuite) TestNewDepAction() {
cfg := env.Config{
Chart: "scatterplot",
}
d := NewDepAction(cfg)
suite.Equal("scatterplot", d.chart)
}
func (suite *DepActionTestSuite) TestPrepareAndExecuteBuild() {
defer suite.ctrl.Finish()
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := env.Config{
Chart: "your_top_songs_2019",
Stdout: &stdout,
Stderr: &stderr,
DependenciesAction: "build",
}
command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path)
suite.Equal([]string{"dependency", "build", "your_top_songs_2019"}, args)
return suite.mockCmd
}
suite.mockCmd.EXPECT().
Stdout(&stdout)
suite.mockCmd.EXPECT().
Stderr(&stderr)
suite.mockCmd.EXPECT().
Run().
Times(1)
d := NewDepAction(cfg)
suite.Require().NoError(d.Prepare())
suite.NoError(d.Execute())
}
func (suite *DepActionTestSuite) TestPrepareAndExecuteUpdate() {
defer suite.ctrl.Finish()
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := env.Config{
Chart: "your_top_songs_2019",
Stdout: &stdout,
Stderr: &stderr,
DependenciesAction: "update",
}
command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path)
suite.Equal([]string{"dependency", "update", "your_top_songs_2019"}, args)
return suite.mockCmd
}
suite.mockCmd.EXPECT().
Stdout(&stdout)
suite.mockCmd.EXPECT().
Stderr(&stderr)
suite.mockCmd.EXPECT().
Run().
Times(1)
d := NewDepAction(cfg)
suite.Require().NoError(d.Prepare())
suite.NoError(d.Execute())
}
func (suite *DepActionTestSuite) TestPrepareAndExecuteUnknown() {
defer suite.ctrl.Finish()
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := env.Config{
Chart: "your_top_songs_2019",
Stdout: &stdout,
Stderr: &stderr,
DependenciesAction: "downgrade",
}
d := NewDepAction(cfg)
suite.Require().Equal(errors.New("unknown dependency_action: downgrade"), d.Prepare())
}
func (suite *DepActionTestSuite) TestPrepareChartRequired() {
d := NewDepAction(env.Config{})
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
err := d.Prepare()
suite.EqualError(err, "chart is required")
}

View File

@@ -2,44 +2,42 @@ package run
import ( import (
"fmt" "fmt"
"github.com/pelotech/drone-helm3/internal/env"
) )
// DepUpdate is an execution step that calls `helm dependency update` when executed. // DepUpdate is an execution step that calls `helm dependency update` when executed.
type DepUpdate struct { type DepUpdate struct {
*config Chart string
chart string
cmd cmd cmd cmd
} }
// NewDepUpdate creates a DepUpdate using fields from the given Config. No validation is performed at this time.
func NewDepUpdate(cfg env.Config) *DepUpdate {
return &DepUpdate{
config: newConfig(cfg),
chart: cfg.Chart,
}
}
// Execute executes the `helm upgrade` command. // Execute executes the `helm upgrade` command.
func (d *DepUpdate) Execute() error { func (d *DepUpdate) Execute(_ Config) error {
return d.cmd.Run() return d.cmd.Run()
} }
// Prepare gets the DepUpdate ready to execute. // Prepare gets the DepUpdate ready to execute.
func (d *DepUpdate) Prepare() error { func (d *DepUpdate) Prepare(cfg Config) error {
if d.chart == "" { if d.Chart == "" {
return fmt.Errorf("chart is required") return fmt.Errorf("chart is required")
} }
args := d.globalFlags() args := make([]string, 0)
args = append(args, "dependency", "update", d.chart)
if cfg.Namespace != "" {
args = append(args, "--namespace", cfg.Namespace)
}
if cfg.Debug {
args = append(args, "--debug")
}
args = append(args, "dependency", "update", d.Chart)
d.cmd = command(helmBin, args...) d.cmd = command(helmBin, args...)
d.cmd.Stdout(d.stdout) d.cmd.Stdout(cfg.Stdout)
d.cmd.Stderr(d.stderr) d.cmd.Stderr(cfg.Stderr)
if d.debug { if cfg.Debug {
fmt.Fprintf(d.stderr, "Generated command: '%s'\n", d.cmd.String()) fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", d.cmd.String())
} }
return nil return nil

View File

@@ -1,8 +1,8 @@
package run package run
import ( import (
"fmt"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"strings" "strings"
"testing" "testing"
@@ -31,21 +31,12 @@ func TestDepUpdateTestSuite(t *testing.T) {
suite.Run(t, new(DepUpdateTestSuite)) suite.Run(t, new(DepUpdateTestSuite))
} }
func (suite *DepUpdateTestSuite) TestNewDepUpdate() {
cfg := env.Config{
Chart: "scatterplot",
}
d := NewDepUpdate(cfg)
suite.Equal("scatterplot", d.chart)
}
func (suite *DepUpdateTestSuite) TestPrepareAndExecute() { func (suite *DepUpdateTestSuite) TestPrepareAndExecute() {
defer suite.ctrl.Finish() defer suite.ctrl.Finish()
stdout := strings.Builder{} stdout := strings.Builder{}
stderr := strings.Builder{} stderr := strings.Builder{}
cfg := env.Config{ cfg := Config{
Chart: "your_top_songs_2019",
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
} }
@@ -64,18 +55,74 @@ func (suite *DepUpdateTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
d := NewDepUpdate(cfg) d := DepUpdate{
Chart: "your_top_songs_2019",
}
suite.Require().NoError(d.Prepare()) suite.Require().NoError(d.Prepare(cfg))
suite.NoError(d.Execute()) suite.NoError(d.Execute(cfg))
}
func (suite *DepUpdateTestSuite) TestPrepareNamespaceFlag() {
defer suite.ctrl.Finish()
cfg := Config{
Namespace: "spotify",
}
command = func(path string, args ...string) cmd {
suite.Equal([]string{"--namespace", "spotify", "dependency", "update", "your_top_songs_2019"}, args)
return suite.mockCmd
}
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
d := DepUpdate{
Chart: "your_top_songs_2019",
}
suite.Require().NoError(d.Prepare(cfg))
}
func (suite *DepUpdateTestSuite) TestPrepareDebugFlag() {
defer suite.ctrl.Finish()
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := Config{
Debug: true,
Stdout: &stdout,
Stderr: &stderr,
}
command = func(path string, args ...string) cmd {
suite.mockCmd.EXPECT().
String().
Return(fmt.Sprintf("%s %s", path, strings.Join(args, " ")))
return suite.mockCmd
}
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
d := DepUpdate{
Chart: "your_top_songs_2019",
}
suite.Require().NoError(d.Prepare(cfg))
want := fmt.Sprintf("Generated command: '%s --debug dependency update your_top_songs_2019'\n", helmBin)
suite.Equal(want, stderr.String())
suite.Equal("", stdout.String())
} }
func (suite *DepUpdateTestSuite) TestPrepareChartRequired() { func (suite *DepUpdateTestSuite) TestPrepareChartRequired() {
d := NewDepUpdate(env.Config{}) d := DepUpdate{}
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()
err := d.Prepare() err := d.Prepare(Config{})
suite.EqualError(err, "chart is required") suite.EqualError(err, "chart is required")
} }

View File

@@ -2,47 +2,39 @@ package run
import ( import (
"fmt" "fmt"
"github.com/pelotech/drone-helm3/internal/env"
) )
// Help is a step in a helm Plan that calls `helm help`. // Help is a step in a helm Plan that calls `helm help`.
type Help struct { type Help struct {
*config HelmCommand string
helmCommand string
cmd cmd cmd cmd
} }
// NewHelp creates a Help using fields from the given Config. No validation is performed at this time.
func NewHelp(cfg env.Config) *Help {
return &Help{
config: newConfig(cfg),
helmCommand: cfg.Command,
}
}
// Execute executes the `helm help` command. // Execute executes the `helm help` command.
func (h *Help) Execute() error { func (h *Help) Execute(cfg Config) error {
if err := h.cmd.Run(); err != nil { if err := h.cmd.Run(); err != nil {
return fmt.Errorf("while running '%s': %w", h.cmd.String(), err) return fmt.Errorf("while running '%s': %w", h.cmd.String(), err)
} }
if h.helmCommand == "help" { if h.HelmCommand == "help" {
return nil return nil
} }
return fmt.Errorf("unknown command '%s'", h.helmCommand) return fmt.Errorf("unknown command '%s'", h.HelmCommand)
} }
// Prepare gets the Help ready to execute. // Prepare gets the Help ready to execute.
func (h *Help) Prepare() error { func (h *Help) Prepare(cfg Config) error {
args := h.globalFlags() args := []string{"help"}
args = append(args, "help") if cfg.Debug {
args = append([]string{"--debug"}, args...)
}
h.cmd = command(helmBin, args...) h.cmd = command(helmBin, args...)
h.cmd.Stdout(h.stdout) h.cmd.Stdout(cfg.Stdout)
h.cmd.Stderr(h.stderr) h.cmd.Stderr(cfg.Stderr)
if h.debug { if cfg.Debug {
fmt.Fprintf(h.stderr, "Generated command: '%s'\n", h.cmd.String()) fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", h.cmd.String())
} }
return nil return nil

View File

@@ -1,8 +1,8 @@
package run package run
import ( import (
"fmt"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"strings" "strings"
@@ -17,15 +17,6 @@ func TestHelpTestSuite(t *testing.T) {
suite.Run(t, new(HelpTestSuite)) suite.Run(t, new(HelpTestSuite))
} }
func (suite *HelpTestSuite) TestNewHelp() {
cfg := env.Config{
Command: "everybody dance NOW!!",
}
help := NewHelp(cfg)
suite.Require().NotNil(help)
suite.Equal("everybody dance NOW!!", help.helmCommand)
}
func (suite *HelpTestSuite) TestPrepare() { func (suite *HelpTestSuite) TestPrepare() {
ctrl := gomock.NewController(suite.T()) ctrl := gomock.NewController(suite.T())
defer ctrl.Finish() defer ctrl.Finish()
@@ -48,13 +39,13 @@ func (suite *HelpTestSuite) TestPrepare() {
mCmd.EXPECT(). mCmd.EXPECT().
Stderr(&stderr) Stderr(&stderr)
cfg := env.Config{ cfg := Config{
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
} }
h := NewHelp(cfg) h := Help{}
err := h.Prepare() err := h.Prepare(cfg)
suite.NoError(err) suite.NoError(err)
} }
@@ -62,15 +53,41 @@ func (suite *HelpTestSuite) TestExecute() {
ctrl := gomock.NewController(suite.T()) ctrl := gomock.NewController(suite.T())
defer ctrl.Finish() defer ctrl.Finish()
mCmd := NewMockcmd(ctrl) mCmd := NewMockcmd(ctrl)
originalCommand := command
command = func(_ string, _ ...string) cmd {
return mCmd
}
defer func() { command = originalCommand }()
mCmd.EXPECT(). mCmd.EXPECT().
Run(). Run().
Times(2) Times(2)
help := NewHelp(env.Config{Command: "help"}) cfg := Config{}
help.cmd = mCmd help := Help{
suite.NoError(help.Execute()) HelmCommand: "help",
cmd: mCmd,
}
suite.NoError(help.Execute(cfg))
help.helmCommand = "get down on friday" help.HelmCommand = "get down on friday"
suite.EqualError(help.Execute(), "unknown command 'get down on friday'") suite.EqualError(help.Execute(cfg), "unknown command 'get down on friday'")
}
func (suite *HelpTestSuite) TestPrepareDebugFlag() {
help := Help{}
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := Config{
Debug: true,
Stdout: &stdout,
Stderr: &stderr,
}
help.Prepare(cfg)
want := fmt.Sprintf("Generated command: '%s --debug help'\n", helmBin)
suite.Equal(want, stderr.String())
suite.Equal("", stdout.String())
} }

View File

@@ -6,18 +6,21 @@ import (
"io" "io"
"os" "os"
"text/template" "text/template"
"github.com/pelotech/drone-helm3/internal/env"
) )
// InitKube is a step in a helm Plan that initializes the kubernetes config file. // InitKube is a step in a helm Plan that initializes the kubernetes config file.
type InitKube struct { type InitKube struct {
*config SkipTLSVerify bool
templateFilename string Certificate string
configFilename string APIServer string
template *template.Template ServiceAccount string
configFile io.WriteCloser Token string
values kubeValues TemplateFile string
ConfigFile string
template *template.Template
configFile io.WriteCloser
values kubeValues
} }
type kubeValues struct { type kubeValues struct {
@@ -29,66 +32,58 @@ type kubeValues struct {
Token string Token string
} }
// NewInitKube creates a InitKube using the given Config and filepaths. No validation is performed at this time.
func NewInitKube(cfg env.Config, templateFile, configFile string) *InitKube {
return &InitKube{
config: newConfig(cfg),
values: kubeValues{
SkipTLSVerify: cfg.SkipTLSVerify,
Certificate: cfg.Certificate,
APIServer: cfg.APIServer,
Namespace: cfg.Namespace,
ServiceAccount: cfg.ServiceAccount,
Token: cfg.KubeToken,
},
templateFilename: templateFile,
configFilename: configFile,
}
}
// 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() error { func (i *InitKube) Execute(cfg Config) error {
if i.debug { if cfg.Debug {
fmt.Fprintf(i.stderr, "writing kubeconfig file to %s\n", i.configFilename) 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)
} }
// Prepare ensures all required configuration is present and that the config file is writable. // Prepare ensures all required configuration is present and that the config file is writable.
func (i *InitKube) Prepare() error { func (i *InitKube) Prepare(cfg Config) error {
var err error var err error
if i.values.APIServer == "" { if i.APIServer == "" {
return errors.New("an API Server is needed to deploy") return errors.New("an API Server is needed to deploy")
} }
if i.values.Token == "" { if i.Token == "" {
return errors.New("token is needed to deploy") return errors.New("token is needed to deploy")
} }
if i.values.ServiceAccount == "" { if i.ServiceAccount == "" {
i.values.ServiceAccount = "helm" i.ServiceAccount = "helm"
} }
if i.debug { if cfg.Debug {
fmt.Fprintf(i.stderr, "loading kubeconfig template from %s\n", i.templateFilename) fmt.Fprintf(cfg.Stderr, "loading kubeconfig template from %s\n", i.TemplateFile)
} }
i.template, err = template.ParseFiles(i.templateFilename) i.template, err = template.ParseFiles(i.TemplateFile)
if err != nil { if err != nil {
return fmt.Errorf("could not load kubeconfig template: %w", err) return fmt.Errorf("could not load kubeconfig template: %w", err)
} }
if i.debug { i.values = kubeValues{
if _, err := os.Stat(i.configFilename); err != nil { SkipTLSVerify: i.SkipTLSVerify,
// non-nil err here isn't an actual error state; the kubeconfig just doesn't exist Certificate: i.Certificate,
fmt.Fprint(i.stderr, "creating ") APIServer: i.APIServer,
} else { ServiceAccount: i.ServiceAccount,
fmt.Fprint(i.stderr, "truncating ") Token: i.Token,
} Namespace: cfg.Namespace,
fmt.Fprintf(i.stderr, "kubeconfig file at %s\n", i.configFilename)
} }
i.configFile, err = os.OpenFile(i.configFilename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if cfg.Debug {
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", i.ConfigFile)
}
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,13 +1,10 @@
package run package run
import ( import (
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"os" "os"
"strings"
"testing" "testing"
"text/template" "text/template"
) )
@@ -20,30 +17,6 @@ func TestInitKubeTestSuite(t *testing.T) {
suite.Run(t, new(InitKubeTestSuite)) suite.Run(t, new(InitKubeTestSuite))
} }
func (suite *InitKubeTestSuite) TestNewInitKube() {
cfg := env.Config{
SkipTLSVerify: true,
Certificate: "cHJvY2xhaW1zIHdvbmRlcmZ1bCBmcmllbmRzaGlw",
APIServer: "98.765.43.21",
ServiceAccount: "greathelm",
KubeToken: "b2YgbXkgYWZmZWN0aW9u",
Stderr: &strings.Builder{},
Debug: true,
}
init := NewInitKube(cfg, "conf.tpl", "conf.yml")
suite.Equal(kubeValues{
SkipTLSVerify: true,
Certificate: "cHJvY2xhaW1zIHdvbmRlcmZ1bCBmcmllbmRzaGlw",
APIServer: "98.765.43.21",
ServiceAccount: "greathelm",
Token: "b2YgbXkgYWZmZWN0aW9u",
}, init.values)
suite.Equal("conf.tpl", init.templateFilename)
suite.Equal("conf.yml", init.configFilename)
suite.NotNil(init.config)
}
func (suite *InitKubeTestSuite) TestPrepareExecute() { func (suite *InitKubeTestSuite) TestPrepareExecute() {
templateFile, err := tempfile("kubeconfig********.yml.tpl", ` templateFile, err := tempfile("kubeconfig********.yml.tpl", `
certificate: {{ .Certificate }} certificate: {{ .Certificate }}
@@ -56,20 +29,23 @@ namespace: {{ .Namespace }}
defer os.Remove(configFile.Name()) defer os.Remove(configFile.Name())
suite.Require().Nil(err) suite.Require().Nil(err)
cfg := env.Config{ init := InitKube{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
KubeToken: "Aspire virtual currency", Token: "Aspire virtual currency",
Namespace: "Cisco", TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
} }
init := NewInitKube(cfg, templateFile.Name(), configFile.Name()) cfg := Config{
err = init.Prepare() Namespace: "Cisco",
}
err = init.Prepare(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
suite.IsType(&template.Template{}, init.template) suite.IsType(&template.Template{}, init.template)
suite.NotNil(init.configFile) suite.NotNil(init.configFile)
err = init.Execute() err = init.Execute(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
conf, err := ioutil.ReadFile(configFile.Name()) conf, err := ioutil.ReadFile(configFile.Name())
@@ -87,16 +63,19 @@ func (suite *InitKubeTestSuite) TestExecuteGeneratesConfig() {
defer os.Remove(configFile.Name()) defer os.Remove(configFile.Name())
suite.Require().NoError(err) suite.Require().NoError(err)
cfg := env.Config{ cfg := Config{
Namespace: "marshmallow",
}
init := InitKube{
ConfigFile: configFile.Name(),
TemplateFile: "../../assets/kubeconfig.tpl", // the actual kubeconfig template
APIServer: "https://kube.cluster/peanut", APIServer: "https://kube.cluster/peanut",
ServiceAccount: "chef", ServiceAccount: "chef",
KubeToken: "eWVhaCB3ZSB0b2tpbic=", Token: "eWVhaCB3ZSB0b2tpbic=",
Certificate: "d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE", Certificate: "d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE",
Namespace: "marshmallow",
} }
init := NewInitKube(cfg, "../../assets/kubeconfig.tpl", configFile.Name()) // the actual kubeconfig template suite.Require().NoError(init.Prepare(cfg))
suite.Require().NoError(init.Prepare()) suite.Require().NoError(init.Execute(cfg))
suite.Require().NoError(init.Execute())
contents, err := ioutil.ReadFile(configFile.Name()) contents, err := ioutil.ReadFile(configFile.Name())
suite.Require().NoError(err) suite.Require().NoError(err)
@@ -119,11 +98,11 @@ func (suite *InitKubeTestSuite) TestExecuteGeneratesConfig() {
suite.NoError(yaml.UnmarshalStrict(contents, &conf)) suite.NoError(yaml.UnmarshalStrict(contents, &conf))
// test the other branch of the certificate/SkipTLSVerify conditional // test the other branch of the certificate/SkipTLSVerify conditional
init.values.SkipTLSVerify = true init.SkipTLSVerify = true
init.values.Certificate = "" init.Certificate = ""
suite.Require().NoError(init.Prepare()) suite.Require().NoError(init.Prepare(cfg))
suite.Require().NoError(init.Execute()) suite.Require().NoError(init.Execute(cfg))
contents, err = ioutil.ReadFile(configFile.Name()) contents, err = ioutil.ReadFile(configFile.Name())
suite.Require().NoError(err) suite.Require().NoError(err)
suite.Contains(string(contents), "insecure-skip-tls-verify: true") suite.Contains(string(contents), "insecure-skip-tls-verify: true")
@@ -137,25 +116,25 @@ func (suite *InitKubeTestSuite) TestPrepareParseError() {
defer os.Remove(templateFile.Name()) defer os.Remove(templateFile.Name())
suite.Require().Nil(err) suite.Require().Nil(err)
cfg := env.Config{ init := InitKube{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
KubeToken: "Aspire virtual currency", Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
} }
init := NewInitKube(cfg, templateFile.Name(), "") err = init.Prepare(Config{})
err = init.Prepare()
suite.Error(err) suite.Error(err)
suite.Regexp("could not load kubeconfig .* function .* not defined", err) suite.Regexp("could not load kubeconfig .* function .* not defined", err)
} }
func (suite *InitKubeTestSuite) TestPrepareNonexistentTemplateFile() { func (suite *InitKubeTestSuite) TestPrepareNonexistentTemplateFile() {
cfg := env.Config{ init := InitKube{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
KubeToken: "Aspire virtual currency", Token: "Aspire virtual currency",
TemplateFile: "/usr/foreign/exclude/kubeprofig.tpl",
} }
init := NewInitKube(cfg, "/usr/foreign/exclude/kubeprofig.tpl", "") err := init.Prepare(Config{})
err := init.Prepare()
suite.Error(err) suite.Error(err)
suite.Regexp("could not load kubeconfig .* no such file or directory", err) suite.Regexp("could not load kubeconfig .* no such file or directory", err)
} }
@@ -164,14 +143,16 @@ func (suite *InitKubeTestSuite) TestPrepareCannotOpenDestinationFile() {
templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity") templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity")
defer os.Remove(templateFile.Name()) defer os.Remove(templateFile.Name())
suite.Require().Nil(err) suite.Require().Nil(err)
cfg := env.Config{ init := InitKube{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
KubeToken: "Aspire virtual currency", Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: "/usr/foreign/exclude/kubeprofig",
} }
init := NewInitKube(cfg, templateFile.Name(), "/usr/foreign/exclude/kubeprofig")
err = init.Prepare() cfg := Config{}
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)
} }
@@ -186,21 +167,24 @@ func (suite *InitKubeTestSuite) TestPrepareRequiredConfig() {
suite.Require().Nil(err) suite.Require().Nil(err)
// initial config with all required fields present // initial config with all required fields present
cfg := env.Config{ init := InitKube{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
KubeToken: "Aspire virtual currency", Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
} }
init := NewInitKube(cfg, templateFile.Name(), configFile.Name()) cfg := Config{}
suite.NoError(init.Prepare()) // consistency check; we should be starting in a happy state
init.values.APIServer = "" suite.NoError(init.Prepare(cfg)) // consistency check; we should be starting in a happy state
suite.Error(init.Prepare(), "APIServer should be required.")
init.values.APIServer = "Sysadmin" init.APIServer = ""
init.values.Token = "" suite.Error(init.Prepare(cfg), "APIServer should be required.")
suite.Error(init.Prepare(), "Token should be required.")
init.APIServer = "Sysadmin"
init.Token = ""
suite.Error(init.Prepare(cfg), "Token should be required.")
} }
func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() { func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
@@ -212,43 +196,18 @@ func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
defer os.Remove(configFile.Name()) defer os.Remove(configFile.Name())
suite.Require().Nil(err) suite.Require().Nil(err)
cfg := env.Config{ init := InitKube{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
KubeToken: "Aspire virtual currency", Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
} }
init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
init.Prepare() cfg := Config{}
suite.Equal("helm", init.values.ServiceAccount)
}
func (suite *InitKubeTestSuite) TestDebugOutput() { init.Prepare(cfg)
templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity") suite.Equal("helm", init.ServiceAccount)
defer os.Remove(templateFile.Name())
suite.Require().Nil(err)
configFile, err := tempfile("kubeconfig********.yml", "")
defer os.Remove(configFile.Name())
suite.Require().Nil(err)
stdout := &strings.Builder{}
stderr := &strings.Builder{}
cfg := env.Config{
APIServer: "http://my.kube.server/",
KubeToken: "QSBzaW5nbGUgcm9zZQ==",
Debug: true,
Stdout: stdout,
Stderr: stderr,
}
init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
suite.NoError(init.Prepare())
suite.Contains(stderr.String(), fmt.Sprintf("loading kubeconfig template from %s\n", templateFile.Name()))
suite.Contains(stderr.String(), fmt.Sprintf("truncating kubeconfig file at %s\n", configFile.Name()))
suite.NoError(init.Execute())
suite.Contains(stderr.String(), fmt.Sprintf("writing kubeconfig file to %s\n", configFile.Name()))
} }
func tempfile(name, contents string) (*os.File, error) { func tempfile(name, contents string) (*os.File, error) {

View File

@@ -2,67 +2,61 @@ package run
import ( import (
"fmt" "fmt"
"github.com/pelotech/drone-helm3/internal/env"
) )
// Lint is an execution step that calls `helm lint` when executed. // Lint is an execution step that calls `helm lint` when executed.
type Lint struct { type Lint struct {
*config Chart string
chart string Values string
values string StringValues string
stringValues string ValuesFiles []string
valuesFiles []string Strict bool
strict bool
cmd cmd cmd cmd
} }
// NewLint creates a Lint using fields from the given Config. No validation is performed at this time.
func NewLint(cfg env.Config) *Lint {
return &Lint{
config: newConfig(cfg),
chart: cfg.Chart,
values: cfg.Values,
stringValues: cfg.StringValues,
valuesFiles: cfg.ValuesFiles,
strict: cfg.LintStrictly,
}
}
// Execute executes the `helm lint` command. // Execute executes the `helm lint` command.
func (l *Lint) Execute() error { func (l *Lint) Execute(_ Config) error {
return l.cmd.Run() return l.cmd.Run()
} }
// Prepare gets the Lint ready to execute. // Prepare gets the Lint ready to execute.
func (l *Lint) Prepare() 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")
} }
args := l.globalFlags() args := make([]string, 0)
if cfg.Namespace != "" {
args = append(args, "--namespace", cfg.Namespace)
}
if cfg.Debug {
args = append(args, "--debug")
}
args = append(args, "lint") args = append(args, "lint")
if l.values != "" { if l.Values != "" {
args = append(args, "--set", l.values) args = append(args, "--set", l.Values)
} }
if l.stringValues != "" { if l.StringValues != "" {
args = append(args, "--set-string", l.stringValues) args = append(args, "--set-string", l.StringValues)
} }
for _, vFile := range l.valuesFiles { for _, vFile := range l.ValuesFiles {
args = append(args, "--values", vFile) args = append(args, "--values", vFile)
} }
if l.strict { if l.Strict {
args = append(args, "--strict") args = append(args, "--strict")
} }
args = append(args, l.chart) args = append(args, l.Chart)
l.cmd = command(helmBin, args...) l.cmd = command(helmBin, args...)
l.cmd.Stdout(l.stdout) l.cmd.Stdout(cfg.Stdout)
l.cmd.Stderr(l.stderr) l.cmd.Stderr(cfg.Stderr)
if l.debug { if cfg.Debug {
fmt.Fprintf(l.stderr, "Generated command: '%s'\n", l.cmd.String()) fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", l.cmd.String())
} }
return nil return nil

View File

@@ -1,8 +1,8 @@
package run package run
import ( import (
"fmt"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"strings" "strings"
"testing" "testing"
@@ -31,36 +31,19 @@ func TestLintTestSuite(t *testing.T) {
suite.Run(t, new(LintTestSuite)) suite.Run(t, new(LintTestSuite))
} }
func (suite *LintTestSuite) TestNewLint() {
cfg := env.Config{
Chart: "./flow",
Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
LintStrictly: true,
}
lint := NewLint(cfg)
suite.Require().NotNil(lint)
suite.Equal("./flow", lint.chart)
suite.Equal("steadfastness,forthrightness", lint.values)
suite.Equal("tensile_strength,flexibility", lint.stringValues)
suite.Equal([]string{"/root/price_inventory.yml"}, lint.valuesFiles)
suite.Equal(true, lint.strict)
suite.NotNil(lint.config)
}
func (suite *LintTestSuite) TestPrepareAndExecute() { func (suite *LintTestSuite) TestPrepareAndExecute() {
defer suite.ctrl.Finish() defer suite.ctrl.Finish()
stdout := strings.Builder{} stdout := strings.Builder{}
stderr := strings.Builder{} stderr := strings.Builder{}
cfg := env.Config{ l := Lint{
Chart: "./epic/mychart", Chart: "./epic/mychart",
}
cfg := Config{
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
} }
l := NewLint(cfg)
command = func(path string, args ...string) cmd { command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path) suite.Equal(helmBin, path)
@@ -69,7 +52,6 @@ func (suite *LintTestSuite) TestPrepareAndExecute() {
return suite.mockCmd return suite.mockCmd
} }
suite.mockCmd.EXPECT().String().AnyTimes()
suite.mockCmd.EXPECT(). suite.mockCmd.EXPECT().
Stdout(&stdout) Stdout(&stdout)
suite.mockCmd.EXPECT(). suite.mockCmd.EXPECT().
@@ -78,9 +60,9 @@ func (suite *LintTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
err := l.Prepare() err := l.Prepare(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
l.Execute() l.Execute(cfg)
} }
func (suite *LintTestSuite) TestPrepareRequiresChart() { func (suite *LintTestSuite) TestPrepareRequiresChart() {
@@ -88,22 +70,25 @@ func (suite *LintTestSuite) TestPrepareRequiresChart() {
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()
l := NewLint(env.Config{}) cfg := Config{}
err := l.Prepare() l := Lint{}
err := l.Prepare(cfg)
suite.EqualError(err, "chart is required", "Chart should be mandatory") suite.EqualError(err, "chart is required", "Chart should be mandatory")
} }
func (suite *LintTestSuite) TestPrepareWithLintFlags() { func (suite *LintTestSuite) TestPrepareWithLintFlags() {
defer suite.ctrl.Finish() defer suite.ctrl.Finish()
cfg := env.Config{ cfg := Config{}
l := Lint{
Chart: "./uk/top_40", Chart: "./uk/top_40",
Values: "width=5", Values: "width=5",
StringValues: "version=2.0", StringValues: "version=2.0",
ValuesFiles: []string{"/usr/local/underrides", "/usr/local/overrides"}, ValuesFiles: []string{"/usr/local/underrides", "/usr/local/overrides"},
LintStrictly: true, Strict: true,
} }
l := NewLint(cfg)
command = func(path string, args ...string) cmd { command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path) suite.Equal(helmBin, path)
@@ -120,8 +105,66 @@ func (suite *LintTestSuite) TestPrepareWithLintFlags() {
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.mockCmd.EXPECT().String().AnyTimes()
err := l.Prepare() err := l.Prepare(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
} }
func (suite *LintTestSuite) TestPrepareWithDebugFlag() {
defer suite.ctrl.Finish()
stderr := strings.Builder{}
cfg := Config{
Debug: true,
Stderr: &stderr,
}
l := Lint{
Chart: "./scotland/top_40",
}
command = func(path string, args ...string) cmd {
suite.mockCmd.EXPECT().
String().
Return(fmt.Sprintf("%s %s", path, strings.Join(args, " ")))
return suite.mockCmd
}
suite.mockCmd.EXPECT().Stdout(gomock.Any())
suite.mockCmd.EXPECT().Stderr(&stderr)
err := l.Prepare(cfg)
suite.Require().Nil(err)
want := fmt.Sprintf("Generated command: '%s --debug lint ./scotland/top_40'\n", helmBin)
suite.Equal(want, stderr.String())
}
func (suite *LintTestSuite) TestPrepareWithNamespaceFlag() {
defer suite.ctrl.Finish()
cfg := Config{
Namespace: "table-service",
}
l := Lint{
Chart: "./wales/top_40",
}
actual := []string{}
command = func(path string, args ...string) cmd {
actual = args
return suite.mockCmd
}
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
err := l.Prepare(cfg)
suite.Require().Nil(err)
expected := []string{"--namespace", "table-service", "lint", "./wales/top_40"}
suite.Equal(expected, actual)
}

View File

@@ -1,77 +0,0 @@
package run
import (
"encoding/base64"
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
"io/ioutil"
)
type repoCerts struct {
*config
cert string
certFilename string
caCert string
caCertFilename string
}
func newRepoCerts(cfg env.Config) *repoCerts {
return &repoCerts{
config: newConfig(cfg),
cert: cfg.RepoCertificate,
caCert: cfg.RepoCACertificate,
}
}
func (rc *repoCerts) write() error {
if rc.cert != "" {
file, err := ioutil.TempFile("", "repo********.cert")
defer file.Close()
if err != nil {
return fmt.Errorf("failed to create certificate file: %w", err)
}
rc.certFilename = file.Name()
rawCert, err := base64.StdEncoding.DecodeString(rc.cert)
if err != nil {
return fmt.Errorf("failed to base64-decode certificate string: %w", err)
}
if rc.debug {
fmt.Fprintf(rc.stderr, "writing repo certificate to %s\n", rc.certFilename)
}
if _, err := file.Write(rawCert); err != nil {
return fmt.Errorf("failed to write certificate file: %w", err)
}
}
if rc.caCert != "" {
file, err := ioutil.TempFile("", "repo********.ca.cert")
defer file.Close()
if err != nil {
return fmt.Errorf("failed to create CA certificate file: %w", err)
}
rc.caCertFilename = file.Name()
rawCert, err := base64.StdEncoding.DecodeString(rc.caCert)
if err != nil {
return fmt.Errorf("failed to base64-decode CA certificate string: %w", err)
}
if rc.debug {
fmt.Fprintf(rc.stderr, "writing repo ca certificate to %s\n", rc.caCertFilename)
}
if _, err := file.Write(rawCert); err != nil {
return fmt.Errorf("failed to write CA certificate file: %w", err)
}
}
return nil
}
func (rc *repoCerts) flags() []string {
flags := make([]string, 0)
if rc.certFilename != "" {
flags = append(flags, "--cert-file", rc.certFilename)
}
if rc.caCertFilename != "" {
flags = append(flags, "--ca-file", rc.caCertFilename)
}
return flags
}

View File

@@ -1,80 +0,0 @@
package run
import (
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite"
"io/ioutil"
"os"
"strings"
"testing"
)
type RepoCertsTestSuite struct {
suite.Suite
}
func TestRepoCertsTestSuite(t *testing.T) {
suite.Run(t, new(RepoCertsTestSuite))
}
func (suite *RepoCertsTestSuite) TestNewRepoCerts() {
cfg := env.Config{
RepoCertificate: "bGljZW5zZWQgYnkgdGhlIFN0YXRlIG9mIE9yZWdvbiB0byBwZXJmb3JtIHJlcG9zc2Vzc2lvbnM=",
RepoCACertificate: "T3JlZ29uIFN0YXRlIExpY2Vuc3VyZSBib2FyZA==",
}
rc := newRepoCerts(cfg)
suite.Require().NotNil(rc)
suite.Equal("bGljZW5zZWQgYnkgdGhlIFN0YXRlIG9mIE9yZWdvbiB0byBwZXJmb3JtIHJlcG9zc2Vzc2lvbnM=", rc.cert)
suite.Equal("T3JlZ29uIFN0YXRlIExpY2Vuc3VyZSBib2FyZA==", rc.caCert)
}
func (suite *RepoCertsTestSuite) TestWrite() {
cfg := env.Config{
RepoCertificate: "bGljZW5zZWQgYnkgdGhlIFN0YXRlIG9mIE9yZWdvbiB0byBwZXJmb3JtIHJlcG9zc2Vzc2lvbnM=",
RepoCACertificate: "T3JlZ29uIFN0YXRlIExpY2Vuc3VyZSBib2FyZA==",
}
rc := newRepoCerts(cfg)
suite.Require().NotNil(rc)
suite.NoError(rc.write())
defer os.Remove(rc.certFilename)
defer os.Remove(rc.caCertFilename)
suite.NotEqual("", rc.certFilename)
suite.NotEqual("", rc.caCertFilename)
cert, err := ioutil.ReadFile(rc.certFilename)
suite.Require().NoError(err)
caCert, err := ioutil.ReadFile(rc.caCertFilename)
suite.Require().NoError(err)
suite.Equal("licensed by the State of Oregon to perform repossessions", string(cert))
suite.Equal("Oregon State Licensure board", string(caCert))
}
func (suite *RepoCertsTestSuite) TestFlags() {
rc := newRepoCerts(env.Config{})
suite.Equal([]string{}, rc.flags())
rc.certFilename = "hurgityburgity"
suite.Equal([]string{"--cert-file", "hurgityburgity"}, rc.flags())
rc.caCertFilename = "honglydongly"
suite.Equal([]string{"--cert-file", "hurgityburgity", "--ca-file", "honglydongly"}, rc.flags())
}
func (suite *RepoCertsTestSuite) TestDebug() {
stderr := strings.Builder{}
cfg := env.Config{
RepoCertificate: "bGljZW5zZWQgYnkgdGhlIFN0YXRlIG9mIE9yZWdvbiB0byBwZXJmb3JtIHJlcG9zc2Vzc2lvbnM=",
RepoCACertificate: "T3JlZ29uIFN0YXRlIExpY2Vuc3VyZSBib2FyZA==",
Stderr: &stderr,
Debug: true,
}
rc := newRepoCerts(cfg)
suite.Require().NotNil(rc)
suite.NoError(rc.write())
defer os.Remove(rc.certFilename)
defer os.Remove(rc.caCertFilename)
suite.Contains(stderr.String(), fmt.Sprintf("writing repo certificate to %s", rc.certFilename))
suite.Contains(stderr.String(), fmt.Sprintf("writing repo ca certificate to %s", rc.caCertFilename))
}

View File

@@ -2,57 +2,53 @@ package run
import ( import (
"fmt" "fmt"
"github.com/pelotech/drone-helm3/internal/env"
) )
// Uninstall is an execution step that calls `helm uninstall` when executed. // Uninstall is an execution step that calls `helm uninstall` when executed.
type Uninstall struct { type Uninstall struct {
*config Release string
release string DryRun bool
dryRun bool KeepHistory bool
keepHistory bool
cmd cmd cmd cmd
} }
// NewUninstall creates an Uninstall using fields from the given Config. No validation is performed at this time.
func NewUninstall(cfg env.Config) *Uninstall {
return &Uninstall{
config: newConfig(cfg),
release: cfg.Release,
dryRun: cfg.DryRun,
keepHistory: cfg.KeepHistory,
}
}
// Execute executes the `helm uninstall` command. // Execute executes the `helm uninstall` command.
func (u *Uninstall) Execute() error { func (u *Uninstall) Execute(_ Config) error {
return u.cmd.Run() return u.cmd.Run()
} }
// Prepare gets the Uninstall ready to execute. // Prepare gets the Uninstall ready to execute.
func (u *Uninstall) Prepare() error { func (u *Uninstall) Prepare(cfg Config) error {
if u.release == "" { if u.Release == "" {
return fmt.Errorf("release is required") return fmt.Errorf("release is required")
} }
args := u.globalFlags() args := make([]string, 0)
if cfg.Namespace != "" {
args = append(args, "--namespace", cfg.Namespace)
}
if cfg.Debug {
args = append(args, "--debug")
}
args = append(args, "uninstall") args = append(args, "uninstall")
if u.dryRun { if u.DryRun {
args = append(args, "--dry-run") args = append(args, "--dry-run")
} }
if u.keepHistory { if u.KeepHistory {
args = append(args, "--keep-history") args = append(args, "--keep-history")
} }
args = append(args, u.release) args = append(args, u.Release)
u.cmd = command(helmBin, args...) u.cmd = command(helmBin, args...)
u.cmd.Stdout(u.stdout) u.cmd.Stdout(cfg.Stdout)
u.cmd.Stderr(u.stderr) u.cmd.Stderr(cfg.Stderr)
if u.debug { if cfg.Debug {
fmt.Fprintf(u.stderr, "Generated command: '%s'\n", u.cmd.String()) fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", u.cmd.String())
} }
return nil return nil

View File

@@ -1,9 +1,10 @@
package run package run
import ( import (
"fmt"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"strings"
"testing" "testing"
) )
@@ -34,26 +35,12 @@ func TestUninstallTestSuite(t *testing.T) {
suite.Run(t, new(UninstallTestSuite)) suite.Run(t, new(UninstallTestSuite))
} }
func (suite *UninstallTestSuite) TestNewUninstall() {
cfg := env.Config{
DryRun: true,
Release: "jetta_id_love_to_change_the_world",
KeepHistory: true,
}
u := NewUninstall(cfg)
suite.Equal("jetta_id_love_to_change_the_world", u.release)
suite.Equal(true, u.dryRun)
suite.Equal(true, u.keepHistory)
suite.NotNil(u.config)
}
func (suite *UninstallTestSuite) TestPrepareAndExecute() { func (suite *UninstallTestSuite) TestPrepareAndExecute() {
defer suite.ctrl.Finish() defer suite.ctrl.Finish()
cfg := env.Config{ u := Uninstall{
Release: "zayde_wølf_king", Release: "zayde_wølf_king",
} }
u := NewUninstall(cfg)
actual := []string{} actual := []string{}
command = func(path string, args ...string) cmd { command = func(path string, args ...string) cmd {
@@ -71,49 +58,92 @@ func (suite *UninstallTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
suite.NoError(u.Prepare()) cfg := Config{}
suite.NoError(u.Prepare(cfg))
expected := []string{"uninstall", "zayde_wølf_king"} expected := []string{"uninstall", "zayde_wølf_king"}
suite.Equal(expected, actual) suite.Equal(expected, actual)
u.Execute() u.Execute(cfg)
} }
func (suite *UninstallTestSuite) TestPrepareDryRunFlag() { func (suite *UninstallTestSuite) TestPrepareDryRunFlag() {
cfg := env.Config{ u := Uninstall{
Release: "firefox_ak_wildfire", Release: "firefox_ak_wildfire",
DryRun: true, DryRun: true,
} }
u := NewUninstall(cfg) cfg := 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()) suite.NoError(u.Prepare(cfg))
expected := []string{"uninstall", "--dry-run", "firefox_ak_wildfire"} expected := []string{"uninstall", "--dry-run", "firefox_ak_wildfire"}
suite.Equal(expected, suite.actualArgs) suite.Equal(expected, suite.actualArgs)
} }
func (suite *UninstallTestSuite) TestPrepareKeepHistoryFlag() { func (suite *UninstallTestSuite) TestPrepareKeepHistoryFlag() {
cfg := env.Config{ u := Uninstall{
Release: "perturbator_sentient", Release: "perturbator_sentient",
KeepHistory: true, KeepHistory: true,
} }
u := NewUninstall(cfg) cfg := 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()) suite.NoError(u.Prepare(cfg))
expected := []string{"uninstall", "--keep-history", "perturbator_sentient"} expected := []string{"uninstall", "--keep-history", "perturbator_sentient"}
suite.Equal(expected, suite.actualArgs) suite.Equal(expected, suite.actualArgs)
} }
func (suite *UninstallTestSuite) TestPrepareNamespaceFlag() {
u := Uninstall{
Release: "carly_simon_run_away_with_me",
}
cfg := Config{
Namespace: "emotion",
}
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
suite.NoError(u.Prepare(cfg))
expected := []string{"--namespace", "emotion", "uninstall", "carly_simon_run_away_with_me"}
suite.Equal(expected, suite.actualArgs)
}
func (suite *UninstallTestSuite) TestPrepareDebugFlag() {
u := Uninstall{
Release: "just_a_band_huff_and_puff",
}
stderr := strings.Builder{}
cfg := Config{
Debug: true,
Stderr: &stderr,
}
command = func(path string, args ...string) cmd {
suite.mockCmd.EXPECT().
String().
Return(fmt.Sprintf("%s %s", path, strings.Join(args, " ")))
return suite.mockCmd
}
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(&stderr).AnyTimes()
suite.NoError(u.Prepare(cfg))
suite.Equal(fmt.Sprintf("Generated command: '%s --debug "+
"uninstall just_a_band_huff_and_puff'\n", helmBin), stderr.String())
}
func (suite *UninstallTestSuite) TestPrepareRequiresRelease() { func (suite *UninstallTestSuite) TestPrepareRequiresRelease() {
// These aren't really expected, but allowing them gives clearer test-failure messages // These aren't really expected, but allowing them gives clearer test-failure messages
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()
u := NewUninstall(env.Config{}) u := Uninstall{}
err := u.Prepare() err := u.Prepare(Config{})
suite.EqualError(err, "release is required", "Uninstall.Release should be mandatory") suite.EqualError(err, "release is required", "Uninstall.Release should be mandatory")
} }

View File

@@ -2,127 +2,94 @@ package run
import ( import (
"fmt" "fmt"
"github.com/pelotech/drone-helm3/internal/env"
) )
// Upgrade is an execution step that calls `helm upgrade` when executed. // Upgrade is an execution step that calls `helm upgrade` when executed.
type Upgrade struct { type Upgrade struct {
*config Chart string
chart string Release string
release string
chartVersion string ChartVersion string
dryRun bool DryRun bool
wait bool Wait bool
values string Values string
stringValues string StringValues string
valuesFiles []string ValuesFiles []string
reuseValues bool ReuseValues bool
timeout string Timeout string
force bool Force bool
atomic bool Atomic bool
cleanupOnFail bool CleanupOnFail bool
historyMax int
certs *repoCerts
createNamespace bool
skipCrds bool
cmd cmd cmd cmd
} }
// NewUpgrade creates an Upgrade using fields from the given Config. No validation is performed at this time.
func NewUpgrade(cfg env.Config) *Upgrade {
return &Upgrade{
config: newConfig(cfg),
chart: cfg.Chart,
release: cfg.Release,
chartVersion: cfg.ChartVersion,
dryRun: cfg.DryRun,
wait: cfg.Wait,
values: cfg.Values,
stringValues: cfg.StringValues,
valuesFiles: cfg.ValuesFiles,
reuseValues: cfg.ReuseValues,
timeout: cfg.Timeout,
force: cfg.Force,
atomic: cfg.AtomicUpgrade,
cleanupOnFail: cfg.CleanupOnFail,
historyMax: cfg.HistoryMax,
certs: newRepoCerts(cfg),
createNamespace: cfg.CreateNamespace,
skipCrds: cfg.SkipCrds,
}
}
// Execute executes the `helm upgrade` command. // Execute executes the `helm upgrade` command.
func (u *Upgrade) Execute() error { func (u *Upgrade) Execute(_ Config) error {
return u.cmd.Run() return u.cmd.Run()
} }
// Prepare gets the Upgrade ready to execute. // Prepare gets the Upgrade ready to execute.
func (u *Upgrade) Prepare() 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")
} }
if u.release == "" { if u.Release == "" {
return fmt.Errorf("release is required") return fmt.Errorf("release is required")
} }
args := u.globalFlags() args := make([]string, 0)
if cfg.Namespace != "" {
args = append(args, "--namespace", cfg.Namespace)
}
if cfg.Debug {
args = append(args, "--debug")
}
args = append(args, "upgrade", "--install") args = append(args, "upgrade", "--install")
if u.chartVersion != "" { if u.ChartVersion != "" {
args = append(args, "--version", u.chartVersion) args = append(args, "--version", u.ChartVersion)
} }
if u.dryRun { if u.DryRun {
args = append(args, "--dry-run") args = append(args, "--dry-run")
} }
if u.wait { if u.Wait {
args = append(args, "--wait") args = append(args, "--wait")
} }
if u.reuseValues { if u.ReuseValues {
args = append(args, "--reuse-values") args = append(args, "--reuse-values")
} }
if u.timeout != "" { if u.Timeout != "" {
args = append(args, "--timeout", u.timeout) args = append(args, "--timeout", u.Timeout)
} }
if u.force { if u.Force {
args = append(args, "--force") args = append(args, "--force")
} }
if u.atomic { if u.Atomic {
args = append(args, "--atomic") args = append(args, "--atomic")
} }
if u.cleanupOnFail { if u.CleanupOnFail {
args = append(args, "--cleanup-on-fail") args = append(args, "--cleanup-on-fail")
} }
if u.values != "" { if u.Values != "" {
args = append(args, "--set", u.values) args = append(args, "--set", u.Values)
} }
if u.stringValues != "" { if u.StringValues != "" {
args = append(args, "--set-string", u.stringValues) args = append(args, "--set-string", u.StringValues)
} }
if u.createNamespace { for _, vFile := range u.ValuesFiles {
args = append(args, "--create-namespace")
}
if u.skipCrds {
args = append(args, "--skip-crds")
}
for _, vFile := range u.valuesFiles {
args = append(args, "--values", vFile) args = append(args, "--values", vFile)
} }
args = append(args, u.certs.flags()...)
// always set --history-max since it defaults to non-zero value args = append(args, u.Release, u.Chart)
args = append(args, fmt.Sprintf("--history-max=%d", u.historyMax))
args = append(args, u.release, u.chart)
u.cmd = command(helmBin, args...) u.cmd = command(helmBin, args...)
u.cmd.Stdout(u.stdout) u.cmd.Stdout(cfg.Stdout)
u.cmd.Stderr(u.stderr) u.cmd.Stderr(cfg.Stderr)
if u.debug { if cfg.Debug {
fmt.Fprintf(u.stderr, "Generated command: '%s'\n", u.cmd.String()) fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", u.cmd.String())
} }
return nil return nil

View File

@@ -2,12 +2,10 @@ package run
import ( import (
"fmt" "fmt"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/suite"
"strings" "strings"
"testing" "testing"
"github.com/golang/mock/gomock"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite"
) )
type UpgradeTestSuite struct { type UpgradeTestSuite struct {
@@ -33,53 +31,17 @@ func TestUpgradeTestSuite(t *testing.T) {
suite.Run(t, new(UpgradeTestSuite)) suite.Run(t, new(UpgradeTestSuite))
} }
func (suite *UpgradeTestSuite) TestNewUpgrade() {
cfg := env.NewTestConfig(suite.T())
cfg.ChartVersion = "seventeen"
cfg.DryRun = true
cfg.Wait = true
cfg.Values = "steadfastness,forthrightness"
cfg.StringValues = "tensile_strength,flexibility"
cfg.ValuesFiles = []string{"/root/price_inventory.yml"}
cfg.ReuseValues = true
cfg.Timeout = "go sit in the corner"
cfg.Chart = "billboard_top_100"
cfg.Release = "post_malone_circles"
cfg.Force = true
cfg.AtomicUpgrade = true
cfg.CleanupOnFail = true
up := NewUpgrade(*cfg)
suite.Equal(cfg.Chart, up.chart)
suite.Equal(cfg.Release, up.release)
suite.Equal(cfg.ChartVersion, up.chartVersion)
suite.Equal(true, up.dryRun)
suite.Equal(cfg.Wait, up.wait)
suite.Equal("steadfastness,forthrightness", up.values)
suite.Equal("tensile_strength,flexibility", up.stringValues)
suite.Equal([]string{"/root/price_inventory.yml"}, up.valuesFiles)
suite.Equal(cfg.ReuseValues, up.reuseValues)
suite.Equal(cfg.Timeout, up.timeout)
suite.Equal(cfg.Force, up.force)
suite.Equal(true, up.atomic)
suite.Equal(true, up.cleanupOnFail)
suite.NotNil(up.config)
suite.NotNil(up.certs)
}
func (suite *UpgradeTestSuite) TestPrepareAndExecute() { func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
defer suite.ctrl.Finish() defer suite.ctrl.Finish()
cfg := env.NewTestConfig(suite.T()) u := Upgrade{
cfg.Chart = "at40" Chart: "at40",
cfg.Release = "jonas_brothers_only_human" Release: "jonas_brothers_only_human",
}
u := NewUpgrade(*cfg)
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{"upgrade", "--install", "--history-max=10", "jonas_brothers_only_human", "at40"}, args) suite.Equal([]string{"upgrade", "--install", "jonas_brothers_only_human", "at40"}, args)
return suite.mockCmd return suite.mockCmd
} }
@@ -92,24 +54,23 @@ func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
err := u.Prepare() cfg := Config{}
err := u.Prepare(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
u.Execute() u.Execute(cfg)
} }
func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() { func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
defer suite.ctrl.Finish() defer suite.ctrl.Finish()
cfg := env.NewTestConfig(suite.T()) u := Upgrade{
cfg.Namespace = "melt" Chart: "at40",
cfg.Chart = "at40" Release: "shaed_trampoline",
cfg.Release = "shaed_trampoline" }
u := NewUpgrade(*cfg)
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{"--namespace", "melt", "upgrade", "--install", "--history-max=10", "shaed_trampoline", "at40"}, args) suite.Equal([]string{"--namespace", "melt", "upgrade", "--install", "shaed_trampoline", "at40"}, args)
return suite.mockCmd return suite.mockCmd
} }
@@ -117,31 +78,33 @@ func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
suite.mockCmd.EXPECT().Stdout(gomock.Any()) suite.mockCmd.EXPECT().Stdout(gomock.Any())
suite.mockCmd.EXPECT().Stderr(gomock.Any()) suite.mockCmd.EXPECT().Stderr(gomock.Any())
err := u.Prepare() cfg := Config{
Namespace: "melt",
}
err := u.Prepare(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
} }
func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() { func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
defer suite.ctrl.Finish() defer suite.ctrl.Finish()
cfg := env.NewTestConfig(suite.T()) u := Upgrade{
cfg.Chart = "hot_ac" Chart: "hot_ac",
cfg.Release = "maroon_5_memories" Release: "maroon_5_memories",
cfg.ChartVersion = "radio_edit" ChartVersion: "radio_edit",
cfg.DryRun = true DryRun: true,
cfg.Wait = true Wait: true,
cfg.Values = "age=35" Values: "age=35",
cfg.StringValues = "height=5ft10in" StringValues: "height=5ft10in",
cfg.ValuesFiles = []string{"/usr/local/stats", "/usr/local/grades"} ValuesFiles: []string{"/usr/local/stats", "/usr/local/grades"},
cfg.ReuseValues = true ReuseValues: true,
cfg.Timeout = "sit_in_the_corner" Timeout: "sit_in_the_corner",
cfg.Force = true Force: true,
cfg.AtomicUpgrade = true Atomic: true,
cfg.CleanupOnFail = true CleanupOnFail: true,
}
u := NewUpgrade(*cfg) cfg := Config{}
// inject a ca cert filename so repoCerts won't create any files that we'd have to clean up
u.certs.caCertFilename = "local_ca.cert"
command = func(path string, args ...string) cmd { command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path) suite.Equal(helmBin, path)
@@ -158,8 +121,6 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
"--set-string", "height=5ft10in", "--set-string", "height=5ft10in",
"--values", "/usr/local/stats", "--values", "/usr/local/stats",
"--values", "/usr/local/grades", "--values", "/usr/local/grades",
"--ca-file", "local_ca.cert",
"--history-max=10",
"maroon_5_memories", "hot_ac"}, args) "maroon_5_memories", "hot_ac"}, args)
return suite.mockCmd return suite.mockCmd
@@ -168,7 +129,7 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
suite.mockCmd.EXPECT().Stdout(gomock.Any()) suite.mockCmd.EXPECT().Stdout(gomock.Any())
suite.mockCmd.EXPECT().Stderr(gomock.Any()) suite.mockCmd.EXPECT().Stderr(gomock.Any())
err := u.Prepare() err := u.Prepare(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
} }
@@ -177,31 +138,34 @@ func (suite *UpgradeTestSuite) TestRequiresChartAndRelease() {
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()
u := NewUpgrade(env.Config{}) u := Upgrade{
u.release = "seth_everman_unskippable_cutscene" Release: "seth_everman_unskippable_cutscene",
}
err := u.Prepare() err := u.Prepare(Config{})
suite.EqualError(err, "chart is required", "Chart should be mandatory") suite.EqualError(err, "chart is required", "Chart should be mandatory")
u.release = "" u = Upgrade{
u.chart = "billboard_top_zero" Chart: "billboard_top_zero",
}
err = u.Prepare() err = u.Prepare(Config{})
suite.EqualError(err, "release is required", "Release should be mandatory") suite.EqualError(err, "release is required", "Release should be mandatory")
} }
func (suite *UpgradeTestSuite) TestPrepareDebugFlag() { func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
u := Upgrade{
Chart: "at40",
Release: "lewis_capaldi_someone_you_loved",
}
stdout := strings.Builder{} stdout := strings.Builder{}
stderr := strings.Builder{} stderr := strings.Builder{}
cfg := Config{
cfg := env.NewTestConfig(suite.T()) Debug: true,
cfg.Chart = "at40" Stdout: &stdout,
cfg.Release = "lewis_capaldi_someone_you_loved" Stderr: &stderr,
cfg.Debug = true }
cfg.Stdout = &stdout
cfg.Stderr = &stderr
u := NewUpgrade(*cfg)
command = func(path string, args ...string) cmd { command = func(path string, args ...string) cmd {
suite.mockCmd.EXPECT(). suite.mockCmd.EXPECT().
@@ -216,36 +180,10 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
suite.mockCmd.EXPECT(). suite.mockCmd.EXPECT().
Stderr(&stderr) Stderr(&stderr)
u.Prepare() u.Prepare(cfg)
want := fmt.Sprintf( want := fmt.Sprintf("Generated command: '%s --debug upgrade "+
"Generated command: '%s --debug upgrade --install --history-max=10 lewis_capaldi_someone_you_loved at40'\n", "--install lewis_capaldi_someone_you_loved at40'\n", helmBin)
helmBin,
)
suite.Equal(want, stderr.String()) suite.Equal(want, stderr.String())
suite.Equal("", stdout.String()) suite.Equal("", stdout.String())
} }
func (suite *UpgradeTestSuite) TestPrepareSkipCrdsFlag() {
defer suite.ctrl.Finish()
cfg := env.NewTestConfig(suite.T())
cfg.Chart = "at40"
cfg.Release = "cabbages_smell_great"
cfg.SkipCrds = true
u := NewUpgrade(*cfg)
command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path)
suite.Equal([]string{"upgrade", "--install", "--skip-crds", "--history-max=10", "cabbages_smell_great", "at40"}, args)
return suite.mockCmd
}
suite.mockCmd.EXPECT().Stdout(gomock.Any())
suite.mockCmd.EXPECT().Stderr(gomock.Any())
err := u.Prepare()
suite.Require().Nil(err)
}