157 Commits

Author SHA1 Message Date
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
Joachim Hill-Grannec
353bd76f8f Merge pull request #49 from pelotech/helm-repos
Call `helm repo add` when instructed
2019-12-30 14:44:17 -07:00
Erin Call
1dba561a76 Merge branch 'master' into helm-repos 2019-12-30 13:30:07 -08:00
Erin Call
ae31cf66c3 Merge pull request #41 from pelotech/license
Use the apache 2.0 license [#23]
2019-12-30 13:29:52 -08:00
Erin Call
3985ec8faa Merge branch 'master' into helm-repos 2019-12-30 13:29:23 -08:00
Erin Call
499ab6877f Do repo error-checking in AddRepo.Prepare [#26] 2019-12-30 13:24:57 -08:00
Erin Call
2b802df00f Merge branch 'master' into license 2019-12-30 11:59:12 -08:00
Erin Call
48b6b3f5b3 Create AddRepo steps when there are repos to add [#26] 2019-12-30 11:57:19 -08:00
Joachim Hill-Grannec
29ee0c53ab Merge pull request #38 from pelotech/help-by-default
Run the help step by default
2019-12-30 11:52:17 -07:00
Erin Call
22e30fea56 The prefix setting is implemented [#19,#9]
Just something I noticed while resolving a merge conflict. The "write
some docs" and "implement prefix" branches happened concurrently and
didn't get re-coordinated.
2019-12-30 09:56:47 -08:00
Erin Call
9155d5eeeb Merge branch 'master' into helm-repos 2019-12-30 09:56:18 -08:00
Erin Call
75c99683b5 AddRepo step that calls helm repo add [#26]
As with some of the other commands, I'm not sure `--namespace` is
relevant here. Just rolling with the "at worst it doesn't hurt" theory.
2019-12-30 09:52:00 -08:00
Joachim Hill-Grannec
eb1834df49 Merge branch 'master' into help-by-default 2019-12-28 09:31:04 -07:00
Joachim Hill-Grannec
1b2af0b3ac Merge pull request #44 from pelotech/update-dependencies
Call `helm dependency update` when so instructed
2019-12-28 09:30:32 -07:00
Joachim Hill-Grannec
0f8657b1c2 Merge branch 'master' into update-dependencies 2019-12-28 09:29:53 -07:00
Joachim Hill-Grannec
edb1fb6e27 Merge pull request #47 from pelotech/timeout-formatting
Shim bare numbers into duration strings
2019-12-28 09:27:40 -07:00
Erin Call
d5a59590a1 Shim bare numbers into duration strings [#39]
Helm2's --timeout took a number of seconds, rather than the
ParseDuration-compatible string that helm3 uses. For backward-
compatibility, update a bare number into a duration string.
2019-12-27 16:20:32 -08:00
Erin Call
89ec9425b0 Mention the chart param for uninstalls [#25] 2019-12-27 15:44:09 -08:00
Erin Call
181165cc51 Call helm dependency update when so instructed [#25]
As with Lint, I have no idea whether the --namespace flag actually
matters here. I don't think it will hurt, though!
2019-12-27 15:06:32 -08:00
Erin Call
354dce2e12 Use the apache 2.0 license [#23] 2019-12-27 11:18:13 -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
Erin Call
3b78f01b45 Merge pull request #37 from pelotech/github-metafiles
Metafiles for the github repo config
2019-12-26 15:38:46 -08:00
Erin Call
24060c18fc Provide better e2e-testing instructions [#12]
The "get a kubernetes cluster" part could use some outfleshing, but this
is certainly more straightforward than it was before.
2019-12-26 15:21:34 -08:00
Erin Call
232bb5eb96 Rely on the PR template for docs/code consistency [#12]
These comments were a reasonable attempt at ensuring the documentation
matched reality, but the checkbox in the pull request template is much
more likely to produce results.
2019-12-26 13:03:53 -08:00
Erin Call
c0964ae178 Merge branch 'master' into github-metafiles 2019-12-26 13:02:02 -08:00
Erin Call
818c0246fa Merge branch 'master' into help-by-default 2019-12-26 13:00:13 -08:00
Erin Call
9f9e83da99 Merge pull request #35 from pelotech/testplan
Add omitted plan tests
2019-12-26 12:59:54 -08:00
Erin Call
39aea4c8dd Merge branch 'master' into testplan 2019-12-26 12:58:46 -08:00
Erin Call
7e25218945 Merge pull request #36 from pelotech/kubeconfig-tests
Validation tests for the kubeconfig template
2019-12-26 12:58:27 -08:00
Erin Call
3b85c38714 Test yaml validity without a new dependency [#15]
It turns out testify already depends on yaml, so we aren't adding
anything new by using it here.
2019-12-26 12:53:36 -08:00
Erin Call
1422ec77a4 Merge branch 'master' into kubeconfig-tests 2019-12-26 12:50:00 -08:00
Erin Call
2a13fff548 Don't check the generated config's yaml syntax [#13]
See discussion on https://github.com/pelotech/drone-helm3/pull/36 --it
doesn't really make sense to add a dependency on yaml just for testing.
2019-12-26 12:39:02 -08:00
Erin Call
d53a1ed942 Merge branch 'master' into testplan 2019-12-26 12:26:07 -08:00
Erin Call
167b53691b Put HelmCommand in Help, not run.Config [#15] 2019-12-26 12:23:56 -08:00
Erin Call
b1899dee56 Merge remote-tracking branch 'origin/master' into help-by-default 2019-12-26 12:23:14 -08:00
Erin Call
fc3bfc466a Merge pull request #32 from pelotech/config-fixup
Clarify and improve internal/helm.Config
2019-12-26 12:23:00 -08:00
Erin Call
8f2d4bec49 Merge branch 'master' into testplan 2019-12-26 12:08:30 -08:00
Erin Call
b2066961e1 Merge branch 'master' into kubeconfig-tests 2019-12-26 11:55:32 -08:00
Joachim Hill-Grannec
253a4465f8 Merge branch 'master' into config-fixup 2019-12-26 11:36:55 -08:00
Joachim Hill-Grannec
8857782826 Merge pull request #30 from pelotech/noncompulsory-certificate
Make the Certificate setting optional
2019-12-26 11:36:38 -08:00
Erin Call
41e9e42239 Emit a trailing newline on execution error [#15]
Just something I noticed while testing the help command's error case.
2019-12-26 11:34:07 -08:00
Erin Call
6d28b7b28a Return an error on unknown commands [#15]
I'm probably overthinking this--explicitly calling help is a strange and
unusual case--but it doesn't really hurt, so I'm going for it.
2019-12-26 11:29:33 -08:00
Joachim Hill-Grannec
81c4877a92 Merge branch 'master' into noncompulsory-certificate 2019-12-26 11:27:43 -08:00
Joachim Hill-Grannec
fbfb69f063 Merge pull request #27 from pelotech/useful-readme
Put some useful information in the README
2019-12-26 11:26:59 -08:00
Erin Call
34b9ec1c4c Run the Help step by default [#15] 2019-12-26 10:47:42 -08:00
Erin Call
568f613401 Associate lines of text with their yaml blocks [#8]
As I skimmed through that section I noticed it wasn't immediately clear
whether a line of text was referring to the example above it or the one
below it.
2019-12-26 09:44:46 -08:00
Erin Call
dc05855aa5 Mention the settings/environment equivalency [#8]
It seems like this needs more information, like why you'd want to put
something in one stanza or the other, but I don't really know enough
about drone to give useful advice.
2019-12-26 09:41:10 -08:00
Erin Call
a71bba71fd Give contributors some maybe-adequate information [#12]
I honestly have no idea how useful this document is, but it's probably
better than nothing?
2019-12-25 16:08:29 -08:00
Erin Call
f00f6a6329 Encourage good issues with issue templates [#12]
I've never used github's new multiple-option issue templates before!
I'm excited to see them in action :)
2019-12-25 12:03:36 -08:00
Erin Call
1a70a626ea Use the Contributor Covenant code of conduct [#12] 2019-12-25 11:47:41 -08:00
Erin Call
4f549ddf2a Pre-populate pull request bodies with a template [#12]
Note that as of this writing, docs/parameter_reference.md (and required-
params documentation in README.md) doesn't actually exist; I created it
in ef66bc0 but that commit hasn't been merged yet.
2019-12-25 11:21:33 -08:00
Erin Call
4cd369b051 Auto-assign rewiewers with a CODEOWNERS [#12] 2019-12-25 10:50:20 -08:00
Erin Call
6b331fdf03 Check the validity of the kubeconfig template [#13]
It's a little tricky to find a balance between "brittle" and "thorough"
in this test--I'd like to verify that e.g. the certificate is in
clusters[0].cluster.certificate-authority-data, not at the root. On the
other hand, we can't actually show that it's a valid kubeconfig file
without actually *using* it, so there's a hard upper limit on the
strength of the assertions. I've settled on verifying that all the
settings make it into the file and the file is syntactically-valid yaml.
2019-12-25 10:11:14 -08:00
Erin Call
801598e1c5 Use a clearer filepath for the kubeconfig template [#13] 2019-12-24 16:16:22 -08:00
Erin Call
cb58b5a021 Phrase errors in Execute the same as in Prepare [#33] 2019-12-24 15:49:47 -08:00
Erin Call
d86ac72529 Test Plan.Execute [#33] 2019-12-24 15:47:26 -08:00
Erin Call
52c9fb552c Ensure the plan test mocks' expectations are met [#33] 2019-12-24 15:33:50 -08:00
Erin Call
d4506608d7 Note a backwards-incompatibility in the README [#8]
This probably isn't going to bite anyone, but it's technically possible,
and it doesn't hurt to mention it.
2019-12-24 15:25:44 -08:00
Erin Call
ff8e988122 Use "installation" rather than "deployment" [#8]
"deploy" matches my mental model of what helm does, but "install"
matches helm's own terminology more closely.
2019-12-24 15:22:25 -08:00
Erin Call
ef66bc0f92 Document parameters in a single markdown file [#8]
I was unhappy with the comments-in-yaml approach; it required
duplicating a lot of information and it was hard to find a balance
between "usefully thorough" and "readably concise.""
2019-12-24 14:36:39 -08:00
Erin Call
08ddf5e27a Log debug output in helm.Config [#9]
Redacting KubeToken may not be sufficient, since it's possible that
someone would put secrets in Values or StringValues. Unilaterally
redacting those seems unhelpful, though, since they may be the very
thing the user is trying to debug. I've settled on redacting the obvious
field without trying to promise that all sensitive data will be hidden.
2019-12-24 11:08:09 -08:00
Erin Call
4ba1e694d9 Use a go-idiomatic constructor for helm.Config [#9] 2019-12-24 10:41:01 -08:00
Erin Call
10e7e7fee5 Document the Config struct's behavior correctly [#19] [#9] 2019-12-23 16:45:09 -08:00
Erin Call
285e9d98a4 Allow a configurable env var prefix [#19]
I'd like to keep Prefix's scope fairly limited, because it has potential
to spiral into something magnificently complex. You get one prefix
setting, it goes in `settings` not `environment`, end of feature.
2019-12-23 16:36:37 -08:00
Erin Call
db87bd0507 Require no-error in config tests [#9] 2019-12-23 15:52:01 -08:00
Erin Call
e2f53f3b08 Process non-prefixed forms of all config settings [#9]
Trying to guess in advance which part of the config a user will put in
the `settings` section and which they'll put in `environment` is a
fool's errand. Just let everything go in either place.

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

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

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

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @erincall @grinnellian @kav @josmo

17
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,17 @@
---
name: Bug report
about: Unexpected or broken behavior
title: ''
labels: bug
assignees: ''
---
**What I tried to do:**
<!-- e.g. run a helm installation -->
**What happened:**
<!-- describe the faulty behavior -->
**More info:**
<!-- contents of .drone.yml, etc. -->

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

@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest a new feature
title: ''
labels: enhancement
assignees: ''
---
**The problem I'm trying to solve:**
<!-- describe what you'd like to be able to accomplish -->
**How I imagine it working:**
<!-- e.g. configuration that could go in .drone.yml -->

10
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,10 @@
**Please replace this line with "fixes #ISSUE_NUMBER" (or "relates to #ISSUE_NUMBER", if it is not a complete fix)**
Pre-merge checklist:
* [ ] Code changes have tests
* [ ] Any config changes are documented:
* 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

4
.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
@@ -13,3 +15,5 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
.env
.secrets

76
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [workwithus@pelo.tech](mailto:workwithus@pelo.tech). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

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

202
LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,3 +1,95 @@
# Drone plugin for Helm 3 # Drone plugin for Helm 3
Dissatisfied with this empty README? Consider grabbing [the "put stuff in the README" issue](https://github.com/pelotech/drone-helm3/issues/8)! [![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):
* Lint your charts
* Deploy your service
* Delete your service
The plugin is inpsired by [drone-helm](https://github.com/ipedrazas/drone-helm), which fills the same role for Helm 2. It provides a comparable feature-set and the configuration settings are backward-compatible.
## Example configuration
The examples below give a minimal and sufficient configuration for each use-case. For a full description of each command's settings, see [docs/parameter_reference.md](docs/parameter_reference.md).
### Linting
```yaml
steps:
- name: lint
image: pelotech/drone-helm3
settings:
mode: lint
chart: ./
```
### Installation
```yaml
steps:
- name: deploy
image: pelotech/drone-helm3
settings:
mode: upgrade
chart: ./
release: my-project
environment:
KUBE_API_SERVER: https://my.kubernetes.installation/clusters/a-1234
KUBE_TOKEN:
from_secret: kubernetes_token
```
### Uninstallation
```yaml
steps:
- name: uninstall
image: pelotech/drone-helm3
settings:
mode: uninstall
release: my-project
environment:
KUBE_API_SERVER: https://my.kubernetes.installation/clusters/a-1234
KUBE_TOKEN:
from_secret: kubernetes_token
```
## Upgrading from drone-helm
drone-helm3 is largely backward-compatible with drone-helm. There are some known differences:
* 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/).
* 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
* `recreate_pods`
* `tiller_ns`
* `upgrade`
* `canary_image`
* `client_only`
* `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.
### [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

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

@@ -2,22 +2,23 @@ package main
import ( import (
"fmt" "fmt"
"github.com/kelseyhightower/envconfig"
"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() {
var c helm.Config cfg, err := env.NewConfig(os.Stdout, os.Stderr)
if err := envconfig.Process("plugin", &c); err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error()) fmt.Fprintf(os.Stderr, "%s\n", err.Error())
return return
} }
// Make the plan // Make the plan
plan, err := helm.NewPlan(c) plan, err := helm.NewPlan(*cfg)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%w\n", err) fmt.Fprintf(os.Stderr, "%w\n", err)
os.Exit(1) os.Exit(1)
@@ -28,7 +29,7 @@ func main() {
// Expect the plan to go off the rails // Expect the plan to go off the rails
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, err.Error()) fmt.Fprintf(os.Stderr, "%s\n", err.Error())
// Throw away the plan // Throw away the plan
os.Exit(1) os.Exit(1)
} }

48
docs/contributing.md Normal file
View File

@@ -0,0 +1,48 @@
# Contributing to drone-helm3
We're glad you're interested in contributing! Here are some guidelines that will help make sure everyone has a good experience:
## Submitting a patch
Before you start working on a change, please make sure there's an associated issue. It doesn't need to be thoroughly scrutinized and dissected, but it needs to exist.
Please put the relevant issue number in the first line of your commit messages, e.g. `vorpalize the frabjulator [#42]`. Branch names do not need issue numbers, but feel free to include them if you like.
We encourage you to follow [the guidelines in Pro Git](https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project#_commit_guidelines) when making commits. In short:
* Commit early and commit often.
* Make the first line of the commit message concise--no more than 50 characters or so.
* Make the rest of the commit message verbose--information about _why_ you did what you did is particularly helpful.
Once you're satisfied with your work, send us a pull request. If you'd like, you can send the pull request _before_ you're satisfied with your work; just be sure to mark the PR a draft or put `[WIP]` in the title.
## How to run the tests
We use `go test`, `go vet`, and `golint`:
```
go test ./cmd/... ./internal/...
go vet ./cmd/... ./internal/...
golint -set_exit_status ./cmd/... ./internal/...
```
If you have [the Drone cli tool](https://docs.drone.io/cli/install/) installed, you can also use `drone exec --include test --include lint`.
## Testing the plugin end-to-end
Although we aim to make the internal tests as thorough as possible, they can't test drone-helm3's integration with drone and helm themselves. However, you can test a change manually by building an image and running it with a fixture repository.
You will need:
* Access to a docker image registry. This document assumes you'll use [Docker Hub](https://hub.docker.com).
* [The Drone cli tool](https://docs.drone.io/cli/install/).
* A fixture repository--a directory with a `.drone.yml` and a helm chart. If you don't have one handy, try adding a `.drone.yml` to a chart from [Helm's "stable" repository](https://github.com/helm/charts/tree/master/stable/).
* Access to a kubernetes cluster (unless `lint` or `dry_run` is sufficient for your purposes).
Once you have what you need, you can publish and consume an image with your changes:
1. [Create a repository on Docker Hub](https://hub.docker.com/repository/create). This document assumes you've called it drone-helm3-testing.
1. Create a `.secrets` file with your docker credentials (see [example.secrets](./example.secrets) for an example). While you can use your Docker Hub password, it's better to [generate an access token](https://hub.docker.com/settings/security) and use that instead.
1. Use Drone to build and publish an image with your changes: `drone exec --secret-file ./secrets --event push`
1. In the `.drone.yml` of your fixture repository, set the `image` for each relevant stanza to `your_dockerhub_username/drone-helm3-testing`
1. Use `drone exec` in the fixture repo to verify your changes.

3
docs/example.secrets Normal file
View File

@@ -0,0 +1,3 @@
DOCKER_PASSWORD=your_access_token
DOCKER_USERNAME=your_dockerhub_username
PLUGIN_REPO=your_dockerhub_username/drone-helm3-testing

110
docs/parameter_reference.md Normal file
View File

@@ -0,0 +1,110 @@
# Parameter reference
## Global
| Param name | Type | Alias | Purpose |
|---------------------|-----------------|--------------|---------|
| 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.|
| 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/`. |
| repo_ca_file | string | | TLS certificate for a chart repository certificate authority. |
| 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 is only triggered when the `mode` setting is "lint".
| Param name | Type | Required | Purpose |
|---------------|----------------|----------|---------|
| chart | string | yes | The chart to be linted. Must be a local path. |
| values | list\<string\> | | Chart values to use as the `--set` argument to `helm lint`. |
| string_values | list\<string\> | | Chart values to use as the `--set-string` argument to `helm lint`. |
| values_files | list\<string\> | | Values to use as `--values` arguments to `helm lint`. |
| lint_strictly | boolean | | Pass `--strict` to `helm lint`, to turn warnings into errors. |
## Installation
Installations are triggered when the `mode` setting is "upgrade." They can also be triggered when the build was triggered by a `push`, `tag`, `deployment`, `pull_request`, `promote`, or `rollback` Drone event.
| Param name | Type | Required | Alias | Purpose |
|------------------------|----------------|----------|------------------------|---------|
| chart | string | yes | | The chart to use for this installation. |
| release | string | yes | | The release name for helm to use. |
| kube_api_server | string | yes | api_server | API endpoint for the Kubernetes cluster. |
| kube_token | string | yes | kubernetes_token | Token for authenticating to Kubernetes. |
| kube_service_account | string | | service_account | Service account for authenticating to Kubernetes. Default is `helm`. |
| 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. |
| dry_run | boolean | | | Pass `--dry-run` to `helm upgrade`. |
| 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. |
| force_upgrade | boolean | | force | Pass `--force` to `helm upgrade`. |
| atomic_upgrade | boolean | | | Pass `--atomic` to `helm upgrade`. |
| cleanup_failed_upgrade | boolean | | | Pass `--cleanup-on-fail` to `helm upgrade`. |
| values | list\<string\> | | | Chart values to use as the `--set` argument to `helm upgrade`. |
| string_values | list\<string\> | | | Chart values to use as the `--set-string` argument to `helm upgrade`. |
| values_files | list\<string\> | | | Values to use as `--values` arguments to `helm upgrade`. |
| reuse_values | boolean | | | Reuse the values from a previous release. |
| skip_tls_verify | boolean | | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. |
## Uninstallation
Uninstallations are triggered when the `mode` setting is "uninstall" or "delete." They can also be triggered when the build was triggered by a `delete` Drone event.
| Param name | Type | Required | Alias | Purpose |
|------------------------|----------|----------|------------------------|---------|
| release | string | yes | | The release name for helm to use. |
| kube_api_server | string | yes | api_server | API endpoint for the Kubernetes cluster. |
| kube_token | string | yes | kubernetes_token | Token for authenticating to Kubernetes. |
| kube_service_account | string | | service_account | Service account for authenticating to Kubernetes. Default is `helm`. |
| kube_certificate | string | | kubernetes_certificate | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. |
| keep_history | boolean | | | Pass `--keep-history` to `helm uninstall`, to retain the release history. |
| dry_run | boolean | | | Pass `--dry-run` to `helm uninstall`. |
| timeout | duration | | | Timeout for any *individual* Kubernetes operation. The uninstallation's full runtime may exceed this duration. |
| skip_tls_verify | boolean | | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. |
| chart | string | | | Required when the global `update_dependencies` parameter is true. No effect otherwise. |
### Where to put settings
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
* Booleans can be yaml's `true` and `false` literals or the strings `"true"` and `"false"`.
* Durations are strings formatted with the syntax accepted by [golang's ParseDuration function](https://golang.org/pkg/time/#ParseDuration) (e.g. 5m30s)
* For backward-compatibility with drone-helm, a duration can also be an integer, in which case it will be interpreted to mean seconds.
* List\<string\>s can be a yaml sequence or a comma-separated string.
All of the following are equivalent:
```yaml
values: "foo=1,bar=2"
values: ["foo=1", "bar=2"]
values:
- foo=1
- bar=2
```
Note that **list members must not contain commas**. Both of the following are equivalent:
```yaml
values_files: [ "./over_9,000.yml" ]
values_files: [ "./over_9", "000.yml" ]
```
### Backward-compatibility aliases
Some settings have alternate names, for backward-compatibility with drone-helm. We recommend using the canonical name unless you require the backward-compatible form.
| Canonical name | Alias |
|----------------------|-------|
| mode | helm_command |
| add_repos | helm_repos |
| kube_api_server | api_server |
| kube_service_account | service_account |
| kube_token | kubernetes_token |
| kube_certificate | kubernetes_certificate |
| wait_for_upgrade | wait |
| force_upgrade | force |

2
go.mod
View File

@@ -4,8 +4,10 @@ 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
golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 // indirect golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 // indirect
gopkg.in/yaml.v2 v2.2.2
) )

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=

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

@@ -0,0 +1,126 @@
package env
import (
"fmt"
"github.com/kelseyhightower/envconfig"
"io"
"os"
"regexp"
"strings"
)
var (
justNumbers = regexp.MustCompile(`^\d+$`)
deprecatedVars = []string{"PURGE", "RECREATE_PODS", "TILLER_NS", "UPGRADE", "CANARY_IMAGE", "CLIENT_ONLY", "STABLE_REPO_URL"}
)
// The Config struct captures the `settings` and `environment` blocks in the application's drone
// config. Configuration in drone's `settings` block arrives as uppercase env vars matching the
// config key, prefixed with `PLUGIN_`. Config from the `environment` block is uppercased, but does
// not have the `PLUGIN_` prefix.
type Config struct {
// Configuration for drone-helm itself
Command string `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
RepoCAFile string `envconfig:"repo_ca_file"` // CA certificate for `helm repo add`
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)
}
if cfg.Debug && cfg.Stderr != nil {
cfg.logDebug()
}
cfg.deprecationWarn()
return &cfg, nil
}
func (cfg Config) logDebug() {
if cfg.KubeToken != "" {
cfg.KubeToken = "(redacted)"
}
fmt.Fprintf(cfg.Stderr, "Generated config: %+v\n", cfg)
}
func (cfg *Config) deprecationWarn() {
for _, varname := range deprecatedVars {
_, barePresent := os.LookupEnv(varname)
_, prefixedPresent := os.LookupEnv("PLUGIN_" + varname)
if barePresent || prefixedPresent {
fmt.Fprintf(cfg.Stderr, "Warning: ignoring deprecated '%s' setting\n", strings.ToLower(varname))
}
}
}
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"`
}

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

@@ -0,0 +1,218 @@
package env
import (
"fmt"
"github.com/stretchr/testify/suite"
"os"
"strings"
"testing"
)
type ConfigTestSuite struct {
suite.Suite
// These tests need to mutate the environment, so the suite.setenv and .unsetenv functions store the original contents of the
// relevant variable in this map. Its use of *string is so they can distinguish between "not set" and "set to empty string"
envBackup map[string]*string
}
func TestConfigTestSuite(t *testing.T) {
suite.Run(t, new(ConfigTestSuite))
}
func (suite *ConfigTestSuite) TestNewConfigWithPluginPrefix() {
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) 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,63 +0,0 @@
package helm
import (
"fmt"
"strings"
)
// The Config struct captures the `settings` and `environment` blocks inthe application's drone
// config. Configuration in drone's `settings` block arrives as uppercase env vars matching the
// config key, prefixed with `PLUGIN_`. Config from the `environment` block is *not* prefixed; any
// keys that are likely to be in that block (i.e. things that use `from_secret` need an explicit
// `envconfig:` tag so that envconfig will look for a non-prefixed env var.
type Config struct {
// Configuration for drone-helm itself
Command helmCommand `envconfig:"HELM_COMMAND"` // Helm command to run
DroneEvent string `envconfig:"DRONE_BUILD_EVENT"` // Drone event that invoked this plugin.
UpdateDependencies bool `split_words:"true"` // call `helm dependency update` before the main command
Repos []string `envconfig:"HELM_REPOS"` // call `helm repo add` before the main command
Prefix string `` // Prefix to use when looking up secret env vars
// Global helm config
Debug bool `` // global helm flag (also applies to drone-helm itself)
KubeConfig string `split_words:"true" default:"/root/.kube/config"` // path to the kube config file
Values string ``
StringValues string `split_words:"true"`
ValuesFiles []string `split_words:"true"`
Namespace string ``
KubeToken string `envconfig:"KUBERNETES_TOKEN"`
SkipTLSVerify bool `envconfig:"SKIP_TLS_VERIFY"`
Certificate string `envconfig:"KUBERNETES_CERTIFICATE"`
APIServer string `envconfig:"API_SERVER"`
ServiceAccount string `envconfig:"SERVICE_ACCOUNT"` // Can't just use split_words; need envconfig to find the non-prefixed form
// Config specifically for `helm upgrade`
ChartVersion string `split_words:"true"` //
DryRun bool `split_words:"true"` // also available for `delete`
Wait bool `` //
ReuseValues bool `split_words:"true"` //
Timeout string `` //
Chart string `` // Also available for `lint`, in which case it must be a path to a chart directory
Release string ``
Force bool `` //
}
type helmCommand string
// helmCommand.Decode checks the given value against the list of known commands and generates a helpful error if the command is unknown.
func (cmd *helmCommand) Decode(value string) error {
known := []string{"upgrade", "delete", "lint", "help"}
for _, c := range known {
if value == c {
*cmd = helmCommand(value)
return nil
}
}
if value == "" {
return nil
}
known[len(known)-1] = fmt.Sprintf("or %s", known[len(known)-1])
return fmt.Errorf("unknown command '%s'. If specified, command must be %s",
value, strings.Join(known, ", "))
}

View File

@@ -1,28 +0,0 @@
package helm
import (
"github.com/stretchr/testify/suite"
"testing"
)
type ConfigTestSuite struct {
suite.Suite
}
func TestConfigTestSuite(t *testing.T) {
suite.Run(t, new(ConfigTestSuite))
}
func (suite *ConfigTestSuite) TestHelmCommandDecodeSuccess() {
cmd := helmCommand("")
err := cmd.Decode("upgrade")
suite.Require().Nil(err)
suite.EqualValues(cmd, "upgrade")
}
func (suite *ConfigTestSuite) TestHelmCommandDecodeFailure() {
cmd := helmCommand("")
err := cmd.Decode("execute order 66")
suite.EqualError(err, "unknown command 'execute order 66'. If specified, command must be upgrade, delete, lint, or help")
}

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,39 +2,32 @@ 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"
) )
const kubeConfigTemplate = "/root/.kube/config.tpl" const (
kubeConfigTemplate = "/root/.kube/config.tpl"
kubeConfigFile = "/root/.kube/config"
)
// A Step is one step in the plan. // A Step is one step in the plan.
type Step interface { type Step interface {
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,
KubeConfig: cfg.KubeConfig,
Values: cfg.Values,
StringValues: cfg.StringValues,
ValuesFiles: cfg.ValuesFiles,
Namespace: cfg.Namespace,
Stdout: os.Stdout,
Stderr: os.Stderr,
},
} }
p.steps = (*determineSteps(cfg))(cfg) p.steps = (*determineSteps(cfg))(cfg)
@@ -44,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
} }
@@ -55,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
@@ -72,7 +65,7 @@ func determineSteps(cfg Config) *func(Config) []Step {
case "delete": case "delete":
return &uninstall return &uninstall
default: default:
panic("not implemented") return &help
} }
} }
} }
@@ -81,66 +74,54 @@ func determineSteps(cfg Config) *func(Config) []Step {
func (p *Plan) Execute() error { func (p *Plan) Execute() error {
for i, step := range p.steps { for i, step := range p.steps {
if p.cfg.Debug { if p.cfg.Debug {
fmt.Fprintf(os.Stderr, "calling %T.Execute (step %d)\n", step, i) fmt.Fprintf(p.cfg.Stderr, "calling %T.Execute (step %d)\n", step, i)
} }
if err := step.Execute(p.runCfg); err != nil { if err := step.Execute(); err != nil {
return fmt.Errorf("in execution step %d: %w", i, err) return fmt.Errorf("while executing %T step: %w", step, err)
} }
} }
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, run.NewInitKube(cfg, kubeConfigTemplate, kubeConfigFile))
steps = append(steps, &run.Upgrade{ for _, repo := range cfg.AddRepos {
Chart: cfg.Chart, steps = append(steps, run.NewAddRepo(cfg, repo))
Release: cfg.Release, }
ChartVersion: cfg.ChartVersion, if cfg.UpdateDependencies {
DryRun: cfg.DryRun, steps = append(steps, run.NewDepUpdate(cfg))
Wait: cfg.Wait, }
ReuseValues: cfg.ReuseValues, steps = append(steps, run.NewUpgrade(cfg))
Timeout: cfg.Timeout,
Force: cfg.Force,
})
return steps return steps
} }
var uninstall = func(cfg Config) []Step { var uninstall = func(cfg env.Config) []Step {
steps := initKube(cfg) var steps []Step
steps = append(steps, &run.Uninstall{ steps = append(steps, run.NewInitKube(cfg, kubeConfigTemplate, kubeConfigFile))
Release: cfg.Release, if cfg.UpdateDependencies {
DryRun: cfg.DryRun, steps = append(steps, run.NewDepUpdate(cfg))
}) }
steps = append(steps, run.NewUninstall(cfg))
return steps return steps
} }
var lint = func(cfg Config) []Step { var lint = func(cfg env.Config) []Step {
lint := &run.Lint{ var steps []Step
Chart: cfg.Chart, 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
} }
return []Step{lint} var help = func(cfg env.Config) []Step {
} return []Step{run.NewHelp(cfg)}
var help = func(cfg Config) []Step {
help := &run.Help{}
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,
},
}
} }

View File

@@ -4,9 +4,10 @@ import (
"fmt" "fmt"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"os" "strings"
"testing" "testing"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/pelotech/drone-helm3/internal/run" "github.com/pelotech/drone-helm3/internal/run"
) )
@@ -20,64 +21,54 @@ func TestPlanTestSuite(t *testing.T) {
func (suite *PlanTestSuite) TestNewPlan() { func (suite *PlanTestSuite) TestNewPlan() {
ctrl := gomock.NewController(suite.T()) ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
stepOne := NewMockStep(ctrl) stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl) stepTwo := NewMockStep(ctrl)
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{ stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := env.Config{
Command: "help", Command: "help",
Debug: false, Debug: false,
KubeConfig: "/branch/.sfere/profig",
Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
Namespace: "outer", Namespace: "outer",
} Stdout: &stdout,
Stderr: &stderr,
runCfg := run.Config{
Debug: false,
KubeConfig: "/branch/.sfere/profig",
Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
Namespace: "outer",
Stdout: os.Stdout,
Stderr: os.Stderr,
} }
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() {
ctrl := gomock.NewController(suite.T()) ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
stepOne := NewMockStep(ctrl) stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl) stepTwo := NewMockStep(ctrl)
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)
@@ -85,117 +76,117 @@ func (suite *PlanTestSuite) TestNewPlanAbortsOnError() {
suite.EqualError(err, "while preparing *helm.MockStep step: I'm starry Dave, aye, cat blew that") suite.EqualError(err, "while preparing *helm.MockStep step: I'm starry Dave, aye, cat blew that")
} }
func (suite *PlanTestSuite) TestExecute() {
ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl)
plan := Plan{
steps: []Step{stepOne, stepTwo},
}
stepOne.EXPECT().
Execute().
Times(1)
stepTwo.EXPECT().
Execute().
Times(1)
suite.NoError(plan.Execute())
}
func (suite *PlanTestSuite) TestExecuteAbortsOnError() {
ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
stepOne := NewMockStep(ctrl)
stepTwo := NewMockStep(ctrl)
plan := Plan{
steps: []Step{stepOne, stepTwo},
}
stepOne.EXPECT().
Execute().
Times(1).
Return(fmt.Errorf("oh, he'll gnaw"))
err := plan.Execute()
suite.EqualError(err, "while executing *helm.MockStep step: oh, he'll gnaw")
}
func (suite *PlanTestSuite) TestUpgrade() { func (suite *PlanTestSuite) TestUpgrade() {
cfg := Config{ 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() {
cfg := env.Config{
UpdateDependencies: true,
}
steps := upgrade(cfg)
suite.Require().Equal(3, len(steps), "upgrade should have a third step when DepUpdate is true")
suite.IsType(&run.InitKube{}, steps[0])
suite.IsType(&run.DepUpdate{}, steps[1])
} }
func (suite *PlanTestSuite) TestDel() { func (suite *PlanTestSuite) TestUpgradeWithAddRepos() {
cfg := Config{ cfg := env.Config{
KubeToken: "b2YgbXkgYWZmZWN0aW9u", AddRepos: []string{
SkipTLSVerify: true, "machine=https://github.com/harold_finch/themachine",
Certificate: "cHJvY2xhaW1zIHdvbmRlcmZ1bCBmcmllbmRzaGlw", },
APIServer: "98.765.43.21", }
ServiceAccount: "greathelm", steps := upgrade(cfg)
DryRun: true, suite.Require().True(len(steps) > 1, "upgrade should generate at least two steps")
Timeout: "think about what you did", suite.IsType(&run.AddRepo{}, steps[1])
Release: "jetta_id_love_to_change_the_world",
} }
steps := uninstall(cfg) func (suite *PlanTestSuite) TestUninstall() {
steps := uninstall(env.Config{})
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,
} }
suite.Equal(expected, init) func (suite *PlanTestSuite) TestUninstallWithUpdateDependencies() {
cfg := env.Config{
suite.Require().IsType(&run.Uninstall{}, steps[1]) UpdateDependencies: true,
actual, _ := steps[1].(*run.Uninstall)
expected = &run.Uninstall{
Release: "jetta_id_love_to_change_the_world",
DryRun: true,
} }
suite.Equal(expected, actual) steps := uninstall(cfg)
} suite.Require().Equal(3, len(steps), "uninstall should have a third step when DepUpdate is true")
suite.IsType(&run.InitKube{}, steps[0])
func (suite *PlanTestSuite) TestInitKube() { suite.IsType(&run.DepUpdate{}, steps[1])
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,
}
suite.Equal(expected, init)
} }
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])
} }
func (suite *PlanTestSuite) TestLintWithUpdateDependencies() {
cfg := env.Config{
UpdateDependencies: true,
}
steps := lint(cfg) steps := lint(cfg)
suite.Equal(1, len(steps)) suite.Require().Equal(2, len(steps), "lint should have a second step when DepUpdate is true")
suite.IsType(&run.DepUpdate{}, steps[0])
want := &run.Lint{
Chart: "./flow",
} }
suite.Equal(want, steps[0])
func (suite *PlanTestSuite) TestLintWithAddRepos() {
cfg := env.Config{
AddRepos: []string{"friendczar=https://github.com/logan_pierce/friendczar"},
}
steps := lint(cfg)
suite.Require().True(len(steps) > 0, "lint should return at least one step")
suite.IsType(&run.AddRepo{}, steps[0])
} }
func (suite *PlanTestSuite) TestDeterminePlanUpgradeCommand() { func (suite *PlanTestSuite) TestDeterminePlanUpgradeCommand() {
cfg := Config{ cfg := env.Config{
Command: "upgrade", Command: "upgrade",
} }
stepsMaker := determineSteps(cfg) stepsMaker := determineSteps(cfg)
@@ -203,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 {
@@ -214,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)
@@ -231,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)
@@ -239,7 +230,7 @@ func (suite *PlanTestSuite) TestDeterminePlanDeleteFromDroneEvent() {
} }
func (suite *PlanTestSuite) TestDeterminePlanLintCommand() { func (suite *PlanTestSuite) TestDeterminePlanLintCommand() {
cfg := Config{ cfg := env.Config{
Command: "lint", Command: "lint",
} }
@@ -248,7 +239,7 @@ func (suite *PlanTestSuite) TestDeterminePlanLintCommand() {
} }
func (suite *PlanTestSuite) TestDeterminePlanHelpCommand() { func (suite *PlanTestSuite) TestDeterminePlanHelpCommand() {
cfg := Config{ cfg := env.Config{
Command: "help", Command: "help",
} }

60
internal/run/addrepo.go Normal file
View File

@@ -0,0 +1,60 @@
package run
import (
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
"strings"
)
// AddRepo is an execution step that calls `helm repo add` when executed.
type AddRepo struct {
*config
repo string
caFile string
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,
caFile: cfg.RepoCAFile,
}
}
// Execute executes the `helm repo add` command.
func (a *AddRepo) Execute() error {
return a.cmd.Run()
}
// Prepare gets the AddRepo ready to execute.
func (a *AddRepo) Prepare() error {
if a.repo == "" {
return fmt.Errorf("repo is required")
}
split := strings.SplitN(a.repo, "=", 2)
if len(split) != 2 {
return fmt.Errorf("bad repo spec '%s'", a.repo)
}
name := split[0]
url := split[1]
args := a.globalFlags()
args = append(args, "repo", "add")
if a.caFile != "" {
args = append(args, "--ca-file", a.caFile)
}
args = append(args, name, url)
a.cmd = command(helmBin, args...)
a.cmd.Stdout(a.stdout)
a.cmd.Stderr(a.stderr)
if a.debug {
fmt.Fprintf(a.stderr, "Generated command: '%s'\n", a.cmd.String())
}
return nil
}

View File

@@ -0,0 +1,110 @@
package run
import (
"github.com/golang/mock/gomock"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite"
"strings"
"testing"
)
type AddRepoTestSuite struct {
suite.Suite
ctrl *gomock.Controller
mockCmd *Mockcmd
originalCommand func(string, ...string) cmd
commandPath string
commandArgs []string
}
func (suite *AddRepoTestSuite) BeforeTest(_, _ string) {
suite.ctrl = gomock.NewController(suite.T())
suite.mockCmd = NewMockcmd(suite.ctrl)
suite.originalCommand = command
command = func(path string, args ...string) cmd {
suite.commandPath = path
suite.commandArgs = args
return suite.mockCmd
}
}
func (suite *AddRepoTestSuite) AfterTest(_, _ string) {
suite.ctrl.Finish()
command = suite.originalCommand
}
func TestAddRepoTestSuite(t *testing.T) {
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)
}
func (suite *AddRepoTestSuite) TestPrepareAndExecute() {
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := env.Config{
Stdout: &stdout,
Stderr: &stderr,
}
a := NewAddRepo(cfg, "edeath=https://github.com/n_marks/e-death")
suite.mockCmd.EXPECT().
Stdout(&stdout).
Times(1)
suite.mockCmd.EXPECT().
Stderr(&stderr).
Times(1)
suite.Require().NoError(a.Prepare())
suite.Equal(helmBin, suite.commandPath)
suite.Equal([]string{"repo", "add", "edeath", "https://github.com/n_marks/e-death"}, suite.commandArgs)
suite.mockCmd.EXPECT().
Run().
Times(1)
suite.Require().NoError(a.Execute())
}
func (suite *AddRepoTestSuite) TestPrepareRepoIsRequired() {
// These aren't really expected, but allowing them gives clearer test-failure messages
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
a := NewAddRepo(env.Config{}, "")
err := a.Prepare()
suite.EqualError(err, "repo is required")
}
func (suite *AddRepoTestSuite) TestPrepareMalformedRepo() {
a := NewAddRepo(env.Config{}, "dwim")
err := a.Prepare()
suite.EqualError(err, "bad repo spec 'dwim'")
}
func (suite *AddRepoTestSuite) TestPrepareWithEqualSignInURL() {
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
a := NewAddRepo(env.Config{}, "samaritan=https://github.com/arthur_claypool/samaritan?version=2.1")
suite.NoError(a.Prepare())
suite.Contains(suite.commandArgs, "https://github.com/arthur_claypool/samaritan?version=2.1")
}
func (suite *AddRepoTestSuite) TestRepoAddFlags() {
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
cfg := env.Config{
RepoCAFile: "./helm/reporepo.cert",
}
a := NewAddRepo(cfg, "machine=https://github.com/harold_finch/themachine")
suite.NoError(a.Prepare())
suite.Equal([]string{"repo", "add", "--ca-file", "./helm/reporepo.cert",
"machine", "https://github.com/harold_finch/themachine"}, suite.commandArgs)
}

View File

@@ -1,17 +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
KubeConfig string stdout io.Writer
Values string stderr io.Writer
StringValues string }
ValuesFiles []string
Namespace string func newConfig(cfg env.Config) *config {
Stdout io.Writer return &config{
Stderr io.Writer 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)
}

46
internal/run/depupdate.go Normal file
View File

@@ -0,0 +1,46 @@
package run
import (
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
)
// DepUpdate is an execution step that calls `helm dependency update` when executed.
type DepUpdate struct {
*config
chart string
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.
func (d *DepUpdate) Execute() error {
return d.cmd.Run()
}
// Prepare gets the DepUpdate ready to execute.
func (d *DepUpdate) Prepare() error {
if d.chart == "" {
return fmt.Errorf("chart is required")
}
args := d.globalFlags()
args = append(args, "dependency", "update", d.chart)
d.cmd = command(helmBin, args...)
d.cmd.Stdout(d.stdout)
d.cmd.Stderr(d.stderr)
if d.debug {
fmt.Fprintf(d.stderr, "Generated command: '%s'\n", d.cmd.String())
}
return nil
}

View File

@@ -0,0 +1,81 @@
package run
import (
"github.com/golang/mock/gomock"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite"
"strings"
"testing"
)
type DepUpdateTestSuite struct {
suite.Suite
ctrl *gomock.Controller
mockCmd *Mockcmd
originalCommand func(string, ...string) cmd
}
func (suite *DepUpdateTestSuite) BeforeTest(_, _ string) {
suite.ctrl = gomock.NewController(suite.T())
suite.mockCmd = NewMockcmd(suite.ctrl)
suite.originalCommand = command
command = func(path string, args ...string) cmd { return suite.mockCmd }
}
func (suite *DepUpdateTestSuite) AfterTest(_, _ string) {
command = suite.originalCommand
}
func TestDepUpdateTestSuite(t *testing.T) {
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() {
defer suite.ctrl.Finish()
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := env.Config{
Chart: "your_top_songs_2019",
Stdout: &stdout,
Stderr: &stderr,
}
command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path)
suite.Equal([]string{"dependency", "update", "your_top_songs_2019"}, args)
return suite.mockCmd
}
suite.mockCmd.EXPECT().
Stdout(&stdout)
suite.mockCmd.EXPECT().
Stderr(&stderr)
suite.mockCmd.EXPECT().
Run().
Times(1)
d := NewDepUpdate(cfg)
suite.Require().NoError(d.Prepare())
suite.NoError(d.Execute())
}
func (suite *DepUpdateTestSuite) TestPrepareChartRequired() {
d := NewDepUpdate(env.Config{})
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
err := d.Prepare()
suite.EqualError(err, "chart is required")
}

View File

@@ -2,31 +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 {
*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(_ Config) error { func (h *Help) Execute() error {
return h.cmd.Run() if err := h.cmd.Run(); err != nil {
return fmt.Errorf("while running '%s': %w", h.cmd.String(), err)
}
if h.helmCommand == "help" {
return nil
}
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()
@@ -38,35 +47,30 @@ func (suite *HelpTestSuite) TestPrepare() {
Stdout(&stdout) Stdout(&stdout)
mCmd.EXPECT(). mCmd.EXPECT().
Stderr(&stderr) Stderr(&stderr)
cfg := env.Config{
Stdout: &stdout,
Stderr: &stderr,
}
h := NewHelp(cfg)
err := h.Prepare()
suite.NoError(err)
}
func (suite *HelpTestSuite) TestExecute() {
ctrl := gomock.NewController(suite.T())
defer ctrl.Finish()
mCmd := NewMockcmd(ctrl)
mCmd.EXPECT(). mCmd.EXPECT().
Run(). Run().
Times(1) Times(2)
cfg := Config{ help := NewHelp(env.Config{Command: "help"})
Stdout: &stdout, help.cmd = mCmd
Stderr: &stderr, suite.NoError(help.Execute())
}
help.helmCommand = "get down on friday"
h := Help{} suite.EqualError(help.Execute(), "unknown command 'get down on friday'")
err := h.Prepare(cfg)
suite.Require().Nil(err)
h.Execute(cfg)
}
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,13 +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
template *template.Template template *template.Template
configFile io.WriteCloser configFile io.WriteCloser
values kubeValues values kubeValues
@@ -31,61 +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", cfg.KubeConfig) 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.Certificate == "" && !i.SkipTLSVerify {
return errors.New("certificate is needed to deploy") if i.values.ServiceAccount == "" {
i.values.ServiceAccount = "helm"
} }
if i.ServiceAccount == "" { if i.debug {
i.ServiceAccount = "helm" fmt.Fprintf(i.stderr, "loading kubeconfig template from %s\n", i.templateFilename)
} }
i.template, err = template.ParseFiles(i.templateFilename)
if cfg.Debug {
fmt.Fprintf(cfg.Stderr, "loading kubeconfig template from %s\n", i.TemplateFile)
}
i.template, err = template.ParseFiles(i.TemplateFile)
if err != nil { if err != nil {
return fmt.Errorf("could not load kubeconfig template: %w", err) return fmt.Errorf("could not load kubeconfig template: %w", err)
} }
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(cfg.KubeConfig); 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", cfg.KubeConfig) fmt.Fprintf(i.stderr, "kubeconfig file at %s\n", i.configFilename)
} }
i.configFile, err = os.Create(cfg.KubeConfig) 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,12 +1,15 @@
package run package run
import ( import (
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite"
yaml "gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
"os" "os"
"text/template" "strings"
// "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"testing" "testing"
"text/template"
) )
type InitKubeTestSuite struct { type InitKubeTestSuite struct {
@@ -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(),
}
cfg := Config{
Namespace: "Cisco", Namespace: "Cisco",
KubeConfig: configFile.Name(),
} }
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())
@@ -58,30 +82,80 @@ namespace: Cisco
suite.Equal(want, string(conf)) suite.Equal(want, string(conf))
} }
func (suite *InitKubeTestSuite) TestExecuteGeneratesConfig() {
configFile, err := tempfile("kubeconfig********.yml", "")
defer os.Remove(configFile.Name())
suite.Require().NoError(err)
cfg := env.Config{
APIServer: "https://kube.cluster/peanut",
ServiceAccount: "chef",
KubeToken: "eWVhaCB3ZSB0b2tpbic=",
Certificate: "d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE",
Namespace: "marshmallow",
}
init := NewInitKube(cfg, "../../assets/kubeconfig.tpl", configFile.Name()) // the actual kubeconfig template
suite.Require().NoError(init.Prepare())
suite.Require().NoError(init.Execute())
contents, err := ioutil.ReadFile(configFile.Name())
suite.Require().NoError(err)
// each setting should be reflected in the generated file
expectations := []string{
"namespace: marshmallow",
"server: https://kube.cluster/peanut",
"user: chef",
"name: chef",
"token: eWVhaCB3ZSB0b2tpbic",
"certificate-authority-data: d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE",
}
for _, expected := range expectations {
suite.Contains(string(contents), expected)
}
// the generated config should be valid yaml, with no repeated keys
conf := map[string]interface{}{}
suite.NoError(yaml.UnmarshalStrict(contents, &conf))
// test the other branch of the certificate/SkipTLSVerify conditional
init.values.SkipTLSVerify = true
init.values.Certificate = ""
suite.Require().NoError(init.Prepare())
suite.Require().NoError(init.Execute())
contents, err = ioutil.ReadFile(configFile.Name())
suite.Require().NoError(err)
suite.Contains(string(contents), "insecure-skip-tls-verify: true")
conf = map[string]interface{}{}
suite.NoError(yaml.UnmarshalStrict(contents, &conf))
}
func (suite *InitKubeTestSuite) TestPrepareParseError() { func (suite *InitKubeTestSuite) TestPrepareParseError() {
templateFile, err := tempfile("kubeconfig********.yml.tpl", `{{ NonexistentFunction }}`) templateFile, err := tempfile("kubeconfig********.yml.tpl", `{{ NonexistentFunction }}`)
defer os.Remove(templateFile.Name()) defer os.Remove(templateFile.Name())
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)
} }
@@ -90,17 +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(),
} }
init := NewInitKube(cfg, templateFile.Name(), "/usr/foreign/exclude/kubeprofig")
cfg := Config{ err = init.Prepare()
KubeConfig: "/usr/foreign/exclude/kubeprofig",
}
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)
} }
@@ -115,32 +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(),
} }
cfg := Config{ init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
KubeConfig: 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.")
init.Token = "Aspire virtual currency"
init.Certificate = ""
suite.Error(init.Prepare(cfg), "Certificate should be required.")
init.SkipTLSVerify = true
suite.NoError(init.Prepare(cfg), "Certificate should not be required if SkipTLSVerify is true")
} }
func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() { func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
@@ -152,19 +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(), }
init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
init.Prepare()
suite.Equal("helm", init.values.ServiceAccount)
} }
cfg := Config{ func (suite *InitKubeTestSuite) TestDebugOutput() {
KubeConfig: configFile.Name(), templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity")
} defer os.Remove(templateFile.Name())
suite.Require().Nil(err)
init.Prepare(cfg) configFile, err := tempfile("kubeconfig********.yml", "")
suite.Equal("helm", init.ServiceAccount) 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)
}

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 := []string{"--kubeconfig", cfg.KubeConfig} 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,84 +71,49 @@ func (suite *UninstallTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
cfg := Config{ suite.NoError(u.Prepare())
KubeConfig: "/root/.kube/config", expected := []string{"uninstall", "zayde_wølf_king"}
}
suite.NoError(u.Prepare(cfg))
expected := []string{"--kubeconfig", "/root/.kube/config", "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)
KubeConfig: "/root/.kube/config",
}
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes() suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes() suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
suite.NoError(u.Prepare(cfg)) suite.NoError(u.Prepare())
expected := []string{"--kubeconfig", "/root/.kube/config", "uninstall", "--dry-run", "firefox_ak_wildfire"} expected := []string{"uninstall", "--dry-run", "firefox_ak_wildfire"}
suite.Equal(expected, suite.actualArgs) suite.Equal(expected, suite.actualArgs)
} }
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{
KubeConfig: "/root/.kube/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{"--kubeconfig", "/root/.kube/config", expected := []string{"uninstall", "--keep-history", "perturbator_sentient"}
"--namespace", "emotion", "uninstall", "carly_simon_run_away_with_me"}
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{
KubeConfig: "/root/.kube/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 --kubeconfig /root/.kube/config "+
"--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,113 @@ 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
caFile string
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,
caFile: cfg.RepoCAFile,
}
}
// 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 := []string{"--kubeconfig", cfg.KubeConfig} 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)
} }
if u.caFile != "" {
args = append(args, "--ca-file", u.caFile)
}
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,18 +32,52 @@ 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)
}
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)
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "upgrade", "--install", suite.Equal([]string{"upgrade", "--install", "jonas_brothers_only_human", "at40"}, args)
"jonas_brothers_only_human", "at40"}, args)
return suite.mockCmd return suite.mockCmd
} }
@@ -55,26 +90,24 @@ func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
Run(). Run().
Times(1) Times(1)
cfg := Config{ err := u.Prepare()
KubeConfig: "/root/.kube/config",
}
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)
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "--namespace", "melt", "upgrade", suite.Equal([]string{"--namespace", "melt", "upgrade", "--install", "shaed_trampoline", "at40"}, args)
"--install", "shaed_trampoline", "at40"}, args)
return suite.mockCmd return suite.mockCmd
} }
@@ -82,48 +115,47 @@ 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",
KubeConfig: "/root/.kube/config",
}
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{
KubeConfig: "/root/.kube/config",
Values: "age=35", Values: "age=35",
StringValues: "height=5ft10in", StringValues: "height=5ft10in",
ValuesFiles: []string{"/usr/local/stats", "/usr/local/grades"}, ValuesFiles: []string{"/usr/local/stats", "/usr/local/grades"},
ReuseValues: true,
Timeout: "sit_in_the_corner",
Force: true,
AtomicUpgrade: true,
CleanupOnFail: true,
RepoCAFile: "local_ca.cert",
} }
u := NewUpgrade(cfg)
command = func(path string, args ...string) cmd { command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path) suite.Equal(helmBin, path)
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "upgrade", "--install", suite.Equal([]string{"upgrade", "--install",
"--version", "radio_edit", "--version", "radio_edit",
"--dry-run", "--dry-run",
"--wait", "--wait",
"--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
@@ -132,7 +164,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)
} }
@@ -141,35 +173,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,
KubeConfig: "/root/.kube/config",
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().
@@ -184,9 +211,9 @@ 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 --kubeconfig /root/.kube/config --debug upgrade "+ want := fmt.Sprintf("Generated command: '%s --debug upgrade "+
"--install lewis_capaldi_someone_you_loved at40'\n", helmBin) "--install lewis_capaldi_someone_you_loved at40'\n", helmBin)
suite.Equal(want, stderr.String()) suite.Equal(want, stderr.String())
suite.Equal("", stdout.String()) suite.Equal("", stdout.String())