Compare commits
59 Commits
v0.4.0
...
eks-suppor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c1ed849ab | ||
|
|
d8d2e33b20 | ||
|
|
3b78f01b45 | ||
|
|
24060c18fc | ||
|
|
232bb5eb96 | ||
|
|
c0964ae178 | ||
|
|
9f9e83da99 | ||
|
|
39aea4c8dd | ||
|
|
7e25218945 | ||
|
|
3b85c38714 | ||
|
|
1422ec77a4 | ||
|
|
2a13fff548 | ||
|
|
d53a1ed942 | ||
|
|
fc3bfc466a | ||
|
|
8f2d4bec49 | ||
|
|
b2066961e1 | ||
|
|
253a4465f8 | ||
|
|
8857782826 | ||
|
|
81c4877a92 | ||
|
|
fbfb69f063 | ||
|
|
568f613401 | ||
|
|
dc05855aa5 | ||
|
|
a71bba71fd | ||
|
|
f00f6a6329 | ||
|
|
1a70a626ea | ||
|
|
4f549ddf2a | ||
|
|
4cd369b051 | ||
|
|
6b331fdf03 | ||
|
|
801598e1c5 | ||
|
|
cb58b5a021 | ||
|
|
d86ac72529 | ||
|
|
52c9fb552c | ||
|
|
d4506608d7 | ||
|
|
ff8e988122 | ||
|
|
ef66bc0f92 | ||
|
|
08ddf5e27a | ||
|
|
4ba1e694d9 | ||
|
|
10e7e7fee5 | ||
|
|
285e9d98a4 | ||
|
|
db87bd0507 | ||
|
|
e2f53f3b08 | ||
|
|
c4c136b021 | ||
|
|
ef4db923cd | ||
|
|
ae9cb59a1f | ||
|
|
ad5baea3e6 | ||
|
|
4755f502b5 | ||
|
|
59a591eda5 | ||
|
|
3d1c849e75 | ||
|
|
3eb90651d1 | ||
|
|
044caebafd | ||
|
|
dc4ecb6b91 | ||
|
|
cab3a8ae95 | ||
|
|
197a377a82 | ||
|
|
aed59c251e | ||
|
|
2d4688f99b | ||
|
|
420014f9e5 | ||
|
|
285af8a317 | ||
|
|
485eb4375c | ||
|
|
5e2f2f3dc6 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @erincall @grinnellian @kav @josmo
|
||||
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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. -->
|
||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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 -->
|
||||
8
.github/pull_request_template.md
vendored
Normal file
8
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
**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 changes to the config are documented in `docs/parameter_reference.md`
|
||||
* [ ] Any new _required_ config is documented in `README.md`
|
||||
* [ ] Any large changes have been verified by running a Drone job
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,3 +13,5 @@
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
.env
|
||||
.secrets
|
||||
|
||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal 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
|
||||
@@ -2,7 +2,7 @@ FROM alpine/helm
|
||||
MAINTAINER Erin Call <erin@liffft.com>
|
||||
|
||||
COPY build/drone-helm /bin/drone-helm
|
||||
COPY kubeconfig /root/.kube/config.tpl
|
||||
COPY assets/kubeconfig.tpl /root/.kube/config.tpl
|
||||
|
||||
LABEL description="Helm 3 plugin for Drone 3"
|
||||
LABEL base="alpine/helm"
|
||||
|
||||
70
README.md
70
README.md
@@ -1,3 +1,71 @@
|
||||
# Drone plugin for Helm 3
|
||||
|
||||
Dissatisfied with this empty README? Consider grabbing [the "put stuff in the README" issue](https://github.com/pelotech/drone-helm3/issues/8)!
|
||||
This plugin provides an interface between [Drone](https://drone.io/) and [Helm 3](https://github.com/kubernetes/helm):
|
||||
|
||||
* Lint your charts
|
||||
* Deploy your service
|
||||
* Delete your service
|
||||
|
||||
The plugin is inpsired by [drone-helm](https://github.com/ipedrazas/drone-helm), which fills the same role for Helm 2. It provides a comparable feature-set and the configuration settings are backwards-compatible.
|
||||
|
||||
## Example configuration
|
||||
|
||||
The examples below give a minimal and sufficient configuration for each use-case. For a full description of each command's settings, see [docs/parameter_reference.md](docs/parameter_reference.md).
|
||||
|
||||
### Linting
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: lint
|
||||
image: pelotech/drone-helm3
|
||||
settings:
|
||||
helm_command: lint
|
||||
chart: ./
|
||||
```
|
||||
|
||||
### Installation
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: deploy
|
||||
image: pelotech/drone-helm3
|
||||
settings:
|
||||
helm_command: upgrade
|
||||
chart: ./
|
||||
release: my-project
|
||||
environment:
|
||||
API_SERVER: https://my.kubernetes.installation/clusters/a-1234
|
||||
KUBERNETES_TOKEN:
|
||||
from_secret: kubernetes_token
|
||||
```
|
||||
|
||||
### Uninstallation
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: uninstall
|
||||
image: pelotech/drone-helm3
|
||||
settings:
|
||||
helm_command: uninstall
|
||||
release: my-project
|
||||
environment:
|
||||
API_SERVER: https://my.kubernetes.installation/clusters/a-1234
|
||||
KUBERNETES_TOKEN:
|
||||
from_secret: kubernetes_token
|
||||
```
|
||||
|
||||
## Upgrading from drone-helm
|
||||
|
||||
drone-helm3 is largely backwards-compatible with drone-helm. There are some known differences:
|
||||
|
||||
* `prefix` must be supplied via the `settings` block, not `environment`.
|
||||
* Several settings no longer have any effect:
|
||||
* `purge` -- this is the default behavior in Helm 3
|
||||
* `recreate_pods`
|
||||
* `tiller_ns`
|
||||
* `upgrade`
|
||||
* `canary_image`
|
||||
* `client_only`
|
||||
* `stable_repo_url`
|
||||
|
||||
Since helm 3 does not require Tiller, we also recommend switching to a service account with less-expansive permissions.
|
||||
|
||||
@@ -3,7 +3,7 @@ clusters:
|
||||
- cluster:
|
||||
{{- if eq .SkipTLSVerify true }}
|
||||
insecure-skip-tls-verify: true
|
||||
{{- else }}
|
||||
{{- else if .Certificate }}
|
||||
certificate-authority-data: {{ .Certificate }}
|
||||
{{- end}}
|
||||
server: {{ .APIServer }}
|
||||
@@ -2,22 +2,21 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"os"
|
||||
|
||||
"github.com/pelotech/drone-helm3/internal/helm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var c helm.Config
|
||||
cfg, err := helm.NewConfig(os.Stdout, os.Stderr)
|
||||
|
||||
if err := envconfig.Process("plugin", &c); err != nil {
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Make the plan
|
||||
plan, err := helm.NewPlan(c)
|
||||
plan, err := helm.NewPlan(*cfg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%w\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
48
docs/contributing.md
Normal file
48
docs/contributing.md
Normal 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
3
docs/example.secrets
Normal file
@@ -0,0 +1,3 @@
|
||||
DOCKER_PASSWORD=your_access_token
|
||||
DOCKER_USERNAME=your_dockerhub_username
|
||||
PLUGIN_REPO=your_dockerhub_username/drone-helm3-testing
|
||||
133
docs/parameter_reference.md
Normal file
133
docs/parameter_reference.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Parameter reference
|
||||
|
||||
## Global
|
||||
| Param name | Type | Purpose |
|
||||
|---------------------|-----------------|---------|
|
||||
| helm_command | string | Indicates the operation to perform. Recommended, but not required. Valid options are `upgrade`, `uninstall`, `lint`, and `help`. |
|
||||
| update_dependencies | boolean | Calls `helm dependency update` before running the main command. **Not currently implemented**; see [#25](https://github.com/pelotech/drone-helm3/issues/25).|
|
||||
| helm_repos | list\<string\> | Calls `helm repo add $repo` before running the main command. Each string should be formatted as `repo_name=https://repo.url/`. **Not currently implemented**; see [#26](https://github.com/pelotech/drone-helm3/issues/26). |
|
||||
| namespace | string | Kubernetes namespace to use for this operation. |
|
||||
| prefix | string | Expect environment variables to be prefixed with the given string. For more details, see "Using the prefix setting" below. **Not currently implemented**; see [#19](https://github.com/pelotech/drone-helm3/issues/19). |
|
||||
| debug | boolean | Generate debug output within drone-helm3 and pass `--debug` to all helm commands. Use with care, since the debug output may include secrets. |
|
||||
|
||||
## Linting
|
||||
|
||||
Linting is only triggered when the `helm_command` setting is "lint".
|
||||
|
||||
| Param name | Type | Required | Purpose |
|
||||
|---------------|----------------|----------|---------|
|
||||
| chart | string | yes | The chart to be linted. Must be a local path. |
|
||||
| values | list\<string\> | | Chart values to use as the `--set` argument to `helm lint`. |
|
||||
| string_values | list\<string\> | | Chart values to use as the `--set-string` argument to `helm lint`. |
|
||||
| values_files | list\<string\> | | Values to use as `--values` arguments to `helm lint`. |
|
||||
|
||||
## Installation
|
||||
|
||||
Installations are triggered when the `helm_command` setting is "upgrade." They can also be triggered when the build was triggered by a `push`, `tag`, `deployment`, `pull_request`, `promote`, or `rollback` Drone event.
|
||||
|
||||
| Param name | Type | Required | Purpose |
|
||||
|------------------------|----------------|----------|---------|
|
||||
| chart | string | yes | The chart to use for this installation. |
|
||||
| release | string | yes | The release name for helm to use. |
|
||||
| api_server | string | yes | API endpoint for the Kubernetes cluster. |
|
||||
| kubernetes_token | string | yes, unless using EKS | Token for authenticating to Kubernetes. |
|
||||
| eks_cluster | string | | AWS EKS cluster ID. |
|
||||
| eks_role_arn | string | | AWS IAM role ARN for EKS authentication. |
|
||||
| service_account | string | | Service account for authenticating to Kubernetes. Default is `helm`. |
|
||||
| kubernetes_certificate | string | | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. |
|
||||
| chart_version | string | | Specific chart version to install. |
|
||||
| dry_run | boolean | | Pass `--dry-run` to `helm upgrade`. |
|
||||
| wait | boolean | | Wait until kubernetes resources are in a ready state before marking the installation successful. |
|
||||
| timeout | duration | | Timeout for any *individual* Kubernetes operation. The installation's full runtime may exceed this duration. |
|
||||
| force | boolean | | Pass `--force` to `helm upgrade`. |
|
||||
| values | list\<string\> | | Chart values to use as the `--set` argument to `helm upgrade`. |
|
||||
| string_values | list\<string\> | | Chart values to use as the `--set-string` argument to `helm upgrade`. |
|
||||
| values_files | list\<string\> | | Values to use as `--values` arguments to `helm upgrade`. |
|
||||
| reuse_values | boolean | | Reuse the values from a previous release. |
|
||||
| skip_tls_verify | boolean | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. |
|
||||
|
||||
## Uninstallation
|
||||
|
||||
Uninstallations are triggered when the `helm_command` setting is "uninstall" or "delete." They can also be triggered when the build was triggered by a `delete` Drone event.
|
||||
|
||||
| Param name | Type | Required | Purpose |
|
||||
|------------------------|----------|----------|---------|
|
||||
| release | string | yes | The release name for helm to use. |
|
||||
| api_server | string | yes | API endpoint for the Kubernetes cluster. |
|
||||
| kubernetes_token | string | yes, unless using EKS | Token for authenticating to Kubernetes. |
|
||||
| eks_cluster | string | | AWS EKS cluster ID. |
|
||||
| eks_role_arn | string | | AWS IAM role ARN for EKS authentication. |
|
||||
| service_account | string | | Service account for authenticating to Kubernetes. Default is `helm`. |
|
||||
| kubernetes_certificate | string | | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. |
|
||||
| dry_run | boolean | | Pass `--dry-run` to `helm uninstall`. |
|
||||
| timeout | duration | | Timeout for any *individual* Kubernetes operation. The uninstallation's full runtime may exceed this duration. |
|
||||
| skip_tls_verify | boolean | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. |
|
||||
|
||||
### Where to put settings
|
||||
|
||||
Any setting (with the exception of `prefix`; [see below](#user-content-using-the-prefix-setting)), can go in either the `settings` or `environment` section.
|
||||
|
||||
### Formatting non-string values
|
||||
|
||||
* Booleans can be yaml's `true` and `false` literals or the strings `"true"` and `"false"`.
|
||||
* Durations are strings formatted with the syntax accepted by [golang's ParseDuration function](https://golang.org/pkg/time/#ParseDuration) (e.g. 5m30s)
|
||||
* List\<string\>s can be a yaml sequence or a comma-separated string.
|
||||
|
||||
All of the following are equivalent:
|
||||
|
||||
```yaml
|
||||
values: "foo=1,bar=2"
|
||||
values: ["foo=1", "bar=2"]
|
||||
values:
|
||||
- foo=1
|
||||
- bar=2
|
||||
```
|
||||
|
||||
Note that **list members must not contain commas**. Both of the following are equivalent:
|
||||
|
||||
```yaml
|
||||
values_files: [ "./over_9,000.yml" ]
|
||||
values_files: [ "./over_9", "000.yml" ]
|
||||
```
|
||||
|
||||
### Using the `prefix` setting
|
||||
|
||||
Because the prefix setting is meta-configuration, it has some inherent edge-cases. Here is what it does in the cases we've thought of:
|
||||
|
||||
Unlike the other settings, it must be declared in the `settings` block, not `environment`:
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
prefix: helm # drone-helm3 will look for environment variables called HELM_VARNAME
|
||||
environment:
|
||||
prefix: armet # no effect
|
||||
```
|
||||
|
||||
It does not apply to configuration in the `settings` block, only in `environment`:
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
prefix: helm
|
||||
helm_timeout: 5m # no effect
|
||||
environment:
|
||||
helm_timeout: 2m # timeout will be 2 minutes
|
||||
```
|
||||
|
||||
If the environment contains a variable in non-prefixed form, it will still be applied:
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
prefix: helm
|
||||
environment:
|
||||
timeout: 2m # timeout will be 2 minutes
|
||||
```
|
||||
|
||||
If the environment contains both the prefixed and non-prefixed forms, drone-helm3 will use the prefixed form:
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
prefix: helm
|
||||
environment:
|
||||
timeout: 5m # overridden
|
||||
helm_timeout: 2m # timeout will be 2 minutes
|
||||
```
|
||||
1
go.mod
1
go.mod
@@ -8,4 +8,5 @@ require (
|
||||
github.com/stretchr/testify v1.4.0
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
|
||||
golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2
|
||||
)
|
||||
|
||||
@@ -2,62 +2,78 @@ package helm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"io"
|
||||
)
|
||||
|
||||
// The Config struct captures the `settings` and `environment` blocks in the application's drone
|
||||
// config. Configuration in drone's `settings` block arrives as uppercase env vars matching the
|
||||
// config key, prefixed with `PLUGIN_`. Config from the `environment` block is *not* prefixed; any
|
||||
// keys that are likely to be in that block (i.e. things that use `from_secret` need an explicit
|
||||
// `envconfig:` tag so that envconfig will look for a non-prefixed env var.
|
||||
// config key, prefixed with `PLUGIN_`. Config from the `environment` block is uppercased, but does
|
||||
// not have the `PLUGIN_` prefix. It may, however, be prefixed with the value in `$PLUGIN_PREFIX`.
|
||||
type Config struct {
|
||||
// Configuration for drone-helm itself
|
||||
Command helmCommand `envconfig:"HELM_COMMAND"` // Helm command to run
|
||||
Command string `envconfig:"HELM_COMMAND"` // Helm command to run
|
||||
DroneEvent string `envconfig:"DRONE_BUILD_EVENT"` // Drone event that invoked this plugin.
|
||||
UpdateDependencies bool `split_words:"true"` // call `helm dependency update` before the main command
|
||||
Repos []string `envconfig:"HELM_REPOS"` // call `helm repo add` before the main command
|
||||
UpdateDependencies bool `split_words:"true"` // Call `helm dependency update` before the main command
|
||||
Repos []string `envconfig:"HELM_REPOS"` // Call `helm repo add` before the main command
|
||||
Prefix string `` // Prefix to use when looking up secret env vars
|
||||
Debug bool `` // Generate debug output and pass --debug to all helm commands
|
||||
Values string `` // Argument to pass to --set in applicable helm commands
|
||||
StringValues string `split_words:"true"` // Argument to pass to --set-string in applicable helm commands
|
||||
ValuesFiles []string `split_words:"true"` // Arguments to pass to --values in applicable helm commands
|
||||
Namespace string `` // Kubernetes namespace for all helm commands
|
||||
KubeToken string `envconfig:"KUBERNETES_TOKEN"` // Kubernetes authentication token to put in .kube/config
|
||||
EKSCluster string `envconfig:"EKS_CLUSTER"` // AWS EKS Cluster ID to put in .kube/config
|
||||
EKSRoleARN string `envconfig:"EKS_ROLE_ARN"` // AWS IAM role resource name to put in .kube/config
|
||||
SkipTLSVerify bool `envconfig:"SKIP_TLS_VERIFY"` // Put insecure-skip-tls-verify in .kube/config
|
||||
Certificate string `envconfig:"KUBERNETES_CERTIFICATE"` // The Kubernetes cluster CA's self-signed certificate (must be base64-encoded)
|
||||
APIServer string `envconfig:"API_SERVER"` // The Kubernetes cluster's API endpoint
|
||||
ServiceAccount string `split_words:"true"` // Account to use for connecting to the Kubernetes cluster
|
||||
ChartVersion string `split_words:"true"` // Specific chart version to use in `helm upgrade`
|
||||
DryRun bool `split_words:"true"` // Pass --dry-run to applicable helm commands
|
||||
Wait bool `` // Pass --wait to applicable helm commands
|
||||
ReuseValues bool `split_words:"true"` // Pass --reuse-values to `helm upgrade`
|
||||
Timeout string `` // Argument to pass to --timeout in applicable helm commands
|
||||
Chart string `` // Chart argument to use in applicable helm commands
|
||||
Release string `` // Release argument to use in applicable helm commands
|
||||
Force bool `` // Pass --force to applicable helm commands
|
||||
|
||||
// Global helm config
|
||||
Debug bool `` // global helm flag (also applies to drone-helm itself)
|
||||
KubeConfig string `split_words:"true" default:"/root/.kube/config"` // path to the kube config file
|
||||
Values string ``
|
||||
StringValues string `split_words:"true"`
|
||||
ValuesFiles []string `split_words:"true"`
|
||||
Namespace string ``
|
||||
KubeToken string `envconfig:"KUBERNETES_TOKEN"`
|
||||
SkipTLSVerify bool `envconfig:"SKIP_TLS_VERIFY"`
|
||||
Certificate string `envconfig:"KUBERNETES_CERTIFICATE"`
|
||||
APIServer string `envconfig:"API_SERVER"`
|
||||
ServiceAccount string `envconfig:"SERVICE_ACCOUNT"` // Can't just use split_words; need envconfig to find the non-prefixed form
|
||||
|
||||
// Config specifically for `helm upgrade`
|
||||
ChartVersion string `split_words:"true"` //
|
||||
DryRun bool `split_words:"true"` // also available for `delete`
|
||||
Wait bool `` //
|
||||
ReuseValues bool `split_words:"true"` //
|
||||
Timeout string `` //
|
||||
Chart string `` // Also available for `lint`, in which case it must be a path to a chart directory
|
||||
Release string ``
|
||||
Force bool `` //
|
||||
Stdout io.Writer `ignored:"true"`
|
||||
Stderr io.Writer `ignored:"true"`
|
||||
}
|
||||
|
||||
type helmCommand string
|
||||
// NewConfig creates a Config and reads environment variables into it, accounting for several possible formats.
|
||||
func NewConfig(stdout, stderr io.Writer) (*Config, error) {
|
||||
cfg := Config{
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
}
|
||||
if err := envconfig.Process("plugin", &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// helmCommand.Decode checks the given value against the list of known commands and generates a helpful error if the command is unknown.
|
||||
func (cmd *helmCommand) Decode(value string) error {
|
||||
known := []string{"upgrade", "delete", "lint", "help"}
|
||||
for _, c := range known {
|
||||
if value == c {
|
||||
*cmd = helmCommand(value)
|
||||
return nil
|
||||
prefix := cfg.Prefix
|
||||
|
||||
if err := envconfig.Process("", &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if prefix != "" {
|
||||
if err := envconfig.Process(cfg.Prefix, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
return nil
|
||||
if cfg.Debug && cfg.Stderr != nil {
|
||||
cfg.logDebug()
|
||||
}
|
||||
known[len(known)-1] = fmt.Sprintf("or %s", known[len(known)-1])
|
||||
return fmt.Errorf("unknown command '%s'. If specified, command must be %s",
|
||||
value, strings.Join(known, ", "))
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (cfg Config) logDebug() {
|
||||
if cfg.KubeToken != "" {
|
||||
cfg.KubeToken = "(redacted)"
|
||||
}
|
||||
fmt.Fprintf(cfg.Stderr, "Generated config: %+v\n", cfg)
|
||||
}
|
||||
|
||||
@@ -2,27 +2,179 @@ package helm
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type ConfigTestSuite struct {
|
||||
suite.Suite
|
||||
// These tests need to mutate the environment, so the suite.setenv and .unsetenv functions store the original contents of the
|
||||
// relevant variable in this map. Its use of *string is so they can distinguish between "not set" and "set to empty string"
|
||||
envBackup map[string]*string
|
||||
}
|
||||
|
||||
func TestConfigTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConfigTestSuite))
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestHelmCommandDecodeSuccess() {
|
||||
cmd := helmCommand("")
|
||||
err := cmd.Decode("upgrade")
|
||||
suite.Require().Nil(err)
|
||||
func (suite *ConfigTestSuite) TestNewConfigWithPluginPrefix() {
|
||||
suite.unsetenv("PLUGIN_PREFIX")
|
||||
suite.unsetenv("HELM_COMMAND")
|
||||
suite.unsetenv("UPDATE_DEPENDENCIES")
|
||||
suite.unsetenv("DEBUG")
|
||||
|
||||
suite.EqualValues(cmd, "upgrade")
|
||||
suite.setenv("PLUGIN_HELM_COMMAND", "execute order 66")
|
||||
suite.setenv("PLUGIN_UPDATE_DEPENDENCIES", "true")
|
||||
suite.setenv("PLUGIN_DEBUG", "true")
|
||||
|
||||
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Equal("execute order 66", cfg.Command)
|
||||
suite.True(cfg.UpdateDependencies)
|
||||
suite.True(cfg.Debug)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestHelmCommandDecodeFailure() {
|
||||
cmd := helmCommand("")
|
||||
err := cmd.Decode("execute order 66")
|
||||
suite.EqualError(err, "unknown command 'execute order 66'. If specified, command must be upgrade, delete, lint, or help")
|
||||
func (suite *ConfigTestSuite) TestNewConfigWithNoPrefix() {
|
||||
suite.unsetenv("PLUGIN_PREFIX")
|
||||
suite.unsetenv("PLUGIN_HELM_COMMAND")
|
||||
suite.unsetenv("PLUGIN_UPDATE_DEPENDENCIES")
|
||||
suite.unsetenv("PLUGIN_DEBUG")
|
||||
|
||||
suite.setenv("HELM_COMMAND", "execute order 66")
|
||||
suite.setenv("UPDATE_DEPENDENCIES", "true")
|
||||
suite.setenv("DEBUG", "true")
|
||||
|
||||
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Equal("execute order 66", cfg.Command)
|
||||
suite.True(cfg.UpdateDependencies)
|
||||
suite.True(cfg.Debug)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestNewConfigWithConfigurablePrefix() {
|
||||
suite.unsetenv("API_SERVER")
|
||||
suite.unsetenv("PLUGIN_API_SERVER")
|
||||
|
||||
suite.setenv("PLUGIN_PREFIX", "prix_fixe")
|
||||
suite.setenv("PRIX_FIXE_API_SERVER", "your waiter this evening")
|
||||
|
||||
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Equal("prix_fixe", cfg.Prefix)
|
||||
suite.Equal("your waiter this evening", cfg.APIServer)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestPrefixSettingDoesNotAffectPluginPrefix() {
|
||||
suite.setenv("PLUGIN_PREFIX", "IXFREP")
|
||||
suite.setenv("PLUGIN_HELM_COMMAND", "wake me up")
|
||||
suite.setenv("IXFREP_PLUGIN_HELM_COMMAND", "send me to sleep inside")
|
||||
|
||||
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Equal("wake me up", cfg.Command)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestPrefixSettingMustHavePluginPrefix() {
|
||||
suite.unsetenv("PLUGIN_PREFIX")
|
||||
suite.setenv("PREFIX", "refpix")
|
||||
suite.setenv("HELM_COMMAND", "gimme more")
|
||||
suite.setenv("REFPIX_HELM_COMMAND", "gimme less")
|
||||
|
||||
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Equal("gimme more", cfg.Command)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestNewConfigWithConflictingVariables() {
|
||||
suite.setenv("PLUGIN_HELM_COMMAND", "execute order 66")
|
||||
suite.setenv("HELM_COMMAND", "defend the jedi") // values from the `environment` block override those from `settings`
|
||||
|
||||
suite.setenv("PLUGIN_PREFIX", "prod")
|
||||
suite.setenv("TIMEOUT", "5m0s")
|
||||
suite.setenv("PROD_TIMEOUT", "2m30s") // values from prefixed env vars override those from non-prefixed ones
|
||||
|
||||
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Equal("defend the jedi", cfg.Command)
|
||||
suite.Equal("2m30s", cfg.Timeout)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestNewConfigSetsWriters() {
|
||||
stdout := &strings.Builder{}
|
||||
stderr := &strings.Builder{}
|
||||
cfg, err := NewConfig(stdout, stderr)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Equal(stdout, cfg.Stdout)
|
||||
suite.Equal(stderr, cfg.Stderr)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestLogDebug() {
|
||||
suite.setenv("DEBUG", "true")
|
||||
suite.setenv("HELM_COMMAND", "upgrade")
|
||||
|
||||
stderr := strings.Builder{}
|
||||
stdout := strings.Builder{}
|
||||
_, err := NewConfig(&stdout, &stderr)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.Equal("", stdout.String())
|
||||
|
||||
suite.Regexp(`^Generated config: \{Command:upgrade.*\}`, stderr.String())
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) TestLogDebugCensorsKubeToken() {
|
||||
stderr := &strings.Builder{}
|
||||
kubeToken := "I'm shy! Don't put me in your build logs!"
|
||||
cfg := Config{
|
||||
Debug: true,
|
||||
KubeToken: kubeToken,
|
||||
Stderr: stderr,
|
||||
}
|
||||
|
||||
cfg.logDebug()
|
||||
|
||||
suite.Contains(stderr.String(), "KubeToken:(redacted)")
|
||||
suite.Equal(kubeToken, cfg.KubeToken) // The actual config value should be left unchanged
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) setenv(key, val string) {
|
||||
orig, ok := os.LookupEnv(key)
|
||||
if ok {
|
||||
suite.envBackup[key] = &orig
|
||||
} else {
|
||||
suite.envBackup[key] = nil
|
||||
}
|
||||
os.Setenv(key, val)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) unsetenv(key string) {
|
||||
orig, ok := os.LookupEnv(key)
|
||||
if ok {
|
||||
suite.envBackup[key] = &orig
|
||||
} else {
|
||||
suite.envBackup[key] = nil
|
||||
}
|
||||
os.Unsetenv(key)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) BeforeTest(_, _ string) {
|
||||
suite.envBackup = make(map[string]*string)
|
||||
}
|
||||
|
||||
func (suite *ConfigTestSuite) AfterTest(_, _ string) {
|
||||
for key, val := range suite.envBackup {
|
||||
if val == nil {
|
||||
os.Unsetenv(key)
|
||||
} else {
|
||||
os.Setenv(key, *val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
const kubeConfigTemplate = "/root/.kube/config.tpl"
|
||||
const (
|
||||
kubeConfigTemplate = "/root/.kube/config.tpl"
|
||||
kubeConfigFile = "/root/.kube/config"
|
||||
)
|
||||
|
||||
// A Step is one step in the plan.
|
||||
type Step interface {
|
||||
@@ -27,13 +30,12 @@ func NewPlan(cfg Config) (*Plan, error) {
|
||||
cfg: cfg,
|
||||
runCfg: run.Config{
|
||||
Debug: cfg.Debug,
|
||||
KubeConfig: cfg.KubeConfig,
|
||||
Values: cfg.Values,
|
||||
StringValues: cfg.StringValues,
|
||||
ValuesFiles: cfg.ValuesFiles,
|
||||
Namespace: cfg.Namespace,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
Stdout: cfg.Stdout,
|
||||
Stderr: cfg.Stderr,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -81,11 +83,11 @@ func determineSteps(cfg Config) *func(Config) []Step {
|
||||
func (p *Plan) Execute() error {
|
||||
for i, step := range p.steps {
|
||||
if p.cfg.Debug {
|
||||
fmt.Fprintf(os.Stderr, "calling %T.Execute (step %d)\n", step, i)
|
||||
fmt.Fprintf(p.cfg.Stderr, "calling %T.Execute (step %d)\n", step, i)
|
||||
}
|
||||
|
||||
if err := step.Execute(p.runCfg); err != nil {
|
||||
return fmt.Errorf("in execution step %d: %w", i, err)
|
||||
return fmt.Errorf("while executing %T step: %w", step, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +142,10 @@ func initKube(cfg Config) []Step {
|
||||
APIServer: cfg.APIServer,
|
||||
ServiceAccount: cfg.ServiceAccount,
|
||||
Token: cfg.KubeToken,
|
||||
EKSCluster: cfg.EKSCluster,
|
||||
EKSRoleARN: cfg.EKSRoleARN,
|
||||
TemplateFile: kubeConfigTemplate,
|
||||
ConfigFile: kubeConfigFile,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/pelotech/drone-helm3/internal/run"
|
||||
@@ -20,6 +20,7 @@ func TestPlanTestSuite(t *testing.T) {
|
||||
|
||||
func (suite *PlanTestSuite) TestNewPlan() {
|
||||
ctrl := gomock.NewController(suite.T())
|
||||
defer ctrl.Finish()
|
||||
stepOne := NewMockStep(ctrl)
|
||||
stepTwo := NewMockStep(ctrl)
|
||||
|
||||
@@ -29,25 +30,27 @@ func (suite *PlanTestSuite) TestNewPlan() {
|
||||
}
|
||||
defer func() { help = origHelp }()
|
||||
|
||||
stdout := strings.Builder{}
|
||||
stderr := strings.Builder{}
|
||||
cfg := Config{
|
||||
Command: "help",
|
||||
Debug: false,
|
||||
KubeConfig: "/branch/.sfere/profig",
|
||||
Values: "steadfastness,forthrightness",
|
||||
StringValues: "tensile_strength,flexibility",
|
||||
ValuesFiles: []string{"/root/price_inventory.yml"},
|
||||
Namespace: "outer",
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
|
||||
runCfg := run.Config{
|
||||
Debug: false,
|
||||
KubeConfig: "/branch/.sfere/profig",
|
||||
Values: "steadfastness,forthrightness",
|
||||
StringValues: "tensile_strength,flexibility",
|
||||
ValuesFiles: []string{"/root/price_inventory.yml"},
|
||||
Namespace: "outer",
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
|
||||
stepOne.EXPECT().
|
||||
@@ -63,6 +66,7 @@ func (suite *PlanTestSuite) TestNewPlan() {
|
||||
|
||||
func (suite *PlanTestSuite) TestNewPlanAbortsOnError() {
|
||||
ctrl := gomock.NewController(suite.T())
|
||||
defer ctrl.Finish()
|
||||
stepOne := NewMockStep(ctrl)
|
||||
stepTwo := NewMockStep(ctrl)
|
||||
|
||||
@@ -85,6 +89,51 @@ func (suite *PlanTestSuite) TestNewPlanAbortsOnError() {
|
||||
suite.EqualError(err, "while preparing *helm.MockStep step: I'm starry Dave, aye, cat blew that")
|
||||
}
|
||||
|
||||
func (suite *PlanTestSuite) TestExecute() {
|
||||
ctrl := gomock.NewController(suite.T())
|
||||
defer ctrl.Finish()
|
||||
stepOne := NewMockStep(ctrl)
|
||||
stepTwo := NewMockStep(ctrl)
|
||||
|
||||
runCfg := run.Config{}
|
||||
|
||||
plan := Plan{
|
||||
steps: []Step{stepOne, stepTwo},
|
||||
runCfg: runCfg,
|
||||
}
|
||||
|
||||
stepOne.EXPECT().
|
||||
Execute(runCfg).
|
||||
Times(1)
|
||||
stepTwo.EXPECT().
|
||||
Execute(runCfg).
|
||||
Times(1)
|
||||
|
||||
suite.NoError(plan.Execute())
|
||||
}
|
||||
|
||||
func (suite *PlanTestSuite) TestExecuteAbortsOnError() {
|
||||
ctrl := gomock.NewController(suite.T())
|
||||
defer ctrl.Finish()
|
||||
stepOne := NewMockStep(ctrl)
|
||||
stepTwo := NewMockStep(ctrl)
|
||||
|
||||
runCfg := run.Config{}
|
||||
|
||||
plan := Plan{
|
||||
steps: []Step{stepOne, stepTwo},
|
||||
runCfg: runCfg,
|
||||
}
|
||||
|
||||
stepOne.EXPECT().
|
||||
Execute(runCfg).
|
||||
Times(1).
|
||||
Return(fmt.Errorf("oh, he'll gnaw"))
|
||||
|
||||
err := plan.Execute()
|
||||
suite.EqualError(err, "while executing *helm.MockStep step: oh, he'll gnaw")
|
||||
}
|
||||
|
||||
func (suite *PlanTestSuite) TestUpgrade() {
|
||||
cfg := Config{
|
||||
ChartVersion: "seventeen",
|
||||
@@ -142,6 +191,7 @@ func (suite *PlanTestSuite) TestDel() {
|
||||
ServiceAccount: "greathelm",
|
||||
Token: "b2YgbXkgYWZmZWN0aW9u",
|
||||
TemplateFile: kubeConfigTemplate,
|
||||
ConfigFile: kubeConfigFile,
|
||||
}
|
||||
|
||||
suite.Equal(expected, init)
|
||||
@@ -162,6 +212,8 @@ func (suite *PlanTestSuite) TestInitKube() {
|
||||
Certificate: "b2Ygd29rZW5lc3MK",
|
||||
APIServer: "123.456.78.9",
|
||||
ServiceAccount: "helmet",
|
||||
EKSCluster: "eks_reader",
|
||||
EKSRoleARN: "arn:aws:iam::9631085:role/eksSpangleRole",
|
||||
}
|
||||
|
||||
steps := initKube(cfg)
|
||||
@@ -175,7 +227,10 @@ func (suite *PlanTestSuite) TestInitKube() {
|
||||
APIServer: "123.456.78.9",
|
||||
ServiceAccount: "helmet",
|
||||
Token: "cXVlZXIgY2hhcmFjdGVyCg==",
|
||||
EKSCluster: "eks_reader",
|
||||
EKSRoleARN: "arn:aws:iam::9631085:role/eksSpangleRole",
|
||||
TemplateFile: kubeConfigTemplate,
|
||||
ConfigFile: kubeConfigFile,
|
||||
}
|
||||
suite.Equal(expected, init)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
// Config contains configuration applicable to all helm commands
|
||||
type Config struct {
|
||||
Debug bool
|
||||
KubeConfig string
|
||||
Values string
|
||||
StringValues string
|
||||
ValuesFiles []string
|
||||
|
||||
@@ -15,7 +15,10 @@ type InitKube struct {
|
||||
APIServer string
|
||||
ServiceAccount string
|
||||
Token string
|
||||
EKSCluster string
|
||||
EKSRoleARN string
|
||||
TemplateFile string
|
||||
ConfigFile string
|
||||
|
||||
template *template.Template
|
||||
configFile io.WriteCloser
|
||||
@@ -29,12 +32,14 @@ type kubeValues struct {
|
||||
Namespace string
|
||||
ServiceAccount string
|
||||
Token string
|
||||
EKSCluster string
|
||||
EKSRoleARN string
|
||||
}
|
||||
|
||||
// Execute generates a kubernetes config file from drone-helm3's template.
|
||||
func (i *InitKube) Execute(cfg Config) error {
|
||||
if cfg.Debug {
|
||||
fmt.Fprintf(cfg.Stderr, "writing kubeconfig file to %s\n", cfg.KubeConfig)
|
||||
fmt.Fprintf(cfg.Stderr, "writing kubeconfig file to %s\n", i.ConfigFile)
|
||||
}
|
||||
defer i.configFile.Close()
|
||||
return i.template.Execute(i.configFile, i.values)
|
||||
@@ -47,11 +52,11 @@ func (i *InitKube) Prepare(cfg Config) error {
|
||||
if i.APIServer == "" {
|
||||
return errors.New("an API Server is needed to deploy")
|
||||
}
|
||||
if i.Token == "" {
|
||||
if i.Token == "" && i.EKSCluster == "" {
|
||||
return errors.New("token is needed to deploy")
|
||||
}
|
||||
if i.Certificate == "" && !i.SkipTLSVerify {
|
||||
return errors.New("certificate is needed to deploy")
|
||||
if i.Token != "" && i.EKSCluster != "" {
|
||||
return errors.New("token cannot be used simultaneously with eksCluster")
|
||||
}
|
||||
|
||||
if i.ServiceAccount == "" {
|
||||
@@ -72,20 +77,22 @@ func (i *InitKube) Prepare(cfg Config) error {
|
||||
APIServer: i.APIServer,
|
||||
ServiceAccount: i.ServiceAccount,
|
||||
Token: i.Token,
|
||||
EKSCluster: i.EKSCluster,
|
||||
EKSRoleARN: i.EKSRoleARN,
|
||||
Namespace: cfg.Namespace,
|
||||
}
|
||||
|
||||
if cfg.Debug {
|
||||
if _, err := os.Stat(cfg.KubeConfig); err != nil {
|
||||
if _, err := os.Stat(i.ConfigFile); err != nil {
|
||||
// non-nil err here isn't an actual error state; the kubeconfig just doesn't exist
|
||||
fmt.Fprint(cfg.Stderr, "creating ")
|
||||
} else {
|
||||
fmt.Fprint(cfg.Stderr, "truncating ")
|
||||
}
|
||||
fmt.Fprintf(cfg.Stderr, "kubeconfig file at %s\n", cfg.KubeConfig)
|
||||
fmt.Fprintf(cfg.Stderr, "kubeconfig file at %s\n", i.ConfigFile)
|
||||
}
|
||||
|
||||
i.configFile, err = os.Create(cfg.KubeConfig)
|
||||
i.configFile, err = os.Create(i.ConfigFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open kubeconfig file for writing: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"text/template"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"testing"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type InitKubeTestSuite struct {
|
||||
@@ -34,10 +33,10 @@ namespace: {{ .Namespace }}
|
||||
Certificate: "CCNA",
|
||||
Token: "Aspire virtual currency",
|
||||
TemplateFile: templateFile.Name(),
|
||||
ConfigFile: configFile.Name(),
|
||||
}
|
||||
cfg := Config{
|
||||
Namespace: "Cisco",
|
||||
KubeConfig: configFile.Name(),
|
||||
}
|
||||
err = init.Prepare(cfg)
|
||||
suite.Require().Nil(err)
|
||||
@@ -95,11 +94,10 @@ func (suite *InitKubeTestSuite) TestPrepareCannotOpenDestinationFile() {
|
||||
Certificate: "CCNA",
|
||||
Token: "Aspire virtual currency",
|
||||
TemplateFile: templateFile.Name(),
|
||||
ConfigFile: "/usr/foreign/exclude/kubeprofig",
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
KubeConfig: "/usr/foreign/exclude/kubeprofig",
|
||||
}
|
||||
cfg := Config{}
|
||||
err = init.Prepare(cfg)
|
||||
suite.Error(err)
|
||||
suite.Regexp("could not open .* for writing: .* no such file or directory", err)
|
||||
@@ -120,11 +118,10 @@ func (suite *InitKubeTestSuite) TestPrepareRequiredConfig() {
|
||||
Certificate: "CCNA",
|
||||
Token: "Aspire virtual currency",
|
||||
TemplateFile: templateFile.Name(),
|
||||
ConfigFile: configFile.Name(),
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
KubeConfig: configFile.Name(),
|
||||
}
|
||||
cfg := Config{}
|
||||
|
||||
suite.NoError(init.Prepare(cfg)) // consistency check; we should be starting in a happy state
|
||||
|
||||
@@ -134,13 +131,33 @@ func (suite *InitKubeTestSuite) TestPrepareRequiredConfig() {
|
||||
init.APIServer = "Sysadmin"
|
||||
init.Token = ""
|
||||
suite.Error(init.Prepare(cfg), "Token should be required.")
|
||||
}
|
||||
|
||||
init.Token = "Aspire virtual currency"
|
||||
init.Certificate = ""
|
||||
suite.Error(init.Prepare(cfg), "Certificate should be required.")
|
||||
func (suite *InitKubeTestSuite) TestPrepareEKSConfig() {
|
||||
templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity")
|
||||
defer os.Remove(templateFile.Name())
|
||||
suite.Require().Nil(err)
|
||||
|
||||
init.SkipTLSVerify = true
|
||||
suite.NoError(init.Prepare(cfg), "Certificate should not be required if SkipTLSVerify is true")
|
||||
configFile, err := tempfile("kubeconfig********.yml", "")
|
||||
defer os.Remove(configFile.Name())
|
||||
suite.Require().Nil(err)
|
||||
|
||||
init := InitKube{
|
||||
TemplateFile: templateFile.Name(),
|
||||
ConfigFile: configFile.Name(),
|
||||
APIServer: "eks.aws.amazonaws.com",
|
||||
EKSCluster: "it-is-an-eks-parrot",
|
||||
EKSRoleARN: "arn:aws:iam::19691207:role/mrPraline",
|
||||
}
|
||||
|
||||
cfg := Config{}
|
||||
|
||||
suite.NoError(init.Prepare(cfg))
|
||||
suite.Equal(init.values.EKSCluster, "it-is-an-eks-parrot")
|
||||
suite.Equal(init.values.EKSRoleARN, "arn:aws:iam::19691207:role/mrPraline")
|
||||
|
||||
init.Token = "cGluaW5nIGZvciB0aGUgZmrDtnJkcw=="
|
||||
suite.EqualError(init.Prepare(cfg), "token cannot be used simultaneously with eksCluster")
|
||||
}
|
||||
|
||||
func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
|
||||
@@ -157,11 +174,10 @@ func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
|
||||
Certificate: "CCNA",
|
||||
Token: "Aspire virtual currency",
|
||||
TemplateFile: templateFile.Name(),
|
||||
ConfigFile: configFile.Name(),
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
KubeConfig: configFile.Name(),
|
||||
}
|
||||
cfg := Config{}
|
||||
|
||||
init.Prepare(cfg)
|
||||
suite.Equal("helm", init.ServiceAccount)
|
||||
|
||||
109
internal/run/kubeconfig_test.go
Normal file
109
internal/run/kubeconfig_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type KubeconfigTestSuite struct {
|
||||
suite.Suite
|
||||
configFile *os.File
|
||||
initKube InitKube
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) BeforeTest(_, _ string) {
|
||||
file, err := ioutil.TempFile("", "kubeconfig********.yml")
|
||||
suite.Require().NoError(err)
|
||||
file.Close()
|
||||
suite.configFile = file
|
||||
|
||||
// set up an InitKube with the bare minimum configuration
|
||||
suite.initKube = InitKube{
|
||||
ConfigFile: file.Name(),
|
||||
TemplateFile: "../../assets/kubeconfig.tpl", // the actual kubeconfig template
|
||||
APIServer: "a",
|
||||
Token: "b",
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) AfterTest(_, _ string) {
|
||||
if suite.configFile != nil {
|
||||
os.Remove(suite.configFile.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestKubeconfigTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(KubeconfigTestSuite))
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) TestSetsNamespace() {
|
||||
cfg := Config{
|
||||
Namespace: "marshmallow",
|
||||
}
|
||||
contents := suite.generateKubeconfig(cfg)
|
||||
suite.Contains(contents, "namespace: marshmallow")
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) TestSetsAPIServer() {
|
||||
suite.initKube.APIServer = "https://kube.cluster/peanut"
|
||||
contents := suite.generateKubeconfig(Config{})
|
||||
suite.Contains(contents, "server: https://kube.cluster/peanut")
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) TestSetsServiceAccount() {
|
||||
suite.initKube.ServiceAccount = "chef"
|
||||
contents := suite.generateKubeconfig(Config{})
|
||||
suite.Contains(contents, "user: chef")
|
||||
suite.Contains(contents, "name: chef")
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) TestSetsToken() {
|
||||
suite.initKube.Token = "eWVhaCB3ZSB0b2tpbic"
|
||||
contents := suite.generateKubeconfig(Config{})
|
||||
suite.Contains(contents, "token: eWVhaCB3ZSB0b2tpbic")
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) TestSetsCertificate() {
|
||||
suite.initKube.Certificate = "d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE"
|
||||
contents := suite.generateKubeconfig(Config{})
|
||||
suite.Contains(contents, "certificate-authority-data: d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE")
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) TestSetsSkipTLSVerify() {
|
||||
suite.initKube.SkipTLSVerify = true
|
||||
contents := suite.generateKubeconfig(Config{})
|
||||
suite.Contains(contents, "insecure-skip-tls-verify: true")
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) TestSetsEKSCluster() {
|
||||
suite.initKube.Token = ""
|
||||
suite.initKube.EKSCluster = "it-is-an-eks-parrot"
|
||||
contents := suite.generateKubeconfig(Config{})
|
||||
suite.Contains(contents, "command: aws-iam-authenticator")
|
||||
suite.Contains(contents, `- "it-is-an-eks-parrot"`)
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) TestSetsEKSRoleARN() {
|
||||
suite.initKube.Token = ""
|
||||
suite.initKube.EKSCluster = "it-is-an-eks-parrot"
|
||||
suite.initKube.EKSRoleARN = "arn:aws:iam::19691207:role/mrPraline"
|
||||
contents := suite.generateKubeconfig(Config{})
|
||||
suite.Contains(contents, `- "-r"`)
|
||||
suite.Contains(contents, `- "arn:aws:iam::19691207:role/mrPraline"`)
|
||||
}
|
||||
|
||||
func (suite *KubeconfigTestSuite) generateKubeconfig(cfg Config) string {
|
||||
suite.Require().NoError(suite.initKube.Prepare(cfg))
|
||||
suite.Require().NoError(suite.initKube.Execute(cfg))
|
||||
|
||||
contents, err := ioutil.ReadFile(suite.configFile.Name())
|
||||
suite.Require().NoError(err)
|
||||
|
||||
conf := map[string]interface{}{}
|
||||
suite.NoError(yaml.UnmarshalStrict(contents, &conf))
|
||||
|
||||
return string(contents)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func (u *Uninstall) Prepare(cfg Config) error {
|
||||
return fmt.Errorf("release is required")
|
||||
}
|
||||
|
||||
args := []string{"--kubeconfig", cfg.KubeConfig}
|
||||
args := make([]string, 0)
|
||||
|
||||
if cfg.Namespace != "" {
|
||||
args = append(args, "--namespace", cfg.Namespace)
|
||||
|
||||
@@ -58,11 +58,9 @@ func (suite *UninstallTestSuite) TestPrepareAndExecute() {
|
||||
Run().
|
||||
Times(1)
|
||||
|
||||
cfg := Config{
|
||||
KubeConfig: "/root/.kube/config",
|
||||
}
|
||||
cfg := Config{}
|
||||
suite.NoError(u.Prepare(cfg))
|
||||
expected := []string{"--kubeconfig", "/root/.kube/config", "uninstall", "zayde_wølf_king"}
|
||||
expected := []string{"uninstall", "zayde_wølf_king"}
|
||||
suite.Equal(expected, actual)
|
||||
|
||||
u.Execute(cfg)
|
||||
@@ -73,15 +71,13 @@ func (suite *UninstallTestSuite) TestPrepareDryRunFlag() {
|
||||
Release: "firefox_ak_wildfire",
|
||||
DryRun: true,
|
||||
}
|
||||
cfg := Config{
|
||||
KubeConfig: "/root/.kube/config",
|
||||
}
|
||||
cfg := Config{}
|
||||
|
||||
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
|
||||
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
|
||||
|
||||
suite.NoError(u.Prepare(cfg))
|
||||
expected := []string{"--kubeconfig", "/root/.kube/config", "uninstall", "--dry-run", "firefox_ak_wildfire"}
|
||||
expected := []string{"uninstall", "--dry-run", "firefox_ak_wildfire"}
|
||||
suite.Equal(expected, suite.actualArgs)
|
||||
}
|
||||
|
||||
@@ -90,7 +86,6 @@ func (suite *UninstallTestSuite) TestPrepareNamespaceFlag() {
|
||||
Release: "carly_simon_run_away_with_me",
|
||||
}
|
||||
cfg := Config{
|
||||
KubeConfig: "/root/.kube/config",
|
||||
Namespace: "emotion",
|
||||
}
|
||||
|
||||
@@ -98,8 +93,7 @@ func (suite *UninstallTestSuite) TestPrepareNamespaceFlag() {
|
||||
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
|
||||
|
||||
suite.NoError(u.Prepare(cfg))
|
||||
expected := []string{"--kubeconfig", "/root/.kube/config",
|
||||
"--namespace", "emotion", "uninstall", "carly_simon_run_away_with_me"}
|
||||
expected := []string{"--namespace", "emotion", "uninstall", "carly_simon_run_away_with_me"}
|
||||
suite.Equal(expected, suite.actualArgs)
|
||||
}
|
||||
|
||||
@@ -109,7 +103,6 @@ func (suite *UninstallTestSuite) TestPrepareDebugFlag() {
|
||||
}
|
||||
stderr := strings.Builder{}
|
||||
cfg := Config{
|
||||
KubeConfig: "/root/.kube/config",
|
||||
Debug: true,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
@@ -126,8 +119,8 @@ func (suite *UninstallTestSuite) TestPrepareDebugFlag() {
|
||||
suite.mockCmd.EXPECT().Stderr(&stderr).AnyTimes()
|
||||
|
||||
suite.NoError(u.Prepare(cfg))
|
||||
suite.Equal(fmt.Sprintf("Generated command: '%s --kubeconfig /root/.kube/config "+
|
||||
"--debug uninstall just_a_band_huff_and_puff'\n", helmBin), stderr.String())
|
||||
suite.Equal(fmt.Sprintf("Generated command: '%s --debug "+
|
||||
"uninstall just_a_band_huff_and_puff'\n", helmBin), stderr.String())
|
||||
}
|
||||
|
||||
func (suite *UninstallTestSuite) TestPrepareRequiresRelease() {
|
||||
|
||||
@@ -33,7 +33,7 @@ func (u *Upgrade) Prepare(cfg Config) error {
|
||||
return fmt.Errorf("release is required")
|
||||
}
|
||||
|
||||
args := []string{"--kubeconfig", cfg.KubeConfig}
|
||||
args := make([]string, 0)
|
||||
|
||||
if cfg.Namespace != "" {
|
||||
args = append(args, "--namespace", cfg.Namespace)
|
||||
|
||||
@@ -41,8 +41,7 @@ func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
|
||||
|
||||
command = func(path string, args ...string) cmd {
|
||||
suite.Equal(helmBin, path)
|
||||
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "upgrade", "--install",
|
||||
"jonas_brothers_only_human", "at40"}, args)
|
||||
suite.Equal([]string{"upgrade", "--install", "jonas_brothers_only_human", "at40"}, args)
|
||||
|
||||
return suite.mockCmd
|
||||
}
|
||||
@@ -55,9 +54,7 @@ func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
|
||||
Run().
|
||||
Times(1)
|
||||
|
||||
cfg := Config{
|
||||
KubeConfig: "/root/.kube/config",
|
||||
}
|
||||
cfg := Config{}
|
||||
err := u.Prepare(cfg)
|
||||
suite.Require().Nil(err)
|
||||
u.Execute(cfg)
|
||||
@@ -73,8 +70,7 @@ func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
|
||||
|
||||
command = func(path string, args ...string) cmd {
|
||||
suite.Equal(helmBin, path)
|
||||
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "--namespace", "melt", "upgrade",
|
||||
"--install", "shaed_trampoline", "at40"}, args)
|
||||
suite.Equal([]string{"--namespace", "melt", "upgrade", "--install", "shaed_trampoline", "at40"}, args)
|
||||
|
||||
return suite.mockCmd
|
||||
}
|
||||
@@ -84,7 +80,6 @@ func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
|
||||
|
||||
cfg := Config{
|
||||
Namespace: "melt",
|
||||
KubeConfig: "/root/.kube/config",
|
||||
}
|
||||
err := u.Prepare(cfg)
|
||||
suite.Require().Nil(err)
|
||||
@@ -105,7 +100,6 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
KubeConfig: "/root/.kube/config",
|
||||
Values: "age=35",
|
||||
StringValues: "height=5ft10in",
|
||||
ValuesFiles: []string{"/usr/local/stats", "/usr/local/grades"},
|
||||
@@ -113,7 +107,7 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
|
||||
|
||||
command = func(path string, args ...string) cmd {
|
||||
suite.Equal(helmBin, path)
|
||||
suite.Equal([]string{"--kubeconfig", "/root/.kube/config", "upgrade", "--install",
|
||||
suite.Equal([]string{"upgrade", "--install",
|
||||
"--version", "radio_edit",
|
||||
"--dry-run",
|
||||
"--wait",
|
||||
@@ -166,7 +160,6 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
|
||||
stderr := strings.Builder{}
|
||||
cfg := Config{
|
||||
Debug: true,
|
||||
KubeConfig: "/root/.kube/config",
|
||||
Stdout: &stdout,
|
||||
Stderr: &stderr,
|
||||
}
|
||||
@@ -186,7 +179,7 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
|
||||
|
||||
u.Prepare(cfg)
|
||||
|
||||
want := fmt.Sprintf("Generated command: '%s --kubeconfig /root/.kube/config --debug upgrade "+
|
||||
want := fmt.Sprintf("Generated command: '%s --debug upgrade "+
|
||||
"--install lewis_capaldi_someone_you_loved at40'\n", helmBin)
|
||||
suite.Equal(want, stderr.String())
|
||||
suite.Equal("", stdout.String())
|
||||
|
||||
Reference in New Issue
Block a user