90 Commits

Author SHA1 Message Date
Joachim Hill-Grannec
e0157d9bc2 Merge pull request #84 from BnMcG/noissue-repos-environment-interpolation
Allow environment variable interpolation in Helm repository configuration
2020-03-31 22:29:47 -06:00
Ben Magee
1f2da68bbb Remove built binary 2020-03-31 23:39:02 +01:00
Ben Magee
7e2f982af7 Update docs 2020-03-31 23:33:54 +01:00
Ben Magee
0502d76c63 Interpolate environment variables in the AddRepos configuration parameter 2020-03-31 23:07:28 +01:00
Joachim Hill-Grannec
591b084970 upgrading image to helm 3.1.1 2020-03-06 13:25:35 -06:00
Erin Call
f24a8e44ca Merge pull request #79 from pelotech/versions-in-bugs
Ask for version information in the bug template
2020-01-22 11:24:10 -08:00
Erin Call
8cb8a5d95d Merge branch 'master' into versions-in-bugs 2020-01-22 11:21:37 -08:00
Erin Call
a4834dd4f7 Merge pull request #77 from pelotech/interpolate-secrets
Interpolate environment variables into cfg.Values and cfg.StringValues
2020-01-22 11:18:17 -08:00
Erin Call
8b6a8fdd4b Ask for version information in the bug template [#78] 2020-01-22 10:06:57 -08:00
Erin Call
dbcef2699e Avoid polluted-env problems in config tests [#34]
I mean...it's *possible* someone will have SECRET_WATER set in their
env, right? Might as well be paranoid; it doesn't cost much.
2020-01-21 16:25:58 -08:00
Erin Call
22aa1df894 Don't bother trying to hide secrets in values [#34]
While testing this I discovered the secrets are revealed anyway, since
the lint/upgrade jobs' debug output includes the command they generated.
Might as well make the code a little simpler.
2020-01-21 16:23:55 -08:00
Erin Call
8f7b481934 Log debug information in loadValuesSecrets [#34] 2020-01-21 16:04:05 -08:00
Erin Call
e843b26759 Expand env vars in Values/StringValues [#34] 2020-01-21 15:46:32 -08:00
Erin Call
713dcd8317 Merge pull request #76 from pelotech/repo-certificates
Replace repo_ca_file setting with repo_certificate and repo_ca_certificate
2020-01-21 13:06:42 -08:00
Erin Call
18313eeb5c Use base64 strings for chart repo certs [#74]
This should be a more flexible option since certificates aren't likely
to be part of the actual workspace and may be environment-dependent. It
also mirrors the kube_certificate, which is nice.
2020-01-20 15:40:36 -08:00
Erin Call
ee6d8d1724 Merge pull request #73 from pelotech/godiomaticity
More-idiomatic initialization of Steps
2020-01-20 11:48:08 -08:00
Erin Call
fcddc6e077 Merge branch 'master' into godiomaticity 2020-01-20 11:24:28 -08:00
Erin Call
c4b11795e3 Merge pull request #75 from pelotech/repo-ca-file
Add a setting for chart repository CA certificates
2020-01-20 11:03:00 -08:00
Erin Call
ffa636ce47 Use lowercase envconfig tags throughout Config
Followup to discussion on #75. The important part is to have them
consistent, and I like the lowercase a little better since it matches
the casing in parameter_reference.md (and the code doesn't yell at me
:))
2020-01-20 10:54:52 -08:00
Erin Call
c38537ac32 Pass --ca-file to helm upgrade when applicable [#74] 2020-01-20 10:48:21 -08:00
Erin Call
1f7b6bb389 Add a setting for chart repository CA certificates [#74] 2020-01-20 09:15:56 -08:00
Erin Call
79532e7635 Extract the debug/namespace flags into run.Config [#67]
This is a general-purpose cleanup commit; every step except InitKube had
the same six "add the --debug and --namespace flags if applicable" code.
2020-01-17 11:12:53 -08:00
Erin Call
a21848484b Initialize run.Configs in the NewSTEP functions [#67]
This fixes the run package's leaky abstraction; other packages no longer
need to know or care that run.Config even exists.

Note that since the various Steps now depend on having a non-nil pointer
to a run.Config, it's unsafe (or at least risky) to initialize them
directly. They should be created with their NewSTEPNAME functions. All
their fields are now private, to reflect this.
2020-01-17 10:55:12 -08:00
Erin Call
d8ddb79ef4 Test InitKube's use of the Debug flag [#67]
(Just something I happened across while writing the previous commit)
2020-01-16 15:32:40 -08:00
Erin Call
231138563c Remove the cfg argument from Step.Execute [#67]
This is the first step toward removing run.Config entirely. InitKube was
the only Step that even used cfg in its Execute function; the rest just
discarded it.
2020-01-16 15:30:40 -08:00
Erin Call
88bb8085b0 Deduplicate the kubeValues data in InitKube [#67]
Now that the InitKube initialization happens inside its own package, the
private .values field can be populated at the same time, rather than
having to wait for Prepare().

Also clarified the config/template filename fields (configFile vs.
ConfigFile was particularly ambiguous).
2020-01-16 15:12:15 -08:00
Erin Call
21b9d32329 Remove the tiny helper functions from plan.go [#67]
Now that InitKube, AddRepo, and UpdateDependencies are initialized with
NewSTEPNAME functions, the helper functions in plan.go are
unnecessary--they do too little to be a useful abstraction, and they
aren't complex or frequently-used enough to be worth extracting.
2020-01-16 13:57:28 -08:00
Erin Call
588c7cb9f7 Initialize Steps with a NewSTEPNAME function [#67]
This seems to be be a more natural separation of concerns--the knowledge
of which config fields map to which parts of a Step belong to the Step,
not to the Plan.
2020-01-16 13:50:04 -08:00
Erin Call
16117eea2f Put the Config in a new env package [#67]
I'd like to be able to make calls like NewUpgrade(cfg) rather than
Upgrade{...}.Prepare, but I wouldn't be able to define a NewUpgrade
function while Config is in the helm package; there would be a circular
import when Plan tried to import run.
2020-01-14 10:32:20 -08:00
Erin Call
8a9cf23ab9 Merge pull request #71 from pelotech/alias-settings
Use clearer setting names, with backward-compatibility aliases
2020-01-09 11:45:11 -08:00
Erin Call
3d1a2227da Mention aliased settings in parameter_reference [#66] 2020-01-09 10:20:20 -08:00
Erin Call
a826f66425 Test settings aliases without the plugin_ prefix [#66] 2020-01-08 12:37:34 -08:00
Erin Call
71421fbaa5 Fix godotenv dependency in go.mod
go.mod got an update when I ran the tests. It should've happened in
51058470e; I'm not sure why it was never updated.
2020-01-08 11:08:39 -08:00
Erin Call
cfd8e33995 Use "backward-compatible", not "backwardS"
While writing docs in the previous commit, I noticed that we'd been
inconsistent in the naming scheme. Wikipedia's back-compat article
redirects from "backwards" to "backward", so I figure that's a
reasonable source of authority for which form to use.
2020-01-08 11:04:30 -08:00
Erin Call
a5342d7bac Default to the canonical setting names [#66]
The goal with these changes was to give users a clearer, more readable
interface, so we should present that interface up front and only
document the aliases as a backward-compatibility option.

I've renamed the envconfig tags to reflect the switch, but I left the
actual field names the same. I think they're sufficiently meaningful
inside the code, and leaving them unchanged avoids making a bunch of
churn in the rest of the code.
2020-01-08 11:02:29 -08:00
Erin Call
6aa1d79d56 Stabilize the logic for setting-alias conflicts [#66]
This includes a refactor to the way aliases are processed. I had been
thinking in terms of locking down the aliases names pretty tightly, in
order to provide an error if there are conflicts. After discussion with
@josmo, though, it seems like we can do it the same way we do for
"PLUGIN_"/non-prefixed variables, i.e. quietly override them.
2020-01-08 09:35:14 -08:00
Erin Call
1d1117ba49 Use "kube" in setting aliases [#66]
Nobody likes typing "kubernetes"! Writing out that whole word without
typos is the third hard problem in computer science.
2020-01-07 15:40:58 -08:00
Erin Call
04de280821 Rough draft of aliased settings [#66] 2020-01-07 15:25:54 -08:00
Erin Call
7cfe20db1f Merge pull request #69 from pelotech/cleanup-on-fail
Pass --cleanup-on-fail when so instructed
2020-01-07 13:18:31 -08:00
Erin Call
7fc8212451 Merge branch 'master' into cleanup-on-fail 2020-01-07 13:17:58 -08:00
Erin Call
7cb7ecec90 Merge pull request #70 from pelotech/external-param-reference
Mention the external docs in the PR template
2020-01-07 13:17:46 -08:00
Erin Call
55017eed0f Mention the external docs in the PR template [#63]
I just opened a PR with a config change and forgot about the plugin-
index docs...must be time to get around to this issue.
2020-01-07 13:04:21 -08:00
Erin Call
b6ba856c31 Pass CleanupOnFail to the Upgrade Step [#65]
I don't love the mismatch between the helm.Config field (CleanupOnFail)
and the setting name (cleanup_failed_upgrade). I do think the setting
name should contain "upgrade" since it's specific to the upgrade command,
but if I make the config field CleanupFailedUpgrade, it becomes the new
longest field name, and gofmt creates a bunch of churn. Is that a good
enough reason...?
2020-01-07 12:56:51 -08:00
Erin Call
8c6c6fbfa5 CleanupOnFail option for the upgrade Step [#65] 2020-01-07 12:53:55 -08:00
Erin Call
e071d23fef Merge pull request #68 from pelotech/ATOMICS
Atomics
2020-01-07 12:34:32 -08:00
Erin Call
c8b4ad4c46 Pass an atomic_upgrade setting to the Upgrade step [#64] 2020-01-07 12:21:55 -08:00
Erin Call
971e3f17cb Atomic option for the Upgrade step [#64] 2020-01-07 12:17:54 -08:00
Erin Call
ee3dc9ff0e Merge pull request #56 from pelotech/dotenv
Load .env files on startup
2020-01-02 13:51:57 -08:00
Erin Call
7ecfe70e3e Merge branch 'master' into dotenv 2020-01-02 13:30:19 -08:00
Erin Call
d5bd083bf5 Merge pull request #59 from pelotech/keep-history
Pass --keep-history when so instructed
2020-01-02 13:29:43 -08:00
Erin Call
8d8bcf78a7 Merge branch 'master' into keep-history 2020-01-02 13:26:53 -08:00
Joachim Hill-Grannec
222261d931 Merge branch 'master' into dotenv 2020-01-02 15:32:40 -05:00
Joachim Hill-Grannec
e694d93445 Merge pull request #57 from pelotech/documentation-template
Simple issue template for doc requests
2020-01-02 15:32:24 -05:00
Joachim Hill-Grannec
eaac6dd643 Merge pull request #62 from pelotech/values-arent-global
Put step-specific config in those steps
2020-01-02 15:32:05 -05:00
Joachim Hill-Grannec
c569059b87 Merge branch 'master' into dotenv 2020-01-02 15:30:06 -05:00
Erin Call
45428a2e25 Merge branch 'master' into keep-history 2020-01-02 12:29:32 -08:00
Erin Call
7b816ea257 Merge branch 'master' into values-arent-global 2020-01-02 12:29:15 -08:00
Erin Call
3c44be059e Merge pull request #60 from pelotech/lint-strictly
Pass --strict to `helm lint` when so instructed
2020-01-02 12:27:00 -08:00
Erin Call
11ffdc7210 Merge branch 'master' into lint-strictly 2020-01-02 11:44:09 -08:00
Erin Call
b4a13d9971 Merge pull request #58 from pelotech/conflict-resolution-doc
Docs for settings/environment conflict resolution
2020-01-02 11:43:57 -08:00
Erin Call
4330728215 Put step-specific config in those steps [#61]
This is just something that's been bugging me for a while--they're
specific to Lint and Upgrade, so that's where they belong.
2020-01-02 11:38:41 -08:00
Erin Call
3ae13d4b3c Pass --strict to helm lint when so instructed [#28] 2020-01-02 11:25:13 -08:00
Erin Call
17724e7015 Pass --keep-history when so instructed [#24] 2020-01-02 10:58:58 -08:00
Erin Call
04a2c48ece Docs for settings/environment conflict resolution [#45] 2020-01-02 09:23:02 -08:00
Erin Call
997a33fc0d Simple issue template for doc requests [#46] 2020-01-02 09:12:51 -08:00
Erin Call
280c8577a4 Merge branch 'master' into dotenv 2020-01-02 09:08:25 -08:00
Erin Call
51058470e5 Load .env files on startup [#6] 2020-01-02 09:05:26 -08:00
Joachim Hill-Grannec
7d750f097d Merge pull request #40 from pelotech/no-eks-support
Mention that EKS isn't currently supported [#5]
2020-01-01 16:47:18 -05:00
Erin Call
9c91a415b0 Merge branch 'master' into no-eks-support 2020-01-01 09:09:01 -08:00
Joachim Hill-Grannec
9e8de53f8f Merge pull request #52 from josmo/contributions
Directions on how to setup custom build of images till PR gets merged
2019-12-31 18:08:31 -05:00
Joachim Hill-Grannec
2893e09651 Merge branch 'master' into contributions 2019-12-31 18:07:55 -05:00
Joachim Hill-Grannec
69a15a8d48 Merge pull request #51 from pelotech/deprecation-warnings
Emit warnings about deprecated settings
2019-12-31 18:07:32 -05:00
Joachim Hill-Grannec
27827a2940 Merge branch 'master' into deprecation-warnings 2019-12-31 18:03:52 -05:00
Joachim Hill-Grannec
eb2d280d59 Merge pull request #55 from josmo/pin-image
pin version of docker image to alpine/helm:3.0.2
2019-12-31 18:03:32 -05:00
Joachim Hill-Grannec
3bf73acb42 Merge branch 'master' into deprecation-warnings 2019-12-31 18:03:00 -05:00
Joachim Hill-Grannec
997f49bb8d Merge branch 'master' into pin-image 2019-12-31 18:01:42 -05:00
Erin Call
86984bcecd Merge pull request #50 from pelotech/no-prefix
Remove support for the prefix setting [#48]
2019-12-31 15:00:52 -08:00
Joachim Hill-Grannec
c4329833f8 Merge branch 'master' into no-prefix 2019-12-31 18:00:00 -05:00
Joachim Hill-Grannec
3fa2d71559 pin version of docker image to alpine/helm:3.0.2 2019-12-31 17:57:32 -05:00
Joachim Hill-Grannec
f919966cf0 Merge pull request #54 from josmo/migration-link
link to migrate the deployments in the cluster
2019-12-31 17:56:21 -05:00
Joachim Hill-Grannec
fed4de2ed9 Update README.md
Co-Authored-By: Erin Call <hello@erincall.com>
2019-12-31 17:54:46 -05:00
Joachim Hill-Grannec
e641102770 link to migrate the deployments in the cluster 2019-12-31 17:34:53 -05:00
Joachim Hill-Grannec
51db551995 Merge pull request #53 from josmo/badges
Adding basic badges
2019-12-31 17:27:59 -05:00
Joachim Hill-Grannec
2389268fa2 Adding basic badges 2019-12-31 17:24:12 -05:00
Joachim Hill-Grannec
fe7ee09350 add link to contributing 2019-12-31 14:48:21 -05:00
Joachim Hill-Grannec
680989754a Directions on how to setup custom build of images till PR gets merged 2019-12-31 14:19:56 -05:00
Erin Call
7cd46bb8b1 Emit warnings about deprecated settings [#10]
These aren't an error case--the plugin will work just fine--but users
should be aware they (the settings) aren't being respected.
2019-12-31 10:03:53 -08:00
Erin Call
ab7abb699a Remove support for the prefix setting [#48]
The setting isn't necessary with modern versions of Drone, and it
creates a lot of edge-cases. The use-case doesn't justify the added
complexity.
2019-12-31 09:28:42 -08:00
Erin Call
8e5a32ff85 Merge branch 'master' into no-eks-support 2019-12-30 13:45:21 -08:00
Erin Call
12c8e08d6f Mention that EKS isn't currently supported [#5]
We'd like to support it eventually, but the current state of affairs
doesn't justify the effort.

Also removed some vestigial code that was copy-pasta from the kubeconfig
in drone-helm.
2019-12-27 10:49:22 -08:00
36 changed files with 1422 additions and 1227 deletions

View File

@@ -7,6 +7,9 @@ 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 -->

11
.github/ISSUE_TEMPLATE/documentation.md vendored Normal file
View File

@@ -0,0 +1,11 @@
---
name: Documentation
about: Docs you'd like to see, or questions about existing docs
title: ''
labels: documentation
assignees: ''
---
**What needs explanation:**
<!-- e.g. "what happens when ____", "how do I ___", etc. -->

View File

@@ -3,6 +3,8 @@
Pre-merge checklist: Pre-merge checklist:
* [ ] Code changes have tests * [ ] Code changes have tests
* [ ] Any changes to the config are documented in `docs/parameter_reference.md` * [ ] Any config changes are documented:
* [ ] Any new _required_ config is documented in `README.md` * If the change touches _required_ config, there's a corresponding update to `README.md`
* There's a corresponding update to `docs/parameter_reference.md`
* There's a pull request to update [the parameter reference in drone-plugin-index](https://github.com/drone/drone-plugin-index/blob/master/content/pelotech/drone-helm3/index.md)
* [ ] Any large changes have been verified by running a Drone job * [ ] Any large changes have been verified by running a Drone job

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@
*.so *.so
*.dylib *.dylib
.idea
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

View File

@@ -1,4 +1,4 @@
FROM alpine/helm FROM alpine/helm:3.1.1
MAINTAINER Erin Call <erin@liffft.com> MAINTAINER Erin Call <erin@liffft.com>
COPY build/drone-helm /bin/drone-helm COPY build/drone-helm /bin/drone-helm

View File

@@ -1,12 +1,16 @@
# Drone plugin for Helm 3 # Drone plugin for Helm 3
[![Build Status](https://cloud.drone.io/api/badges/pelotech/drone-helm3/status.svg)](https://cloud.drone.io/pelotech/drone-helm3)
[![Go Report](https://goreportcard.com/badge/github.com/pelotech/drone-helm3)](https://goreportcard.com/report/github.com/pelotech/drone-helm3)
[![](https://images.microbadger.com/badges/image/pelotech/drone-helm3.svg)](https://microbadger.com/images/pelotech/drone-helm3 "Get your own image badge on microbadger.com")
This plugin provides an interface between [Drone](https://drone.io/) and [Helm 3](https://github.com/kubernetes/helm): This plugin provides an interface between [Drone](https://drone.io/) and [Helm 3](https://github.com/kubernetes/helm):
* Lint your charts * Lint your charts
* Deploy your service * Deploy your service
* Delete your service * Delete your service
The plugin is inpsired by [drone-helm](https://github.com/ipedrazas/drone-helm), which fills the same role for Helm 2. It provides a comparable feature-set and the configuration settings are backwards-compatible. The plugin is inpsired by [drone-helm](https://github.com/ipedrazas/drone-helm), which fills the same role for Helm 2. It provides a comparable feature-set and the configuration settings are backward-compatible.
## Example configuration ## Example configuration
@@ -19,7 +23,7 @@ steps:
- name: lint - name: lint
image: pelotech/drone-helm3 image: pelotech/drone-helm3
settings: settings:
helm_command: lint mode: lint
chart: ./ chart: ./
``` ```
@@ -30,12 +34,12 @@ steps:
- name: deploy - name: deploy
image: pelotech/drone-helm3 image: pelotech/drone-helm3
settings: settings:
helm_command: upgrade mode: upgrade
chart: ./ chart: ./
release: my-project release: my-project
environment: environment:
API_SERVER: https://my.kubernetes.installation/clusters/a-1234 KUBE_API_SERVER: https://my.kubernetes.installation/clusters/a-1234
KUBERNETES_TOKEN: KUBE_TOKEN:
from_secret: kubernetes_token from_secret: kubernetes_token
``` ```
@@ -46,20 +50,23 @@ steps:
- name: uninstall - name: uninstall
image: pelotech/drone-helm3 image: pelotech/drone-helm3
settings: settings:
helm_command: uninstall mode: uninstall
release: my-project release: my-project
environment: environment:
API_SERVER: https://my.kubernetes.installation/clusters/a-1234 KUBE_API_SERVER: https://my.kubernetes.installation/clusters/a-1234
KUBERNETES_TOKEN: KUBE_TOKEN:
from_secret: kubernetes_token from_secret: kubernetes_token
``` ```
## Upgrading from drone-helm ## Upgrading from drone-helm
drone-helm3 is largely backwards-compatible with drone-helm. There are some known differences: drone-helm3 is largely backward-compatible with drone-helm. There are some known differences:
* `prefix` must be supplied via the `settings` block, not `environment`. * You'll need to migrate the deployments in the cluster [helm-v2-to-helm-v3](https://helm.sh/blog/migrate-from-helm-v2-to-helm-v3/).
* Several settings no longer have any effect: * EKS is not supported. See [#5](https://github.com/pelotech/drone-helm3/issues/5) for more information.
* The `prefix` setting is no longer supported. If you were relying on the `prefix` setting with `secrets: [...]`, you'll need to switch to the `from_secret` syntax.
* During uninstallations, the release history is purged by default. Use `keep_history: true` to return to the old behavior.
* Several settings no longer have any effect. The plugin will produce warnings if any of these are present:
* `purge` -- this is the default behavior in Helm 3 * `purge` -- this is the default behavior in Helm 3
* `recreate_pods` * `recreate_pods`
* `tiller_ns` * `tiller_ns`
@@ -67,5 +74,22 @@ drone-helm3 is largely backwards-compatible with drone-helm. There are some know
* `canary_image` * `canary_image`
* `client_only` * `client_only`
* `stable_repo_url` * `stable_repo_url`
* Several settings have been renamed, to clarify their purpose and provide a more consistent naming scheme. For backward-compatibility, the old names are still available as aliases. If the old and new names are both present, the updated form takes priority. Conflicting settings will make your `.drone.yml` harder to understand, so we recommend updating to the new names:
* `helm_command` is now `mode`
° `helm_repos` is now `add_repos`
* `api_server` is now `kube_api_server`
* `service_account` is now `kube_service_account`
* `kubernetes_token` is now `kube_token`
* `kubernetes_certificate` is now `kube_certificate`
* `wait` is now `wait_for_upgrade`
* `force` is now `force_upgrade`
Since helm 3 does not require Tiller, we also recommend switching to a service account with less-expansive permissions. Since helm 3 does not require Tiller, we also recommend switching to a service account with less-expansive permissions.
### [Contributing](docs/contributing.md)
This repo is setup in a way that if you enable a personal drone server to build your fork it will
build and publish your image (makes it easier to test PRs and use the image till the contributions get merged)
* Build local ```DRONE_REPO_OWNER=josmo DRONE_REPO_NAME=drone-ecs drone exec```
* on your server (or cloud.drone.io) just make sure you have DOCKER_USERNAME, DOCKER_PASSWORD, and PLUGIN_REPO set as secrets

View File

@@ -24,16 +24,4 @@ users:
user: user:
{{- if .Token }} {{- if .Token }}
token: {{ .Token }} token: {{ .Token }}
{{- else if .EKSCluster }}
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
command: aws-iam-authenticator
args:
- "token"
- "-i"
- "{{ .EKSCluster }}"
{{- if .EKSRoleARN }}
- "-r"
- "{{ .EKSRoleARN }}"
{{- end }}
{{- end }} {{- end }}

View File

@@ -4,11 +4,13 @@ import (
"fmt" "fmt"
"os" "os"
_ "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 := helm.NewConfig(os.Stdout, os.Stderr) cfg, err := env.NewConfig(os.Stdout, os.Stderr)
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,18 +1,19 @@
# Parameter reference # Parameter reference
## Global ## Global
| Param name | Type | Purpose | | Param name | Type | Alias | Purpose |
|---------------------|-----------------|---------| |---------------------|-----------------|--------------|---------|
| helm_command | string | Indicates the operation to perform. Recommended, but not required. Valid options are `upgrade`, `uninstall`, `lint`, and `help`. | | mode | string | helm_command | 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.|
| helm_repos | list\<string\> | Calls `helm repo add $repo` before running the main command. Each string should be formatted as `repo_name=https://repo.url/`. | | 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/`. |
| namespace | string | Kubernetes namespace to use for this operation. | | repo_certificate | string | | Base64 encoded TLS certificate for a chart repository. |
| prefix | string | Expect environment variables to be prefixed with the given string. For more details, see "Using the prefix setting" below. | | 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
Linting is only triggered when the `helm_command` setting is "lint". Linting is only triggered when the `mode` setting is "lint".
| Param name | Type | Required | Purpose | | Param name | Type | Required | Purpose |
|---------------|----------------|----------|---------| |---------------|----------------|----------|---------|
@@ -20,49 +21,55 @@ Linting is only triggered when the `helm_command` setting is "lint".
| values | list\<string\> | | Chart values to use as the `--set` argument to `helm lint`. | | values | list\<string\> | | Chart values to use as the `--set` argument to `helm lint`. |
| string_values | list\<string\> | | Chart values to use as the `--set-string` argument to `helm lint`. | | string_values | list\<string\> | | Chart values to use as the `--set-string` argument to `helm lint`. |
| values_files | list\<string\> | | Values to use as `--values` arguments to `helm lint`. | | values_files | list\<string\> | | Values to use as `--values` arguments to `helm lint`. |
| lint_strictly | boolean | | Pass `--strict` to `helm lint`, to turn warnings into errors. |
## Installation ## Installation
Installations are triggered when the `helm_command` setting is "upgrade." They can also be triggered when the build was triggered by a `push`, `tag`, `deployment`, `pull_request`, `promote`, or `rollback` Drone event. 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 | Purpose | | Param name | Type | Required | Alias | 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. |
| api_server | string | yes | API endpoint for the Kubernetes cluster. | | kube_api_server | string | yes | api_server | API endpoint for the Kubernetes cluster. |
| kubernetes_token | string | yes | Token for authenticating to Kubernetes. | | kube_token | string | yes | kubernetes_token | Token for authenticating to Kubernetes. |
| 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`. |
| kubernetes_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. |
| 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 | boolean | | Wait until kubernetes resources are in a ready state before marking the installation successful. | | wait_for_upgrade | boolean | | wait | Wait until kubernetes resources are in a ready state before marking the installation successful. |
| timeout | duration | | Timeout for any *individual* Kubernetes operation. The installation's full runtime may exceed this duration. | | timeout | duration | | | Timeout for any *individual* Kubernetes operation. The installation's full runtime may exceed this duration. |
| force | boolean | | Pass `--force` to `helm upgrade`. | | force_upgrade | boolean | | force | Pass `--force` to `helm upgrade`. |
| values | list\<string\> | | Chart values to use as the `--set` argument to `helm upgrade`. | | atomic_upgrade | boolean | | | Pass `--atomic` to `helm upgrade`. |
| string_values | list\<string\> | | Chart values to use as the `--set-string` argument to `helm upgrade`. | | cleanup_failed_upgrade | boolean | | | Pass `--cleanup-on-fail` 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. |
## Uninstallation ## Uninstallation
Uninstallations are triggered when the `helm_command` setting is "uninstall" or "delete." They can also be triggered when the build was triggered by a `delete` Drone event. 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 | Purpose | | Param name | Type | Required | Alias | Purpose |
|------------------------|----------|----------|---------| |------------------------|----------|----------|------------------------|---------|
| release | string | yes | The release name for helm to use. | | release | string | yes | | The release name for helm to use. |
| api_server | string | yes | API endpoint for the Kubernetes cluster. | | kube_api_server | string | yes | api_server | API endpoint for the Kubernetes cluster. |
| kubernetes_token | string | yes | Token for authenticating to Kubernetes. | | kube_token | string | yes | kubernetes_token | Token for authenticating to Kubernetes. |
| 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`. |
| kubernetes_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. |
| dry_run | boolean | | Pass `--dry-run` to `helm uninstall`. | | keep_history | boolean | | | Pass `--keep-history` to `helm uninstall`, to retain the release history. |
| timeout | duration | | Timeout for any *individual* Kubernetes operation. The uninstallation's full runtime may exceed this duration. | | dry_run | boolean | | | Pass `--dry-run` to `helm uninstall`. |
| skip_tls_verify | boolean | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. | | timeout | duration | | | Timeout for any *individual* Kubernetes operation. The uninstallation's full runtime may exceed this duration. |
| chart | string | | Required when the global `update_dependencies` parameter is true. No effect otherwise. | | skip_tls_verify | boolean | | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. |
| chart | string | | | Required when the global `update_dependencies` parameter is true. No effect otherwise. |
### Where to put settings ### Where to put settings
Any setting (with the exception of `prefix`; [see below](#user-content-using-the-prefix-setting)), can go in either the `settings` or `environment` section. Any setting can go in either the `settings` or `environment` section. If a setting exists in _both_ sections, the version in `environment` will override the version in `settings`.
We recommend putting all drone-helm3 configuration in the `settings` block and limiting the `environment` block to variables that are used when building your charts.
### Formatting non-string values ### Formatting non-string values
@@ -88,44 +95,37 @@ values_files: [ "./over_9,000.yml" ]
values_files: [ "./over_9", "000.yml" ] values_files: [ "./over_9", "000.yml" ]
``` ```
### Using the `prefix` setting ### Interpolating secrets into the `values`, `string_values` and `add_repos` settings
Because the prefix setting is meta-configuration, it has some inherent edge-cases. Here is what it does in the cases we've thought of: 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.
Unlike the other settings, it must be declared in the `settings` block, not `environment`:
```yaml ```yaml
settings:
prefix: helm # drone-helm3 will look for environment variables called HELM_VARNAME
environment: environment:
prefix: armet # no effect 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
``` ```
It does not apply to configuration in the `settings` block, only in `environment`: Variables intended for interpolation must be set in the `environment` section, not `settings`.
```yaml ### Backward-compatibility aliases
settings:
prefix: helm
helm_timeout: 5m # no effect
environment:
helm_timeout: 2m # timeout will be 2 minutes
```
If the environment contains a variable in non-prefixed form, it will still be applied: Some settings have alternate names, for backward-compatibility with drone-helm. We recommend using the canonical name unless you require the backward-compatible form.
```yaml | Canonical name | Alias |
settings: |----------------------|-------|
prefix: helm | mode | helm_command |
environment: | add_repos | helm_repos |
timeout: 2m # timeout will be 2 minutes | kube_api_server | api_server |
``` | kube_service_account | service_account |
| kube_token | kubernetes_token |
If the environment contains both the prefixed and non-prefixed forms, drone-helm3 will use the prefixed form: | kube_certificate | kubernetes_certificate |
| wait_for_upgrade | wait |
```yaml | force_upgrade | force |
settings:
prefix: helm
environment:
timeout: 5m # overridden
helm_timeout: 2m # timeout will be 2 minutes
```

1
go.mod
View File

@@ -4,6 +4,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/kelseyhightower/envconfig v1.4.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 golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect

2
go.sum
View File

@@ -2,6 +2,8 @@ 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/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 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=

155
internal/env/config.go vendored Normal file
View File

@@ -0,0 +1,155 @@
package env
import (
"fmt"
"io"
"os"
"regexp"
"strings"
"github.com/kelseyhightower/envconfig"
)
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"` // Call `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
KubeToken string `split_words:"true"` // Kubernetes authentication token to put in .kube/config
SkipTLSVerify bool `envconfig:"skip_tls_verify"` // Put insecure-skip-tls-verify in .kube/config
Certificate string `envconfig:"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`
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`
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,
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 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))
}
}
}
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"`
}

252
internal/env/config_test.go vendored Normal file
View File

@@ -0,0 +1,252 @@
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) 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,84 +0,0 @@
package helm
import (
"fmt"
"github.com/kelseyhightower/envconfig"
"io"
"regexp"
)
var justNumbers = regexp.MustCompile(`^\d+$`)
// 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. It may, however, be prefixed with the value in `$PLUGIN_PREFIX`.
type Config struct {
// Configuration for drone-helm itself
Command string `envconfig:"HELM_COMMAND"` // Helm command to run
DroneEvent string `envconfig:"DRONE_BUILD_EVENT"` // Drone event that invoked this plugin.
UpdateDependencies bool `split_words:"true"` // Call `helm dependency update` before the main command
AddRepos []string `envconfig:"HELM_REPOS"` // Call `helm repo add` before the main command
Prefix string `` // Prefix to use when looking up secret env vars
Debug bool `` // Generate debug output and pass --debug to all helm commands
Values string `` // Argument to pass to --set in applicable helm commands
StringValues string `split_words:"true"` // Argument to pass to --set-string in applicable helm commands
ValuesFiles []string `split_words:"true"` // Arguments to pass to --values in applicable helm commands
Namespace string `` // Kubernetes namespace for all helm commands
KubeToken string `envconfig:"KUBERNETES_TOKEN"` // Kubernetes authentication token to put in .kube/config
SkipTLSVerify bool `envconfig:"SKIP_TLS_VERIFY"` // Put insecure-skip-tls-verify in .kube/config
Certificate string `envconfig:"KUBERNETES_CERTIFICATE"` // The Kubernetes cluster CA's self-signed certificate (must be base64-encoded)
APIServer string `envconfig:"API_SERVER"` // The Kubernetes cluster's API endpoint
ServiceAccount string `split_words:"true"` // Account to use for connecting to the Kubernetes cluster
ChartVersion string `split_words:"true"` // Specific chart version to use in `helm upgrade`
DryRun bool `split_words:"true"` // Pass --dry-run to applicable helm commands
Wait bool `` // Pass --wait to applicable helm commands
ReuseValues bool `split_words:"true"` // Pass --reuse-values to `helm upgrade`
Timeout string `` // Argument to pass to --timeout in applicable helm commands
Chart string `` // Chart argument to use in applicable helm commands
Release string `` // Release argument to use in applicable helm commands
Force bool `` // Pass --force to applicable helm commands
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) {
cfg := Config{
Stdout: stdout,
Stderr: stderr,
}
if err := envconfig.Process("plugin", &cfg); err != nil {
return nil, err
}
prefix := cfg.Prefix
if err := envconfig.Process("", &cfg); err != nil {
return nil, err
}
if prefix != "" {
if err := envconfig.Process(cfg.Prefix, &cfg); err != nil {
return nil, err
}
}
if justNumbers.MatchString(cfg.Timeout) {
cfg.Timeout = fmt.Sprintf("%ss", cfg.Timeout)
}
if cfg.Debug && cfg.Stderr != nil {
cfg.logDebug()
}
return &cfg, nil
}
func (cfg Config) logDebug() {
if cfg.KubeToken != "" {
cfg.KubeToken = "(redacted)"
}
fmt.Fprintf(cfg.Stderr, "Generated config: %+v\n", cfg)
}

View File

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

View File

@@ -6,7 +6,6 @@ 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"
) )
@@ -34,29 +33,29 @@ func (m *MockStep) EXPECT() *MockStepMockRecorder {
} }
// Prepare mocks base method // Prepare mocks base method
func (m *MockStep) Prepare(arg0 run.Config) error { func (m *MockStep) Prepare() error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Prepare", arg0) ret := m.ctrl.Call(m, "Prepare")
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(arg0 interface{}) *gomock.Call { func (mr *MockStepMockRecorder) Prepare() *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), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockStep)(nil).Prepare))
} }
// Execute mocks base method // Execute mocks base method
func (m *MockStep) Execute(arg0 run.Config) error { func (m *MockStep) Execute() error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Execute", arg0) ret := m.ctrl.Call(m, "Execute")
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(arg0 interface{}) *gomock.Call { func (mr *MockStepMockRecorder) Execute() *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), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockStep)(nil).Execute))
} }

View File

@@ -2,6 +2,7 @@ package helm
import ( import (
"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"
) )
@@ -13,30 +14,20 @@ const (
// A Step is one step in the plan. // A Step is one step in the plan.
type Step interface { type Step interface {
Prepare(run.Config) error Prepare() error
Execute(run.Config) error Execute() 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 Config cfg env.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 Config) (*Plan, error) { func NewPlan(cfg env.Config) (*Plan, error) {
p := Plan{ p := Plan{
cfg: cfg, cfg: cfg,
runCfg: run.Config{
Debug: cfg.Debug,
Values: cfg.Values,
StringValues: cfg.StringValues,
ValuesFiles: cfg.ValuesFiles,
Namespace: cfg.Namespace,
Stdout: cfg.Stdout,
Stderr: cfg.Stderr,
},
} }
p.steps = (*determineSteps(cfg))(cfg) p.steps = (*determineSteps(cfg))(cfg)
@@ -46,7 +37,7 @@ func NewPlan(cfg 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(p.runCfg); err != nil { if err := step.Prepare(); 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
} }
@@ -57,7 +48,7 @@ func NewPlan(cfg 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 Config) *func(Config) []Step { func determineSteps(cfg env.Config) *func(env.Config) []Step {
switch cfg.Command { switch cfg.Command {
case "upgrade": case "upgrade":
return &upgrade return &upgrade
@@ -86,7 +77,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(p.runCfg); err != nil { if err := step.Execute(); err != nil {
return fmt.Errorf("while executing %T step: %w", step, err) return fmt.Errorf("while executing %T step: %w", step, err)
} }
} }
@@ -94,87 +85,43 @@ func (p *Plan) Execute() error {
return nil return nil
} }
var upgrade = func(cfg Config) []Step { var upgrade = func(cfg env.Config) []Step {
steps := initKube(cfg) var steps []Step
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,
ReuseValues: cfg.ReuseValues,
Timeout: cfg.Timeout,
Force: cfg.Force,
})
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,
})
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,
})
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.AddRepo{ steps = append(steps, run.NewAddRepo(cfg, repo))
Repo: repo,
})
} }
if cfg.UpdateDependencies {
steps = append(steps, run.NewDepUpdate(cfg))
}
steps = append(steps, run.NewUpgrade(cfg))
return steps return steps
} }
func depUpdate(cfg Config) []Step { var uninstall = func(cfg env.Config) []Step {
return []Step{ var steps []Step
&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,6 +7,7 @@ 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"
) )
@@ -25,43 +26,29 @@ func (suite *PlanTestSuite) TestNewPlan() {
stepTwo := NewMockStep(ctrl) stepTwo := NewMockStep(ctrl)
origHelp := help origHelp := help
help = func(cfg Config) []Step { help = func(cfg env.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 := Config{ cfg := env.Config{
Command: "help", Command: "help",
Debug: false, Debug: false,
Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
Namespace: "outer",
Stdout: &stdout,
Stderr: &stderr,
}
runCfg := run.Config{
Debug: false,
Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
Namespace: "outer", Namespace: "outer",
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
} }
stepOne.EXPECT(). stepOne.EXPECT().
Prepare(runCfg) Prepare()
stepTwo.EXPECT(). stepTwo.EXPECT().
Prepare(runCfg) Prepare()
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() {
@@ -71,17 +58,17 @@ func (suite *PlanTestSuite) TestNewPlanAbortsOnError() {
stepTwo := NewMockStep(ctrl) stepTwo := NewMockStep(ctrl)
origHelp := help origHelp := help
help = func(cfg Config) []Step { help = func(cfg env.Config) []Step {
return []Step{stepOne, stepTwo} return []Step{stepOne, stepTwo}
} }
defer func() { help = origHelp }() defer func() { help = origHelp }()
cfg := Config{ cfg := env.Config{
Command: "help", Command: "help",
} }
stepOne.EXPECT(). stepOne.EXPECT().
Prepare(gomock.Any()). Prepare().
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)
@@ -95,18 +82,15 @@ 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(runCfg). Execute().
Times(1) Times(1)
stepTwo.EXPECT(). stepTwo.EXPECT().
Execute(runCfg). Execute().
Times(1) Times(1)
suite.NoError(plan.Execute()) suite.NoError(plan.Execute())
@@ -118,15 +102,12 @@ 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(runCfg). Execute().
Times(1). Times(1).
Return(fmt.Errorf("oh, he'll gnaw")) Return(fmt.Errorf("oh, he'll gnaw"))
@@ -135,40 +116,14 @@ func (suite *PlanTestSuite) TestExecuteAbortsOnError() {
} }
func (suite *PlanTestSuite) TestUpgrade() { func (suite *PlanTestSuite) TestUpgrade() {
cfg := Config{ steps := upgrade(env.Config{})
ChartVersion: "seventeen",
DryRun: true,
Wait: true,
ReuseValues: true,
Timeout: "go sit in the corner",
Chart: "billboard_top_100",
Release: "post_malone_circles",
Force: true,
}
steps := upgrade(cfg)
suite.Require().Equal(2, len(steps), "upgrade should return 2 steps") suite.Require().Equal(2, len(steps), "upgrade should return 2 steps")
suite.Require().IsType(&run.InitKube{}, steps[0]) suite.IsType(&run.InitKube{}, steps[0])
suite.IsType(&run.Upgrade{}, steps[1])
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,
ReuseValues: cfg.ReuseValues,
Timeout: cfg.Timeout,
Force: cfg.Force,
}
suite.Equal(expected, upgrade)
} }
func (suite *PlanTestSuite) TestUpgradeWithUpdateDependencies() { func (suite *PlanTestSuite) TestUpgradeWithUpdateDependencies() {
cfg := Config{ cfg := env.Config{
UpdateDependencies: true, UpdateDependencies: true,
} }
steps := upgrade(cfg) steps := upgrade(cfg)
@@ -178,7 +133,7 @@ func (suite *PlanTestSuite) TestUpgradeWithUpdateDependencies() {
} }
func (suite *PlanTestSuite) TestUpgradeWithAddRepos() { func (suite *PlanTestSuite) TestUpgradeWithAddRepos() {
cfg := Config{ cfg := env.Config{
AddRepos: []string{ AddRepos: []string{
"machine=https://github.com/harold_finch/themachine", "machine=https://github.com/harold_finch/themachine",
}, },
@@ -189,45 +144,15 @@ func (suite *PlanTestSuite) TestUpgradeWithAddRepos() {
} }
func (suite *PlanTestSuite) TestUninstall() { func (suite *PlanTestSuite) TestUninstall() {
cfg := Config{ steps := uninstall(env.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",
}
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.Require().IsType(&run.InitKube{}, steps[0]) suite.IsType(&run.InitKube{}, steps[0])
init, _ := steps[0].(*run.InitKube) suite.IsType(&run.Uninstall{}, steps[1])
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,
}
suite.Equal(expected, actual)
} }
func (suite *PlanTestSuite) TestUninstallWithUpdateDependencies() { func (suite *PlanTestSuite) TestUninstallWithUpdateDependencies() {
cfg := Config{ cfg := env.Config{
UpdateDependencies: true, UpdateDependencies: true,
} }
steps := uninstall(cfg) steps := uninstall(cfg)
@@ -236,83 +161,14 @@ 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() {
cfg := Config{ steps := lint(env.Config{})
Chart: "./flow", suite.Require().Equal(1, len(steps))
} suite.IsType(&run.Lint{}, steps[0])
steps := lint(cfg)
suite.Equal(1, len(steps))
want := &run.Lint{
Chart: "./flow",
}
suite.Equal(want, steps[0])
} }
func (suite *PlanTestSuite) TestLintWithUpdateDependencies() { func (suite *PlanTestSuite) TestLintWithUpdateDependencies() {
cfg := Config{ cfg := env.Config{
UpdateDependencies: true, UpdateDependencies: true,
} }
steps := lint(cfg) steps := lint(cfg)
@@ -321,7 +177,7 @@ func (suite *PlanTestSuite) TestLintWithUpdateDependencies() {
} }
func (suite *PlanTestSuite) TestLintWithAddRepos() { func (suite *PlanTestSuite) TestLintWithAddRepos() {
cfg := Config{ cfg := env.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)
@@ -330,7 +186,7 @@ func (suite *PlanTestSuite) TestLintWithAddRepos() {
} }
func (suite *PlanTestSuite) TestDeterminePlanUpgradeCommand() { func (suite *PlanTestSuite) TestDeterminePlanUpgradeCommand() {
cfg := Config{ cfg := env.Config{
Command: "upgrade", Command: "upgrade",
} }
stepsMaker := determineSteps(cfg) stepsMaker := determineSteps(cfg)
@@ -338,7 +194,7 @@ func (suite *PlanTestSuite) TestDeterminePlanUpgradeCommand() {
} }
func (suite *PlanTestSuite) TestDeterminePlanUpgradeFromDroneEvent() { func (suite *PlanTestSuite) TestDeterminePlanUpgradeFromDroneEvent() {
cfg := Config{} cfg := env.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 {
@@ -349,16 +205,16 @@ func (suite *PlanTestSuite) TestDeterminePlanUpgradeFromDroneEvent() {
} }
func (suite *PlanTestSuite) TestDeterminePlanUninstallCommand() { func (suite *PlanTestSuite) TestDeterminePlanUninstallCommand() {
cfg := Config{ cfg := env.Config{
Command: "uninstall", Command: "uninstall",
} }
stepsMaker := determineSteps(cfg) stepsMaker := determineSteps(cfg)
suite.Same(&uninstall, stepsMaker) suite.Same(&uninstall, stepsMaker)
} }
// helm_command = delete is provided as an alias for backwards-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 := Config{ cfg := env.Config{
Command: "delete", Command: "delete",
} }
stepsMaker := determineSteps(cfg) stepsMaker := determineSteps(cfg)
@@ -366,7 +222,7 @@ func (suite *PlanTestSuite) TestDeterminePlanDeleteCommand() {
} }
func (suite *PlanTestSuite) TestDeterminePlanDeleteFromDroneEvent() { func (suite *PlanTestSuite) TestDeterminePlanDeleteFromDroneEvent() {
cfg := Config{ cfg := env.Config{
DroneEvent: "delete", DroneEvent: "delete",
} }
stepsMaker := determineSteps(cfg) stepsMaker := determineSteps(cfg)
@@ -374,7 +230,7 @@ func (suite *PlanTestSuite) TestDeterminePlanDeleteFromDroneEvent() {
} }
func (suite *PlanTestSuite) TestDeterminePlanLintCommand() { func (suite *PlanTestSuite) TestDeterminePlanLintCommand() {
cfg := Config{ cfg := env.Config{
Command: "lint", Command: "lint",
} }
@@ -383,7 +239,7 @@ func (suite *PlanTestSuite) TestDeterminePlanLintCommand() {
} }
func (suite *PlanTestSuite) TestDeterminePlanHelpCommand() { func (suite *PlanTestSuite) TestDeterminePlanHelpCommand() {
cfg := Config{ cfg := env.Config{
Command: "help", Command: "help",
} }

View File

@@ -2,50 +2,60 @@ 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 {
Repo string *config
repo string
certs *repoCerts
cmd cmd 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(_ Config) error { func (a *AddRepo) Execute() 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(cfg Config) error { func (a *AddRepo) Prepare() 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 := make([]string, 0) args := a.globalFlags()
args = append(args, "repo", "add")
if cfg.Namespace != "" { args = append(args, a.certs.flags()...)
args = append(args, "--namespace", cfg.Namespace) args = append(args, name, url)
}
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(cfg.Stdout) a.cmd.Stdout(a.stdout)
a.cmd.Stderr(cfg.Stderr) a.cmd.Stderr(a.stderr)
if cfg.Debug { if a.debug {
fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", a.cmd.String()) fmt.Fprintf(a.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,16 +38,22 @@ 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 := Config{ cfg := env.Config{
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
} }
a := AddRepo{ a := NewAddRepo(cfg, "edeath=https://github.com/n_marks/e-death")
Repo: "edeath=https://github.com/n_marks/e-death",
}
suite.mockCmd.EXPECT(). suite.mockCmd.EXPECT().
Stdout(&stdout). Stdout(&stdout).
@@ -56,7 +62,7 @@ func (suite *AddRepoTestSuite) TestPrepareAndExecute() {
Stderr(&stderr). Stderr(&stderr).
Times(1) Times(1)
suite.Require().NoError(a.Prepare(cfg)) suite.Require().NoError(a.Prepare())
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)
@@ -64,7 +70,7 @@ func (suite *AddRepoTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
suite.Require().NoError(a.Execute(cfg)) suite.Require().NoError(a.Execute())
} }
@@ -72,70 +78,35 @@ 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()
cfg := Config{} a := NewAddRepo(env.Config{}, "")
a := AddRepo{}
err := a.Prepare(cfg) err := a.Prepare()
suite.EqualError(err, "repo is required") suite.EqualError(err, "repo is required")
} }
func (suite *AddRepoTestSuite) TestPrepareMalformedRepo() { func (suite *AddRepoTestSuite) TestPrepareMalformedRepo() {
a := AddRepo{ a := NewAddRepo(env.Config{}, "dwim")
Repo: "dwim", err := a.Prepare()
}
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 := AddRepo{ a := NewAddRepo(env.Config{}, "samaritan=https://github.com/arthur_claypool/samaritan?version=2.1")
Repo: "samaritan=https://github.com/arthur_claypool/samaritan?version=2.1", suite.NoError(a.Prepare())
}
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) TestNamespaceFlag() { func (suite *AddRepoTestSuite) TestRepoAddFlags() {
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 := Config{ cfg := env.Config{}
Namespace: "alliteration", a := NewAddRepo(cfg, "machine=https://github.com/harold_finch/themachine")
}
a := AddRepo{
Repo: "edeath=https://github.com/theater_guy/e-death",
}
suite.NoError(a.Prepare(cfg)) // inject a ca cert filename so repoCerts won't create any files that we'd have to clean up
suite.Equal(suite.commandPath, helmBin) a.certs.caCertFilename = "./helm/reporepo.cert"
suite.Equal(suite.commandArgs, []string{"--namespace", "alliteration", suite.NoError(a.Prepare())
"repo", "add", "edeath", "https://github.com/theater_guy/e-death"}) suite.Equal([]string{"repo", "add", "--ca-file", "./helm/reporepo.cert",
} "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,16 +1,33 @@
package run package run
import ( import (
"github.com/pelotech/drone-helm3/internal/env"
"io" "io"
) )
// Config contains configuration applicable to all helm commands type config struct {
type Config struct { debug bool
Debug bool namespace string
Values string stdout io.Writer
StringValues string stderr io.Writer
ValuesFiles []string }
Namespace string
Stdout io.Writer func newConfig(cfg env.Config) *config {
Stderr io.Writer 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

@@ -0,0 +1,48 @@
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

@@ -2,42 +2,44 @@ 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 {
Chart string *config
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(_ Config) error { func (d *DepUpdate) Execute() 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(cfg Config) error { func (d *DepUpdate) Prepare() error {
if d.Chart == "" { if d.chart == "" {
return fmt.Errorf("chart is required") return fmt.Errorf("chart is required")
} }
args := make([]string, 0) args := d.globalFlags()
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(cfg.Stdout) d.cmd.Stdout(d.stdout)
d.cmd.Stderr(cfg.Stderr) d.cmd.Stderr(d.stderr)
if cfg.Debug { if d.debug {
fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", d.cmd.String()) fmt.Fprintf(d.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,12 +31,21 @@ 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 := Config{ cfg := env.Config{
Chart: "your_top_songs_2019",
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
} }
@@ -55,74 +64,18 @@ func (suite *DepUpdateTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
d := DepUpdate{ d := NewDepUpdate(cfg)
Chart: "your_top_songs_2019",
}
suite.Require().NoError(d.Prepare(cfg)) suite.Require().NoError(d.Prepare())
suite.NoError(d.Execute(cfg)) suite.NoError(d.Execute())
}
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 := DepUpdate{} d := NewDepUpdate(env.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()
err := d.Prepare(Config{}) err := d.Prepare()
suite.EqualError(err, "chart is required") suite.EqualError(err, "chart is required")
} }

View File

@@ -2,39 +2,47 @@ 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 {
HelmCommand string *config
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(cfg Config) error { func (h *Help) Execute() 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(cfg Config) error { func (h *Help) Prepare() error {
args := []string{"help"} args := h.globalFlags()
if cfg.Debug { args = append(args, "help")
args = append([]string{"--debug"}, args...)
}
h.cmd = command(helmBin, args...) h.cmd = command(helmBin, args...)
h.cmd.Stdout(cfg.Stdout) h.cmd.Stdout(h.stdout)
h.cmd.Stderr(cfg.Stderr) h.cmd.Stderr(h.stderr)
if cfg.Debug { if h.debug {
fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", h.cmd.String()) fmt.Fprintf(h.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,6 +17,15 @@ 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()
@@ -39,13 +48,13 @@ func (suite *HelpTestSuite) TestPrepare() {
mCmd.EXPECT(). mCmd.EXPECT().
Stderr(&stderr) Stderr(&stderr)
cfg := Config{ cfg := env.Config{
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, Stderr: &stderr,
} }
h := Help{} h := NewHelp(cfg)
err := h.Prepare(cfg) err := h.Prepare()
suite.NoError(err) suite.NoError(err)
} }
@@ -53,41 +62,15 @@ 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)
cfg := Config{} help := NewHelp(env.Config{Command: "help"})
help := Help{ help.cmd = mCmd
HelmCommand: "help", suite.NoError(help.Execute())
cmd: mCmd,
}
suite.NoError(help.Execute(cfg))
help.HelmCommand = "get down on friday" help.helmCommand = "get down on friday"
suite.EqualError(help.Execute(cfg), "unknown command 'get down on friday'") suite.EqualError(help.Execute(), "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

@@ -3,6 +3,7 @@ package run
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/pelotech/drone-helm3/internal/env"
"io" "io"
"os" "os"
"text/template" "text/template"
@@ -10,14 +11,9 @@ import (
// 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 {
SkipTLSVerify bool *config
Certificate string templateFilename string
APIServer string configFilename string
ServiceAccount string
Token string
TemplateFile string
ConfigFile string
template *template.Template template *template.Template
configFile io.WriteCloser configFile io.WriteCloser
values kubeValues values kubeValues
@@ -32,58 +28,66 @@ 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(cfg Config) error { func (i *InitKube) Execute() error {
if cfg.Debug { if i.debug {
fmt.Fprintf(cfg.Stderr, "writing kubeconfig file to %s\n", i.ConfigFile) fmt.Fprintf(i.stderr, "writing kubeconfig file to %s\n", i.configFilename)
} }
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(cfg Config) error { func (i *InitKube) Prepare() error {
var err error var err error
if i.APIServer == "" { if i.values.APIServer == "" {
return errors.New("an API Server is needed to deploy") return errors.New("an API Server is needed to deploy")
} }
if i.Token == "" { if i.values.Token == "" {
return errors.New("token is needed to deploy") return errors.New("token is needed to deploy")
} }
if i.ServiceAccount == "" { if i.values.ServiceAccount == "" {
i.ServiceAccount = "helm" i.values.ServiceAccount = "helm"
} }
if cfg.Debug { if i.debug {
fmt.Fprintf(cfg.Stderr, "loading kubeconfig template from %s\n", i.TemplateFile) fmt.Fprintf(i.stderr, "loading kubeconfig template from %s\n", i.templateFilename)
} }
i.template, err = template.ParseFiles(i.TemplateFile) i.template, err = template.ParseFiles(i.templateFilename)
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)
} }
i.values = kubeValues{ if i.debug {
SkipTLSVerify: i.SkipTLSVerify, if _, err := os.Stat(i.configFilename); err != nil {
Certificate: i.Certificate,
APIServer: i.APIServer,
ServiceAccount: i.ServiceAccount,
Token: i.Token,
Namespace: cfg.Namespace,
}
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 // non-nil err here isn't an actual error state; the kubeconfig just doesn't exist
fmt.Fprint(cfg.Stderr, "creating ") fmt.Fprint(i.stderr, "creating ")
} else { } else {
fmt.Fprint(cfg.Stderr, "truncating ") fmt.Fprint(i.stderr, "truncating ")
} }
fmt.Fprintf(cfg.Stderr, "kubeconfig file at %s\n", i.ConfigFile) fmt.Fprintf(i.stderr, "kubeconfig file at %s\n", i.configFilename)
} }
i.configFile, err = os.Create(i.ConfigFile) i.configFile, err = os.Create(i.configFilename)
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,10 +1,13 @@
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"
) )
@@ -17,6 +20,30 @@ 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 }}
@@ -29,23 +56,20 @@ namespace: {{ .Namespace }}
defer os.Remove(configFile.Name()) defer os.Remove(configFile.Name())
suite.Require().Nil(err) suite.Require().Nil(err)
init := InitKube{ cfg := env.Config{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
Token: "Aspire virtual currency", KubeToken: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
}
cfg := Config{
Namespace: "Cisco", Namespace: "Cisco",
} }
err = init.Prepare(cfg) init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
err = init.Prepare()
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(cfg) err = init.Execute()
suite.Require().Nil(err) suite.Require().Nil(err)
conf, err := ioutil.ReadFile(configFile.Name()) conf, err := ioutil.ReadFile(configFile.Name())
@@ -63,19 +87,16 @@ func (suite *InitKubeTestSuite) TestExecuteGeneratesConfig() {
defer os.Remove(configFile.Name()) defer os.Remove(configFile.Name())
suite.Require().NoError(err) suite.Require().NoError(err)
cfg := Config{ cfg := env.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",
Token: "eWVhaCB3ZSB0b2tpbic=", KubeToken: "eWVhaCB3ZSB0b2tpbic=",
Certificate: "d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE", Certificate: "d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE",
Namespace: "marshmallow",
} }
suite.Require().NoError(init.Prepare(cfg)) init := NewInitKube(cfg, "../../assets/kubeconfig.tpl", configFile.Name()) // the actual kubeconfig template
suite.Require().NoError(init.Execute(cfg)) suite.Require().NoError(init.Prepare())
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)
@@ -98,11 +119,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.SkipTLSVerify = true init.values.SkipTLSVerify = true
init.Certificate = "" init.values.Certificate = ""
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)
suite.Contains(string(contents), "insecure-skip-tls-verify: true") suite.Contains(string(contents), "insecure-skip-tls-verify: true")
@@ -116,25 +137,25 @@ func (suite *InitKubeTestSuite) TestPrepareParseError() {
defer os.Remove(templateFile.Name()) defer os.Remove(templateFile.Name())
suite.Require().Nil(err) suite.Require().Nil(err)
init := InitKube{ cfg := env.Config{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
Token: "Aspire virtual currency", KubeToken: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
} }
err = init.Prepare(Config{}) init := NewInitKube(cfg, templateFile.Name(), "")
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() {
init := InitKube{ cfg := env.Config{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
Token: "Aspire virtual currency", KubeToken: "Aspire virtual currency",
TemplateFile: "/usr/foreign/exclude/kubeprofig.tpl",
} }
err := init.Prepare(Config{}) init := NewInitKube(cfg, "/usr/foreign/exclude/kubeprofig.tpl", "")
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)
} }
@@ -143,16 +164,14 @@ 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)
init := InitKube{ cfg := env.Config{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
Token: "Aspire virtual currency", KubeToken: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: "/usr/foreign/exclude/kubeprofig",
} }
init := NewInitKube(cfg, templateFile.Name(), "/usr/foreign/exclude/kubeprofig")
cfg := Config{} err = init.Prepare()
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)
} }
@@ -167,24 +186,21 @@ 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
init := InitKube{ cfg := env.Config{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
Token: "Aspire virtual currency", KubeToken: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
} }
cfg := Config{} init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
suite.NoError(init.Prepare()) // consistency check; we should be starting in a happy state
suite.NoError(init.Prepare(cfg)) // consistency check; we should be starting in a happy state init.values.APIServer = ""
suite.Error(init.Prepare(), "APIServer should be required.")
init.APIServer = "" init.values.APIServer = "Sysadmin"
suite.Error(init.Prepare(cfg), "APIServer should be required.") init.values.Token = ""
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() {
@@ -196,18 +212,43 @@ func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
defer os.Remove(configFile.Name()) defer os.Remove(configFile.Name())
suite.Require().Nil(err) suite.Require().Nil(err)
init := InitKube{ cfg := env.Config{
APIServer: "Sysadmin", APIServer: "Sysadmin",
Certificate: "CCNA", Certificate: "CCNA",
Token: "Aspire virtual currency", KubeToken: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
} }
init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
cfg := Config{} init.Prepare()
suite.Equal("helm", init.values.ServiceAccount)
}
init.Prepare(cfg) func (suite *InitKubeTestSuite) TestDebugOutput() {
suite.Equal("helm", init.ServiceAccount) templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity")
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,54 +2,67 @@ 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 {
Chart string *config
chart string
values string
stringValues string
valuesFiles []string
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(_ Config) error { func (l *Lint) Execute() 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(cfg Config) error { func (l *Lint) Prepare() error {
if l.Chart == "" { if l.chart == "" {
return fmt.Errorf("chart is required") return fmt.Errorf("chart is required")
} }
args := make([]string, 0) args := l.globalFlags()
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 cfg.Values != "" { if l.values != "" {
args = append(args, "--set", cfg.Values) args = append(args, "--set", l.values)
} }
if cfg.StringValues != "" { if l.stringValues != "" {
args = append(args, "--set-string", cfg.StringValues) args = append(args, "--set-string", l.stringValues)
} }
for _, vFile := range cfg.ValuesFiles { for _, vFile := range l.valuesFiles {
args = append(args, "--values", vFile) args = append(args, "--values", vFile)
} }
if l.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(cfg.Stdout) l.cmd.Stdout(l.stdout)
l.cmd.Stderr(cfg.Stderr) l.cmd.Stderr(l.stderr)
if cfg.Debug { if l.debug {
fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", l.cmd.String()) fmt.Fprintf(l.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,19 +31,36 @@ 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{}
l := Lint{ cfg := env.Config{
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)
@@ -52,6 +69,7 @@ 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().
@@ -60,9 +78,9 @@ func (suite *LintTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
err := l.Prepare(cfg) err := l.Prepare()
suite.Require().Nil(err) suite.Require().Nil(err)
l.Execute(cfg) l.Execute()
} }
func (suite *LintTestSuite) TestPrepareRequiresChart() { func (suite *LintTestSuite) TestPrepareRequiresChart() {
@@ -70,25 +88,22 @@ 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()
cfg := Config{} l := NewLint(env.Config{})
l := Lint{} err := l.Prepare()
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 := Config{ cfg := env.Config{
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,
} }
l := NewLint(cfg)
l := Lint{
Chart: "./uk/top_40",
}
command = func(path string, args ...string) cmd { command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path) suite.Equal(helmBin, path)
@@ -97,6 +112,7 @@ func (suite *LintTestSuite) TestPrepareWithLintFlags() {
"--set-string", "version=2.0", "--set-string", "version=2.0",
"--values", "/usr/local/underrides", "--values", "/usr/local/underrides",
"--values", "/usr/local/overrides", "--values", "/usr/local/overrides",
"--strict",
"./uk/top_40"}, args) "./uk/top_40"}, args)
return suite.mockCmd return suite.mockCmd
@@ -104,66 +120,8 @@ 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(cfg) err := l.Prepare()
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)
}

77
internal/run/repocerts.go Normal file
View File

@@ -0,0 +1,77 @@
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

@@ -0,0 +1,80 @@
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,49 +2,57 @@ 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 {
Release string *config
DryRun bool release string
dryRun 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(_ Config) error { func (u *Uninstall) Execute() 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(cfg Config) error { func (u *Uninstall) Prepare() error {
if u.Release == "" { if u.release == "" {
return fmt.Errorf("release is required") return fmt.Errorf("release is required")
} }
args := make([]string, 0) args := u.globalFlags()
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 {
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(cfg.Stdout) u.cmd.Stdout(u.stdout)
u.cmd.Stderr(cfg.Stderr) u.cmd.Stderr(u.stderr)
if cfg.Debug { if u.debug {
fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", u.cmd.String()) fmt.Fprintf(u.stderr, "Generated command: '%s'\n", u.cmd.String())
} }
return nil return nil

View File

@@ -1,10 +1,9 @@
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"
) )
@@ -35,12 +34,26 @@ 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()
u := Uninstall{ cfg := env.Config{
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 {
@@ -58,77 +71,49 @@ func (suite *UninstallTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
cfg := Config{} suite.NoError(u.Prepare())
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(cfg) u.Execute()
} }
func (suite *UninstallTestSuite) TestPrepareDryRunFlag() { func (suite *UninstallTestSuite) TestPrepareDryRunFlag() {
u := Uninstall{ cfg := env.Config{
Release: "firefox_ak_wildfire", Release: "firefox_ak_wildfire",
DryRun: true, DryRun: true,
} }
cfg := Config{} u := NewUninstall(cfg)
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes() suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes() suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
suite.NoError(u.Prepare(cfg)) suite.NoError(u.Prepare())
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) TestPrepareNamespaceFlag() { func (suite *UninstallTestSuite) TestPrepareKeepHistoryFlag() {
u := Uninstall{ cfg := env.Config{
Release: "carly_simon_run_away_with_me", Release: "perturbator_sentient",
} KeepHistory: true,
cfg := Config{
Namespace: "emotion",
} }
u := NewUninstall(cfg)
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes() suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes() suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
suite.NoError(u.Prepare(cfg)) suite.NoError(u.Prepare())
expected := []string{"--namespace", "emotion", "uninstall", "carly_simon_run_away_with_me"} expected := []string{"uninstall", "--keep-history", "perturbator_sentient"}
suite.Equal(expected, suite.actualArgs) 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 := Uninstall{} u := NewUninstall(env.Config{})
err := u.Prepare(Config{}) err := u.Prepare()
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,83 +2,111 @@ 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 {
Chart string *config
Release string chart string
release string
ChartVersion string chartVersion string
DryRun bool dryRun bool
Wait bool wait bool
ReuseValues bool values string
Timeout string stringValues string
Force bool valuesFiles []string
reuseValues bool
timeout string
force bool
atomic bool
cleanupOnFail bool
certs *repoCerts
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,
certs: newRepoCerts(cfg),
}
}
// Execute executes the `helm upgrade` command. // Execute executes the `helm upgrade` command.
func (u *Upgrade) Execute(_ Config) error { func (u *Upgrade) Execute() 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(cfg Config) error { func (u *Upgrade) Prepare() 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 := make([]string, 0) args := u.globalFlags()
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 cfg.Values != "" { if u.atomic {
args = append(args, "--set", cfg.Values) args = append(args, "--atomic")
} }
if cfg.StringValues != "" { if u.cleanupOnFail {
args = append(args, "--set-string", cfg.StringValues) args = append(args, "--cleanup-on-fail")
} }
for _, vFile := range cfg.ValuesFiles { if u.values != "" {
args = append(args, "--set", u.values)
}
if u.stringValues != "" {
args = append(args, "--set-string", u.stringValues)
}
for _, vFile := range u.valuesFiles {
args = append(args, "--values", vFile) args = append(args, "--values", vFile)
} }
args = append(args, u.certs.flags()...)
args = append(args, u.Release, u.Chart) args = append(args, u.release, u.chart)
u.cmd = command(helmBin, args...) u.cmd = command(helmBin, args...)
u.cmd.Stdout(cfg.Stdout) u.cmd.Stdout(u.stdout)
u.cmd.Stderr(cfg.Stderr) u.cmd.Stderr(u.stderr)
if cfg.Debug { if u.debug {
fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", u.cmd.String()) fmt.Fprintf(u.stderr, "Generated command: '%s'\n", u.cmd.String())
} }
return nil return nil

View File

@@ -3,6 +3,7 @@ package run
import ( import (
"fmt" "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,13 +32,49 @@ func TestUpgradeTestSuite(t *testing.T) {
suite.Run(t, new(UpgradeTestSuite)) suite.Run(t, new(UpgradeTestSuite))
} }
func (suite *UpgradeTestSuite) TestNewUpgrade() {
cfg := env.Config{
ChartVersion: "seventeen",
DryRun: true,
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,
}
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()
u := Upgrade{ cfg := env.Config{
Chart: "at40", Chart: "at40",
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)
@@ -54,19 +91,20 @@ func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
cfg := Config{} err := u.Prepare()
err := u.Prepare(cfg)
suite.Require().Nil(err) suite.Require().Nil(err)
u.Execute(cfg) u.Execute()
} }
func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() { func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
defer suite.ctrl.Finish() defer suite.ctrl.Finish()
u := Upgrade{ cfg := env.Config{
Namespace: "melt",
Chart: "at40", Chart: "at40",
Release: "shaed_trampoline", 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)
@@ -78,32 +116,31 @@ 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())
cfg := Config{ err := u.Prepare()
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()
u := Upgrade{ cfg := env.Config{
Chart: "hot_ac", Chart: "hot_ac",
Release: "maroon_5_memories", Release: "maroon_5_memories",
ChartVersion: "radio_edit", //-version ChartVersion: "radio_edit",
DryRun: true, //-run DryRun: true,
Wait: true, //-wait Wait: true,
ReuseValues: true, //-values
Timeout: "sit_in_the_corner", //-timeout
Force: true, //-force
}
cfg := Config{
Values: "age=35", Values: "age=35",
StringValues: "height=5ft10in", StringValues: "height=5ft10in",
ValuesFiles: []string{"/usr/local/stats", "/usr/local/grades"}, ValuesFiles: []string{"/usr/local/stats", "/usr/local/grades"},
ReuseValues: true,
Timeout: "sit_in_the_corner",
Force: true,
AtomicUpgrade: true,
CleanupOnFail: true,
} }
u := NewUpgrade(cfg)
// 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)
@@ -114,10 +151,13 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
"--reuse-values", "--reuse-values",
"--timeout", "sit_in_the_corner", "--timeout", "sit_in_the_corner",
"--force", "--force",
"--atomic",
"--cleanup-on-fail",
"--set", "age=35", "--set", "age=35",
"--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",
"maroon_5_memories", "hot_ac"}, args) "maroon_5_memories", "hot_ac"}, args)
return suite.mockCmd return suite.mockCmd
@@ -126,7 +166,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(cfg) err := u.Prepare()
suite.Require().Nil(err) suite.Require().Nil(err)
} }
@@ -135,34 +175,30 @@ 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 := Upgrade{ u := NewUpgrade(env.Config{})
Release: "seth_everman_unskippable_cutscene", u.release = "seth_everman_unskippable_cutscene"
}
err := u.Prepare(Config{}) err := u.Prepare()
suite.EqualError(err, "chart is required", "Chart should be mandatory") suite.EqualError(err, "chart is required", "Chart should be mandatory")
u = Upgrade{ u.release = ""
Chart: "billboard_top_zero", u.chart = "billboard_top_zero"
}
err = u.Prepare(Config{}) err = u.Prepare()
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.Config{
Chart: "at40",
Release: "lewis_capaldi_someone_you_loved",
Debug: true, Debug: true,
Stdout: &stdout, Stdout: &stdout,
Stderr: &stderr, 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().
@@ -177,7 +213,7 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
suite.mockCmd.EXPECT(). suite.mockCmd.EXPECT().
Stderr(&stderr) Stderr(&stderr)
u.Prepare(cfg) u.Prepare()
want := fmt.Sprintf("Generated command: '%s --debug upgrade "+ want := fmt.Sprintf("Generated command: '%s --debug upgrade "+
"--install lewis_capaldi_someone_you_loved at40'\n", helmBin) "--install lewis_capaldi_someone_you_loved at40'\n", helmBin)