25 Commits

Author SHA1 Message Date
Erin Call
cfc59a46ef Merge pull request #102 from minhdanh/skip-kube-init
Support skipping kubeconfig creation
2020-08-25 22:20:05 -07:00
minhdanh
5b4e3ab2ea Rename kube_init_skip to skip_kubeconfig 2020-08-25 09:59:06 +07:00
minhdanh
0e58dde591 Support skipping kubeconfig creation 2020-08-24 13:03:36 +07:00
Joachim Hill-Grannec
03b066bfd7 Merge pull request #94 from pelotech/no-binary
Remove the built binary
2020-05-22 17:24:15 -07:00
Erin Call
fe0c874815 Merge branch 'master' into no-binary 2020-05-22 17:17:18 -07:00
Erin Call
91d4506b37 Ignore build/* [#93]
I definitely thought this rule was already in effect--I had a
`build/.gitignore` that contained just `*`. Unfortunately, that makes
git ignore the .gitignore file itself, so it was never added to the
repo...
2020-05-22 17:15:24 -07:00
Joachim Hill-Grannec
0ab0a7fee4 Merge pull request #96 from pelotech/explain-setting-aliases
Document the purpose of settingAliases
2020-05-22 17:04:28 -07:00
Erin Call
0b17d929a7 Document the purpose of settingAliases [#95] 2020-05-22 15:35:43 -07:00
Erin Call
3fea592040 Remove the built binary [#93]
It was never supposed to be part of the repo, and currently accounts for
at least half of the .git directory's size. We need to nip it in the bud
before `git clone` times become unmanageable!
2020-05-22 14:43:33 -07:00
Joachim Hill-Grannec
d7a9eccb13 Merge pull request #86 from obukhov/add-dependecies-action
Add dependecies action
2020-04-07 14:06:48 -06:00
Alex Obukhov
9e3a424a36 Add configuration option to docs 2020-04-05 14:30:44 +02:00
Alex Obukhov
774bbb74db Add dependencies_action configuration option 2020-04-05 14:22:46 +02:00
Joachim Hill-Grannec
e0157d9bc2 Merge pull request #84 from BnMcG/noissue-repos-environment-interpolation
Allow environment variable interpolation in Helm repository configuration
2020-03-31 22:29:47 -06:00
Ben Magee
1f2da68bbb Remove built binary 2020-03-31 23:39:02 +01:00
Ben Magee
7e2f982af7 Update docs 2020-03-31 23:33:54 +01:00
Ben Magee
0502d76c63 Interpolate environment variables in the AddRepos configuration parameter 2020-03-31 23:07:28 +01:00
Joachim Hill-Grannec
591b084970 upgrading image to helm 3.1.1 2020-03-06 13:25:35 -06:00
Erin Call
f24a8e44ca Merge pull request #79 from pelotech/versions-in-bugs
Ask for version information in the bug template
2020-01-22 11:24:10 -08:00
Erin Call
8cb8a5d95d Merge branch 'master' into versions-in-bugs 2020-01-22 11:21:37 -08:00
Erin Call
a4834dd4f7 Merge pull request #77 from pelotech/interpolate-secrets
Interpolate environment variables into cfg.Values and cfg.StringValues
2020-01-22 11:18:17 -08:00
Erin Call
8b6a8fdd4b Ask for version information in the bug template [#78] 2020-01-22 10:06:57 -08:00
Erin Call
dbcef2699e Avoid polluted-env problems in config tests [#34]
I mean...it's *possible* someone will have SECRET_WATER set in their
env, right? Might as well be paranoid; it doesn't cost much.
2020-01-21 16:25:58 -08:00
Erin Call
22aa1df894 Don't bother trying to hide secrets in values [#34]
While testing this I discovered the secrets are revealed anyway, since
the lint/upgrade jobs' debug output includes the command they generated.
Might as well make the code a little simpler.
2020-01-21 16:23:55 -08:00
Erin Call
8f7b481934 Log debug information in loadValuesSecrets [#34] 2020-01-21 16:04:05 -08:00
Erin Call
e843b26759 Expand env vars in Values/StringValues [#34] 2020-01-21 15:46:32 -08:00
10 changed files with 330 additions and 16 deletions

View File

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

3
.gitignore vendored
View File

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

View File

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

View File

@@ -31,12 +31,14 @@ Installations are triggered when the `mode` setting is "upgrade." They can also
|------------------------|----------------|----------|------------------------|---------|
| 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. |
| skip_kubeconfig | boolean | | | Whether to skip kubeconfig file creation. |
| kube_api_server | string | yes | api_server | API endpoint for the Kubernetes cluster. This is ignored if `skip_kubeconfig` is `true`. |
| kube_token | string | yes | kubernetes_token | Token for authenticating to Kubernetes. This is ignored if `skip_kubeconfig` is `true`. |
| kube_service_account | string | | service_account | Service account for authenticating to Kubernetes. Default is `helm`. This is ignored if `skip_kubeconfig` is `true`. |
| kube_certificate | string | | kubernetes_certificate | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. This is ignored if `skip_kubeconfig` is `true`. |
| chart_version | string | | | Specific chart version to install. |
| dry_run | boolean | | | Pass `--dry-run` to `helm upgrade`. |
| dependencies_action | string | | | Calls `helm dependency build` OR `helm dependency update` before running the main command. Possible values: `build`, `update`. |
| 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`. |
@@ -46,7 +48,7 @@ Installations are triggered when the `mode` setting is "upgrade." They can also
| 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. |
| skip_tls_verify | boolean | | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. This is ignored if `skip_kubeconfig` is `true`. |
## Uninstallation
@@ -55,14 +57,15 @@ Uninstallations are triggered when the `mode` setting is "uninstall" or "delete.
| 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. |
| skip_kubeconfig | boolean | | | Whether to skip kubeconfig file creation. |
| kube_api_server | string | yes | api_server | API endpoint for the Kubernetes cluster. This is ignored if `skip_kubeconfig` is `true`. |
| kube_token | string | yes | kubernetes_token | Token for authenticating to Kubernetes. This is ignored if `skip_kubeconfig` is `true`. |
| kube_service_account | string | | service_account | Service account for authenticating to Kubernetes. Default is `helm`. This is ignored if `skip_kubeconfig` is `true`. |
| kube_certificate | string | | kubernetes_certificate | Base64 encoded TLS certificate used by the Kubernetes cluster's certificate authority. This is ignored if `skip_kubeconfig` is `true`. |
| keep_history | boolean | | | Pass `--keep-history` to `helm uninstall`, to retain the release history. |
| 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. |
| skip_tls_verify | boolean | | | Connect to the Kubernetes cluster without checking for a valid TLS certificate. Not recommended in production. This is ignored if `skip_kubeconfig` is `true`. |
| chart | string | | | Required when the global `update_dependencies` parameter is true. No effect otherwise. |
### Where to put settings
@@ -95,6 +98,26 @@ values_files: [ "./over_9,000.yml" ]
values_files: [ "./over_9", "000.yml" ]
```
### Interpolating secrets into the `values`, `string_values` and `add_repos` settings
If you want to send secrets to your charts, you can use syntax similar to shell variable interpolation--either `$VARNAME` or `$${VARNAME}`. The double dollar-sign is necessary when using curly brackets; using curly brackets with a single dollar-sign will trigger Drone's string substitution (which can't use arbitrary environment variables). If an environment variable is not set, it will be treated as if it were set to the empty string.
```yaml
environment:
DB_PASSWORD:
from_secret: db_password
SESSION_KEY:
from_secret: session_key
settings:
values:
- db_password=$DB_PASSWORD # db_password will be set to the contents of the db_password secret
- db_pass=$DB_PASS # db_pass will be set to "" since $DB_PASS is not set
- session_key=$${SESSION_KEY} # session_key will be set to the contents of the session_key secret
- sess_key=${SESSION_KEY} # sess_key will be set to "" by Drone's variable substitution
```
Variables intended for interpolation must be set in the `environment` section, not `settings`.
### Backward-compatibility aliases
Some settings have alternate names, for backward-compatibility with drone-helm. We recommend using the canonical name unless you require the backward-compatible form.

View File

@@ -2,11 +2,12 @@ package env
import (
"fmt"
"github.com/kelseyhightower/envconfig"
"io"
"os"
"regexp"
"strings"
"github.com/kelseyhightower/envconfig"
)
var (
@@ -22,7 +23,8 @@ 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
UpdateDependencies bool `split_words:"true"` // [Deprecated] Call `helm dependency update` before the main command (deprecated, use dependencies_action: update instead)
DependenciesAction string `split_words:"true"` // Call `helm dependency build` or `helm dependency update` before the main command
AddRepos []string `split_words:"true"` // Call `helm repo add` before the main command
RepoCertificate string `envconfig:"repo_certificate"` // The Helm chart repository's self-signed certificate (must be base64-encoded)
RepoCACertificate string `envconfig:"repo_ca_certificate"` // The Helm chart repository CA's self-signed certificate (must be base64-encoded)
@@ -32,6 +34,7 @@ type Config struct {
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
SkipKubeconfig bool `envconfig:"skip_kubeconfig"` // Skip kubeconfig creation
SkipTLSVerify bool `envconfig:"skip_tls_verify"` // Put insecure-skip-tls-verify in .kube/config
Certificate string `envconfig:"kube_certificate"` // The Kubernetes cluster CA's self-signed certificate (must be base64-encoded)
APIServer string `envconfig:"kube_api_server"` // The Kubernetes cluster's API endpoint
@@ -85,10 +88,18 @@ func NewConfig(stdout, stderr io.Writer) (*Config, error) {
return nil, err
}
if cfg.SkipKubeconfig {
if cfg.KubeToken != "" || cfg.Certificate != "" || cfg.APIServer != "" || cfg.ServiceAccount != "" || cfg.SkipTLSVerify {
fmt.Fprintf(cfg.Stderr, "Warning: skip_kubeconfig is set. The following kubeconfig-related settings will be ignored: kube_config, kube_certificate, kube_api_server, kube_service_account, skip_tls_verify.")
}
}
if justNumbers.MatchString(cfg.Timeout) {
cfg.Timeout = fmt.Sprintf("%ss", cfg.Timeout)
}
cfg.loadValuesSecrets()
if cfg.Debug && cfg.Stderr != nil {
cfg.logDebug()
}
@@ -98,6 +109,31 @@ func NewConfig(stdout, stderr io.Writer) (*Config, error) {
return &cfg, nil
}
func (cfg *Config) loadValuesSecrets() {
findVar := regexp.MustCompile(`\$\{?(\w+)\}?`)
replacer := func(varName string) string {
sigils := regexp.MustCompile(`[${}]`)
varName = sigils.ReplaceAllString(varName, "")
if value, ok := os.LookupEnv(varName); ok {
return value
}
if cfg.Debug {
fmt.Fprintf(cfg.Stderr, "$%s not present in environment, replaced with \"\"\n", varName)
}
return ""
}
cfg.Values = findVar.ReplaceAllStringFunc(cfg.Values, replacer)
cfg.StringValues = findVar.ReplaceAllStringFunc(cfg.StringValues, replacer)
for i := 0; i < len(cfg.AddRepos); i++ {
cfg.AddRepos[i] = findVar.ReplaceAllStringFunc(cfg.AddRepos[i], replacer)
}
}
func (cfg Config) logDebug() {
if cfg.KubeToken != "" {
cfg.KubeToken = "(redacted)"
@@ -115,6 +151,10 @@ func (cfg *Config) deprecationWarn() {
}
}
// settingAliases provides alternate environment variable names for certain settings, either because
// they were renamed during drone-helm3's lifetime or for backward-compatibility with the original
// drone-helm. Most config options don't need to be included here; adding them to the main Config
// struct is sufficient.
type settingAliases struct {
Command string `envconfig:"helm_command"`
AddRepos []string `envconfig:"helm_repos"`

View File

@@ -2,10 +2,11 @@ package env
import (
"fmt"
"github.com/stretchr/testify/suite"
"os"
"strings"
"testing"
"github.com/stretchr/testify/suite"
)
type ConfigTestSuite struct {
@@ -183,6 +184,39 @@ func (suite *ConfigTestSuite) TestLogDebugCensorsKubeToken() {
suite.Equal(kubeToken, cfg.KubeToken) // The actual config value should be left unchanged
}
func (suite *ConfigTestSuite) TestNewConfigWithValuesSecrets() {
suite.unsetenv("VALUES")
suite.unsetenv("STRING_VALUES")
suite.unsetenv("SECRET_WATER")
suite.setenv("SECRET_FIRE", "Eru_Ilúvatar")
suite.setenv("SECRET_RINGS", "1")
suite.setenv("PLUGIN_VALUES", "fire=$SECRET_FIRE,water=${SECRET_WATER}")
suite.setenv("PLUGIN_STRING_VALUES", "rings=${SECRET_RINGS}")
suite.setenv("PLUGIN_ADD_REPOS", "testrepo=https://user:${SECRET_FIRE}@testrepo.test")
cfg, err := NewConfig(&strings.Builder{}, &strings.Builder{})
suite.Require().NoError(err)
suite.Equal("fire=Eru_Ilúvatar,water=", cfg.Values)
suite.Equal("rings=1", cfg.StringValues)
suite.Equal(fmt.Sprintf("testrepo=https://user:%s@testrepo.test", os.Getenv("SECRET_FIRE")), cfg.AddRepos[0])
}
func (suite *ConfigTestSuite) TestValuesSecretsWithDebugLogging() {
suite.unsetenv("VALUES")
suite.unsetenv("SECRET_WATER")
suite.setenv("SECRET_FIRE", "Eru_Ilúvatar")
suite.setenv("PLUGIN_DEBUG", "true")
suite.setenv("PLUGIN_STRING_VALUES", "fire=$SECRET_FIRE")
suite.setenv("PLUGIN_VALUES", "fire=$SECRET_FIRE,water=$SECRET_WATER")
stderr := strings.Builder{}
_, err := NewConfig(&strings.Builder{}, &stderr)
suite.Require().NoError(err)
suite.Contains(stderr.String(), "Values:fire=Eru_Ilúvatar,water=")
suite.Contains(stderr.String(), `$SECRET_WATER not present in environment, replaced with ""`)
}
func (suite *ConfigTestSuite) setenv(key, val string) {
orig, ok := os.LookupEnv(key)
if ok {

View File

@@ -1,6 +1,7 @@
package helm
import (
"errors"
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/pelotech/drone-helm3/internal/run"
@@ -30,6 +31,10 @@ func NewPlan(cfg env.Config) (*Plan, error) {
cfg: cfg,
}
if cfg.UpdateDependencies && cfg.DependenciesAction != "" {
return nil, errors.New("update_dependencies is deprecated and cannot be provided together with dependencies_action")
}
p.steps = (*determineSteps(cfg))(cfg)
for i, step := range p.steps {
@@ -87,13 +92,21 @@ func (p *Plan) Execute() error {
var upgrade = func(cfg env.Config) []Step {
var steps []Step
if !cfg.SkipKubeconfig {
steps = append(steps, run.NewInitKube(cfg, kubeConfigTemplate, kubeConfigFile))
}
for _, repo := range cfg.AddRepos {
steps = append(steps, run.NewAddRepo(cfg, repo))
}
if cfg.DependenciesAction != "" {
steps = append(steps, run.NewDepAction(cfg))
}
if cfg.UpdateDependencies {
steps = append(steps, run.NewDepUpdate(cfg))
}
steps = append(steps, run.NewUpgrade(cfg))
return steps
@@ -101,7 +114,9 @@ var upgrade = func(cfg env.Config) []Step {
var uninstall = func(cfg env.Config) []Step {
var steps []Step
if !cfg.SkipKubeconfig {
steps = append(steps, run.NewInitKube(cfg, kubeConfigTemplate, kubeConfigFile))
}
if cfg.UpdateDependencies {
steps = append(steps, run.NewDepUpdate(cfg))
}

View File

@@ -122,6 +122,12 @@ func (suite *PlanTestSuite) TestUpgrade() {
suite.IsType(&run.Upgrade{}, steps[1])
}
func (suite *PlanTestSuite) TestUpgradeWithSkipKubeconfig() {
steps := upgrade(env.Config{SkipKubeconfig: true})
suite.Require().Equal(1, len(steps), "upgrade should return 1 step")
suite.IsType(&run.Upgrade{}, steps[0])
}
func (suite *PlanTestSuite) TestUpgradeWithUpdateDependencies() {
cfg := env.Config{
UpdateDependencies: true,

59
internal/run/depaction.go Normal file
View File

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

View File

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