29 Commits

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

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

Also clarified the config/template filename fields (configFile vs.
ConfigFile was particularly ambiguous).
2020-01-16 15:12:15 -08:00
Erin Call
21b9d32329 Remove the tiny helper functions from plan.go [#67]
Now that InitKube, AddRepo, and UpdateDependencies are initialized with
NewSTEPNAME functions, the helper functions in plan.go are
unnecessary--they do too little to be a useful abstraction, and they
aren't complex or frequently-used enough to be worth extracting.
2020-01-16 13:57:28 -08:00
Erin Call
588c7cb9f7 Initialize Steps with a NewSTEPNAME function [#67]
This seems to be be a more natural separation of concerns--the knowledge
of which config fields map to which parts of a Step belong to the Step,
not to the Plan.
2020-01-16 13:50:04 -08:00
Erin Call
16117eea2f Put the Config in a new env package [#67]
I'd like to be able to make calls like NewUpgrade(cfg) rather than
Upgrade{...}.Prepare, but I wouldn't be able to define a NewUpgrade
function while Config is in the helm package; there would be a circular
import when Plan tried to import run.
2020-01-14 10:32:20 -08:00
27 changed files with 943 additions and 888 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 -->

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

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

View File

@@ -6,6 +6,8 @@
| 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_certificate | string | | Base64 encoded TLS certificate for a chart repository. |
| repo_ca_certificate | string | | Base64 encoded 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. |
@@ -93,6 +95,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

@@ -1,12 +1,13 @@
package helm
package env
import (
"fmt"
"github.com/kelseyhightower/envconfig"
"io"
"os"
"regexp"
"strings"
"github.com/kelseyhightower/envconfig"
)
var (
@@ -21,16 +22,18 @@ var (
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.
DroneEvent string `envconfig:"drone_build_event"` // Drone event that invoked this plugin.
UpdateDependencies bool `split_words:"true"` // Call `helm dependency update` before the main command
AddRepos []string `split_words:"true"` // Call `helm repo add` before the main command
RepoCertificate string `envconfig:"repo_certificate"` // The Helm chart repository's self-signed certificate (must be base64-encoded)
RepoCACertificate string `envconfig:"repo_ca_certificate"` // The Helm chart repository CA's self-signed certificate (must be base64-encoded)
Debug bool `` // Generate debug output and pass --debug to all helm commands
Values string `` // Argument to pass to --set in applicable helm commands
StringValues string `split_words:"true"` // Argument to pass to --set-string in applicable helm commands
ValuesFiles []string `split_words:"true"` // Arguments to pass to --values in applicable helm commands
Namespace string `` // Kubernetes namespace for all helm commands
KubeToken string `split_words:"true"` // Kubernetes authentication token to put in .kube/config
SkipTLSVerify bool `envconfig:"SKIP_TLS_VERIFY"` // Put insecure-skip-tls-verify in .kube/config
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
@@ -44,7 +47,7 @@ type Config struct {
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`
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"`
@@ -87,6 +90,8 @@ func NewConfig(stdout, stderr io.Writer) (*Config, error) {
cfg.Timeout = fmt.Sprintf("%ss", cfg.Timeout)
}
cfg.loadValuesSecrets()
if cfg.Debug && cfg.Stderr != nil {
cfg.logDebug()
}
@@ -96,6 +101,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)"

View File

@@ -1,11 +1,12 @@
package helm
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

@@ -6,7 +6,6 @@ package helm
import (
gomock "github.com/golang/mock/gomock"
run "github.com/pelotech/drone-helm3/internal/run"
reflect "reflect"
)
@@ -34,29 +33,29 @@ func (m *MockStep) EXPECT() *MockStepMockRecorder {
}
// Prepare mocks base method
func (m *MockStep) Prepare(arg0 run.Config) error {
func (m *MockStep) Prepare() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Prepare", arg0)
ret := m.ctrl.Call(m, "Prepare")
ret0, _ := ret[0].(error)
return ret0
}
// 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()
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
func (m *MockStep) Execute(arg0 run.Config) error {
func (m *MockStep) Execute() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Execute", arg0)
ret := m.ctrl.Call(m, "Execute")
ret0, _ := ret[0].(error)
return ret0
}
// 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()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockStep)(nil).Execute), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockStep)(nil).Execute))
}

View File

@@ -2,6 +2,7 @@ package helm
import (
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/pelotech/drone-helm3/internal/run"
"os"
)
@@ -13,27 +14,20 @@ const (
// A Step is one step in the plan.
type Step interface {
Prepare(run.Config) error
Execute(run.Config) error
Prepare() error
Execute() error
}
// A Plan is a series of steps to perform.
type Plan struct {
steps []Step
cfg Config
runCfg run.Config
cfg env.Config
}
// NewPlan makes a plan for running a helm operation.
func NewPlan(cfg Config) (*Plan, error) {
func NewPlan(cfg env.Config) (*Plan, error) {
p := Plan{
cfg: cfg,
runCfg: run.Config{
Debug: cfg.Debug,
Namespace: cfg.Namespace,
Stdout: cfg.Stdout,
Stderr: cfg.Stderr,
},
}
p.steps = (*determineSteps(cfg))(cfg)
@@ -43,7 +37,7 @@ func NewPlan(cfg Config) (*Plan, error) {
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)
return nil, err
}
@@ -54,7 +48,7 @@ func NewPlan(cfg Config) (*Plan, error) {
// 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.
func determineSteps(cfg Config) *func(Config) []Step {
func determineSteps(cfg env.Config) *func(env.Config) []Step {
switch cfg.Command {
case "upgrade":
return &upgrade
@@ -83,7 +77,7 @@ func (p *Plan) Execute() error {
fmt.Fprintf(p.cfg.Stderr, "calling %T.Execute (step %d)\n", step, i)
}
if err := step.Execute(p.runCfg); err != nil {
if err := step.Execute(); err != nil {
return fmt.Errorf("while executing %T step: %w", step, err)
}
}
@@ -91,97 +85,43 @@ func (p *Plan) Execute() error {
return nil
}
var upgrade = func(cfg Config) []Step {
steps := initKube(cfg)
steps = append(steps, addRepos(cfg)...)
if cfg.UpdateDependencies {
steps = append(steps, depUpdate(cfg)...)
}
steps = append(steps, &run.Upgrade{
Chart: cfg.Chart,
Release: cfg.Release,
ChartVersion: cfg.ChartVersion,
DryRun: cfg.DryRun,
Wait: cfg.Wait,
Values: cfg.Values,
StringValues: cfg.StringValues,
ValuesFiles: cfg.ValuesFiles,
ReuseValues: cfg.ReuseValues,
Timeout: cfg.Timeout,
Force: cfg.Force,
Atomic: cfg.AtomicUpgrade,
CleanupOnFail: cfg.CleanupOnFail,
})
return steps
}
var uninstall = func(cfg Config) []Step {
steps := initKube(cfg)
if cfg.UpdateDependencies {
steps = append(steps, depUpdate(cfg)...)
}
steps = append(steps, &run.Uninstall{
Release: cfg.Release,
DryRun: cfg.DryRun,
KeepHistory: cfg.KeepHistory,
})
return steps
}
var lint = func(cfg Config) []Step {
steps := addRepos(cfg)
if cfg.UpdateDependencies {
steps = append(steps, depUpdate(cfg)...)
}
steps = append(steps, &run.Lint{
Chart: cfg.Chart,
Values: cfg.Values,
StringValues: cfg.StringValues,
ValuesFiles: cfg.ValuesFiles,
Strict: cfg.LintStrictly,
})
return steps
}
var help = func(cfg Config) []Step {
help := &run.Help{
HelmCommand: cfg.Command,
}
return []Step{help}
}
func initKube(cfg Config) []Step {
return []Step{
&run.InitKube{
SkipTLSVerify: cfg.SkipTLSVerify,
Certificate: cfg.Certificate,
APIServer: cfg.APIServer,
ServiceAccount: cfg.ServiceAccount,
Token: cfg.KubeToken,
TemplateFile: kubeConfigTemplate,
ConfigFile: kubeConfigFile,
},
}
}
func addRepos(cfg Config) []Step {
steps := make([]Step, 0)
var upgrade = func(cfg env.Config) []Step {
var steps []Step
steps = append(steps, run.NewInitKube(cfg, kubeConfigTemplate, kubeConfigFile))
for _, repo := range cfg.AddRepos {
steps = append(steps, &run.AddRepo{
Repo: repo,
})
steps = append(steps, run.NewAddRepo(cfg, repo))
}
if cfg.UpdateDependencies {
steps = append(steps, run.NewDepUpdate(cfg))
}
steps = append(steps, run.NewUpgrade(cfg))
return steps
}
func depUpdate(cfg Config) []Step {
return []Step{
&run.DepUpdate{
Chart: cfg.Chart,
},
var uninstall = func(cfg env.Config) []Step {
var steps []Step
steps = append(steps, run.NewInitKube(cfg, kubeConfigTemplate, kubeConfigFile))
if cfg.UpdateDependencies {
steps = append(steps, run.NewDepUpdate(cfg))
}
steps = append(steps, run.NewUninstall(cfg))
return steps
}
var lint = func(cfg env.Config) []Step {
var steps []Step
for _, repo := range cfg.AddRepos {
steps = append(steps, run.NewAddRepo(cfg, repo))
}
if cfg.UpdateDependencies {
steps = append(steps, run.NewDepUpdate(cfg))
}
steps = append(steps, run.NewLint(cfg))
return steps
}
var help = func(cfg env.Config) []Step {
return []Step{run.NewHelp(cfg)}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,39 +2,47 @@ package run
import (
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
)
// Help is a step in a helm Plan that calls `helm help`.
type Help struct {
HelmCommand string
*config
helmCommand string
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.
func (h *Help) Execute(cfg Config) error {
func (h *Help) Execute() error {
if err := h.cmd.Run(); err != nil {
return fmt.Errorf("while running '%s': %w", h.cmd.String(), err)
}
if h.HelmCommand == "help" {
if h.helmCommand == "help" {
return nil
}
return fmt.Errorf("unknown command '%s'", h.HelmCommand)
return fmt.Errorf("unknown command '%s'", h.helmCommand)
}
// Prepare gets the Help ready to execute.
func (h *Help) Prepare(cfg Config) error {
args := []string{"help"}
if cfg.Debug {
args = append([]string{"--debug"}, args...)
}
func (h *Help) Prepare() error {
args := h.globalFlags()
args = append(args, "help")
h.cmd = command(helmBin, args...)
h.cmd.Stdout(cfg.Stdout)
h.cmd.Stderr(cfg.Stderr)
h.cmd.Stdout(h.stdout)
h.cmd.Stderr(h.stderr)
if cfg.Debug {
fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", h.cmd.String())
if h.debug {
fmt.Fprintf(h.stderr, "Generated command: '%s'\n", h.cmd.String())
}
return nil

View File

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

View File

@@ -3,6 +3,7 @@ package run
import (
"errors"
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
"io"
"os"
"text/template"
@@ -10,14 +11,9 @@ import (
// InitKube is a step in a helm Plan that initializes the kubernetes config file.
type InitKube struct {
SkipTLSVerify bool
Certificate string
APIServer string
ServiceAccount string
Token string
TemplateFile string
ConfigFile string
*config
templateFilename string
configFilename string
template *template.Template
configFile io.WriteCloser
values kubeValues
@@ -32,58 +28,66 @@ type kubeValues struct {
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.
func (i *InitKube) Execute(cfg Config) error {
if cfg.Debug {
fmt.Fprintf(cfg.Stderr, "writing kubeconfig file to %s\n", i.ConfigFile)
func (i *InitKube) Execute() error {
if i.debug {
fmt.Fprintf(i.stderr, "writing kubeconfig file to %s\n", i.configFilename)
}
defer i.configFile.Close()
return i.template.Execute(i.configFile, i.values)
}
// 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
if i.APIServer == "" {
if i.values.APIServer == "" {
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")
}
if i.ServiceAccount == "" {
i.ServiceAccount = "helm"
if i.values.ServiceAccount == "" {
i.values.ServiceAccount = "helm"
}
if cfg.Debug {
fmt.Fprintf(cfg.Stderr, "loading kubeconfig template from %s\n", i.TemplateFile)
if i.debug {
fmt.Fprintf(i.stderr, "loading kubeconfig template from %s\n", i.templateFilename)
}
i.template, err = template.ParseFiles(i.TemplateFile)
i.template, err = template.ParseFiles(i.templateFilename)
if err != nil {
return fmt.Errorf("could not load kubeconfig template: %w", err)
}
i.values = kubeValues{
SkipTLSVerify: i.SkipTLSVerify,
Certificate: i.Certificate,
APIServer: i.APIServer,
ServiceAccount: i.ServiceAccount,
Token: i.Token,
Namespace: cfg.Namespace,
}
if cfg.Debug {
if _, err := os.Stat(i.ConfigFile); err != nil {
if i.debug {
if _, err := os.Stat(i.configFilename); err != nil {
// 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 {
fmt.Fprint(cfg.Stderr, "truncating ")
fmt.Fprint(i.stderr, "truncating ")
}
fmt.Fprintf(cfg.Stderr, "kubeconfig file at %s\n", i.ConfigFile)
fmt.Fprintf(i.stderr, "kubeconfig file at %s\n", i.configFilename)
}
i.configFile, err = os.Create(i.ConfigFile)
i.configFile, err = os.Create(i.configFilename)
if err != nil {
return fmt.Errorf("could not open kubeconfig file for writing: %w", err)
}

View File

@@ -1,10 +1,13 @@
package run
import (
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite"
yaml "gopkg.in/yaml.v2"
"io/ioutil"
"os"
"strings"
"testing"
"text/template"
)
@@ -17,6 +20,30 @@ func TestInitKubeTestSuite(t *testing.T) {
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() {
templateFile, err := tempfile("kubeconfig********.yml.tpl", `
certificate: {{ .Certificate }}
@@ -29,23 +56,20 @@ namespace: {{ .Namespace }}
defer os.Remove(configFile.Name())
suite.Require().Nil(err)
init := InitKube{
cfg := env.Config{
APIServer: "Sysadmin",
Certificate: "CCNA",
Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
}
cfg := Config{
KubeToken: "Aspire virtual currency",
Namespace: "Cisco",
}
err = init.Prepare(cfg)
init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
err = init.Prepare()
suite.Require().Nil(err)
suite.IsType(&template.Template{}, init.template)
suite.NotNil(init.configFile)
err = init.Execute(cfg)
err = init.Execute()
suite.Require().Nil(err)
conf, err := ioutil.ReadFile(configFile.Name())
@@ -63,19 +87,16 @@ func (suite *InitKubeTestSuite) TestExecuteGeneratesConfig() {
defer os.Remove(configFile.Name())
suite.Require().NoError(err)
cfg := Config{
Namespace: "marshmallow",
}
init := InitKube{
ConfigFile: configFile.Name(),
TemplateFile: "../../assets/kubeconfig.tpl", // the actual kubeconfig template
cfg := env.Config{
APIServer: "https://kube.cluster/peanut",
ServiceAccount: "chef",
Token: "eWVhaCB3ZSB0b2tpbic=",
KubeToken: "eWVhaCB3ZSB0b2tpbic=",
Certificate: "d293LCB5b3UgYXJlIHNvIGNvb2wgZm9yIHNtb2tpbmcgd2VlZCDwn5mE",
Namespace: "marshmallow",
}
suite.Require().NoError(init.Prepare(cfg))
suite.Require().NoError(init.Execute(cfg))
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)
@@ -98,11 +119,11 @@ func (suite *InitKubeTestSuite) TestExecuteGeneratesConfig() {
suite.NoError(yaml.UnmarshalStrict(contents, &conf))
// test the other branch of the certificate/SkipTLSVerify conditional
init.SkipTLSVerify = true
init.Certificate = ""
init.values.SkipTLSVerify = true
init.values.Certificate = ""
suite.Require().NoError(init.Prepare(cfg))
suite.Require().NoError(init.Execute(cfg))
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")
@@ -116,25 +137,25 @@ func (suite *InitKubeTestSuite) TestPrepareParseError() {
defer os.Remove(templateFile.Name())
suite.Require().Nil(err)
init := InitKube{
cfg := env.Config{
APIServer: "Sysadmin",
Certificate: "CCNA",
Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
KubeToken: "Aspire virtual currency",
}
err = init.Prepare(Config{})
init := NewInitKube(cfg, templateFile.Name(), "")
err = init.Prepare()
suite.Error(err)
suite.Regexp("could not load kubeconfig .* function .* not defined", err)
}
func (suite *InitKubeTestSuite) TestPrepareNonexistentTemplateFile() {
init := InitKube{
cfg := env.Config{
APIServer: "Sysadmin",
Certificate: "CCNA",
Token: "Aspire virtual currency",
TemplateFile: "/usr/foreign/exclude/kubeprofig.tpl",
KubeToken: "Aspire virtual currency",
}
err := init.Prepare(Config{})
init := NewInitKube(cfg, "/usr/foreign/exclude/kubeprofig.tpl", "")
err := init.Prepare()
suite.Error(err)
suite.Regexp("could not load kubeconfig .* no such file or directory", err)
}
@@ -143,16 +164,14 @@ func (suite *InitKubeTestSuite) TestPrepareCannotOpenDestinationFile() {
templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity")
defer os.Remove(templateFile.Name())
suite.Require().Nil(err)
init := InitKube{
cfg := env.Config{
APIServer: "Sysadmin",
Certificate: "CCNA",
Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: "/usr/foreign/exclude/kubeprofig",
KubeToken: "Aspire virtual currency",
}
init := NewInitKube(cfg, templateFile.Name(), "/usr/foreign/exclude/kubeprofig")
cfg := Config{}
err = init.Prepare(cfg)
err = init.Prepare()
suite.Error(err)
suite.Regexp("could not open .* for writing: .* no such file or directory", err)
}
@@ -167,24 +186,21 @@ func (suite *InitKubeTestSuite) TestPrepareRequiredConfig() {
suite.Require().Nil(err)
// initial config with all required fields present
init := InitKube{
cfg := env.Config{
APIServer: "Sysadmin",
Certificate: "CCNA",
Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
KubeToken: "Aspire virtual currency",
}
cfg := Config{}
init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
suite.NoError(init.Prepare()) // consistency check; we should be starting in a happy state
suite.NoError(init.Prepare(cfg)) // consistency check; we should be starting in a happy state
init.values.APIServer = ""
suite.Error(init.Prepare(), "APIServer should be required.")
init.APIServer = ""
suite.Error(init.Prepare(cfg), "APIServer should be required.")
init.APIServer = "Sysadmin"
init.Token = ""
suite.Error(init.Prepare(cfg), "Token should be required.")
init.values.APIServer = "Sysadmin"
init.values.Token = ""
suite.Error(init.Prepare(), "Token should be required.")
}
func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
@@ -196,18 +212,43 @@ func (suite *InitKubeTestSuite) TestPrepareDefaultsServiceAccount() {
defer os.Remove(configFile.Name())
suite.Require().Nil(err)
init := InitKube{
cfg := env.Config{
APIServer: "Sysadmin",
Certificate: "CCNA",
Token: "Aspire virtual currency",
TemplateFile: templateFile.Name(),
ConfigFile: configFile.Name(),
KubeToken: "Aspire virtual currency",
}
init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
init.Prepare()
suite.Equal("helm", init.values.ServiceAccount)
}
cfg := Config{}
func (suite *InitKubeTestSuite) TestDebugOutput() {
templateFile, err := tempfile("kubeconfig********.yml.tpl", "hurgity burgity")
defer os.Remove(templateFile.Name())
suite.Require().Nil(err)
init.Prepare(cfg)
suite.Equal("helm", init.ServiceAccount)
configFile, err := tempfile("kubeconfig********.yml", "")
defer os.Remove(configFile.Name())
suite.Require().Nil(err)
stdout := &strings.Builder{}
stderr := &strings.Builder{}
cfg := env.Config{
APIServer: "http://my.kube.server/",
KubeToken: "QSBzaW5nbGUgcm9zZQ==",
Debug: true,
Stdout: stdout,
Stderr: stderr,
}
init := NewInitKube(cfg, templateFile.Name(), configFile.Name())
suite.NoError(init.Prepare())
suite.Contains(stderr.String(), fmt.Sprintf("loading kubeconfig template from %s\n", templateFile.Name()))
suite.Contains(stderr.String(), fmt.Sprintf("truncating kubeconfig file at %s\n", configFile.Name()))
suite.NoError(init.Execute())
suite.Contains(stderr.String(), fmt.Sprintf("writing kubeconfig file to %s\n", configFile.Name()))
}
func tempfile(name, contents string) (*os.File, error) {

View File

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

View File

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

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

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

View File

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

View File

@@ -2,53 +2,57 @@ package run
import (
"fmt"
"github.com/pelotech/drone-helm3/internal/env"
)
// Uninstall is an execution step that calls `helm uninstall` when executed.
type Uninstall struct {
Release string
DryRun bool
KeepHistory bool
*config
release string
dryRun bool
keepHistory bool
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.
func (u *Uninstall) Execute(_ Config) error {
func (u *Uninstall) Execute() error {
return u.cmd.Run()
}
// Prepare gets the Uninstall ready to execute.
func (u *Uninstall) Prepare(cfg Config) error {
if u.Release == "" {
func (u *Uninstall) Prepare() error {
if u.release == "" {
return fmt.Errorf("release is required")
}
args := make([]string, 0)
if cfg.Namespace != "" {
args = append(args, "--namespace", cfg.Namespace)
}
if cfg.Debug {
args = append(args, "--debug")
}
args := u.globalFlags()
args = append(args, "uninstall")
if u.DryRun {
if u.dryRun {
args = append(args, "--dry-run")
}
if u.KeepHistory {
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.Stdout(cfg.Stdout)
u.cmd.Stderr(cfg.Stderr)
u.cmd.Stdout(u.stdout)
u.cmd.Stderr(u.stderr)
if cfg.Debug {
fmt.Fprintf(cfg.Stderr, "Generated command: '%s'\n", u.cmd.String())
if u.debug {
fmt.Fprintf(u.stderr, "Generated command: '%s'\n", u.cmd.String())
}
return nil

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package run
import (
"fmt"
"github.com/golang/mock/gomock"
"github.com/pelotech/drone-helm3/internal/env"
"github.com/stretchr/testify/suite"
"strings"
"testing"
@@ -31,13 +32,49 @@ func TestUpgradeTestSuite(t *testing.T) {
suite.Run(t, new(UpgradeTestSuite))
}
func (suite *UpgradeTestSuite) TestNewUpgrade() {
cfg := env.Config{
ChartVersion: "seventeen",
DryRun: true,
Wait: true,
Values: "steadfastness,forthrightness",
StringValues: "tensile_strength,flexibility",
ValuesFiles: []string{"/root/price_inventory.yml"},
ReuseValues: true,
Timeout: "go sit in the corner",
Chart: "billboard_top_100",
Release: "post_malone_circles",
Force: true,
AtomicUpgrade: true,
CleanupOnFail: true,
}
up := NewUpgrade(cfg)
suite.Equal(cfg.Chart, up.chart)
suite.Equal(cfg.Release, up.release)
suite.Equal(cfg.ChartVersion, up.chartVersion)
suite.Equal(true, up.dryRun)
suite.Equal(cfg.Wait, up.wait)
suite.Equal("steadfastness,forthrightness", up.values)
suite.Equal("tensile_strength,flexibility", up.stringValues)
suite.Equal([]string{"/root/price_inventory.yml"}, up.valuesFiles)
suite.Equal(cfg.ReuseValues, up.reuseValues)
suite.Equal(cfg.Timeout, up.timeout)
suite.Equal(cfg.Force, up.force)
suite.Equal(true, up.atomic)
suite.Equal(true, up.cleanupOnFail)
suite.NotNil(up.config)
suite.NotNil(up.certs)
}
func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
defer suite.ctrl.Finish()
u := Upgrade{
cfg := env.Config{
Chart: "at40",
Release: "jonas_brothers_only_human",
}
u := NewUpgrade(cfg)
command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path)
@@ -54,19 +91,20 @@ func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
Run().
Times(1)
cfg := Config{}
err := u.Prepare(cfg)
err := u.Prepare()
suite.Require().Nil(err)
u.Execute(cfg)
u.Execute()
}
func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
defer suite.ctrl.Finish()
u := Upgrade{
cfg := env.Config{
Namespace: "melt",
Chart: "at40",
Release: "shaed_trampoline",
}
u := NewUpgrade(cfg)
command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path)
@@ -78,17 +116,14 @@ func (suite *UpgradeTestSuite) TestPrepareNamespaceFlag() {
suite.mockCmd.EXPECT().Stdout(gomock.Any())
suite.mockCmd.EXPECT().Stderr(gomock.Any())
cfg := Config{
Namespace: "melt",
}
err := u.Prepare(cfg)
err := u.Prepare()
suite.Require().Nil(err)
}
func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
defer suite.ctrl.Finish()
u := Upgrade{
cfg := env.Config{
Chart: "hot_ac",
Release: "maroon_5_memories",
ChartVersion: "radio_edit",
@@ -100,11 +135,12 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
ReuseValues: true,
Timeout: "sit_in_the_corner",
Force: true,
Atomic: true,
AtomicUpgrade: true,
CleanupOnFail: true,
}
cfg := Config{}
u := NewUpgrade(cfg)
// inject a ca cert filename so repoCerts won't create any files that we'd have to clean up
u.certs.caCertFilename = "local_ca.cert"
command = func(path string, args ...string) cmd {
suite.Equal(helmBin, path)
@@ -121,6 +157,7 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
"--set-string", "height=5ft10in",
"--values", "/usr/local/stats",
"--values", "/usr/local/grades",
"--ca-file", "local_ca.cert",
"maroon_5_memories", "hot_ac"}, args)
return suite.mockCmd
@@ -129,7 +166,7 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
suite.mockCmd.EXPECT().Stdout(gomock.Any())
suite.mockCmd.EXPECT().Stderr(gomock.Any())
err := u.Prepare(cfg)
err := u.Prepare()
suite.Require().Nil(err)
}
@@ -138,34 +175,30 @@ func (suite *UpgradeTestSuite) TestRequiresChartAndRelease() {
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
u := Upgrade{
Release: "seth_everman_unskippable_cutscene",
}
u := NewUpgrade(env.Config{})
u.release = "seth_everman_unskippable_cutscene"
err := u.Prepare(Config{})
err := u.Prepare()
suite.EqualError(err, "chart is required", "Chart should be mandatory")
u = Upgrade{
Chart: "billboard_top_zero",
}
u.release = ""
u.chart = "billboard_top_zero"
err = u.Prepare(Config{})
err = u.Prepare()
suite.EqualError(err, "release is required", "Release should be mandatory")
}
func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
u := Upgrade{
Chart: "at40",
Release: "lewis_capaldi_someone_you_loved",
}
stdout := strings.Builder{}
stderr := strings.Builder{}
cfg := Config{
cfg := env.Config{
Chart: "at40",
Release: "lewis_capaldi_someone_you_loved",
Debug: true,
Stdout: &stdout,
Stderr: &stderr,
}
u := NewUpgrade(cfg)
command = func(path string, args ...string) cmd {
suite.mockCmd.EXPECT().
@@ -180,7 +213,7 @@ func (suite *UpgradeTestSuite) TestPrepareDebugFlag() {
suite.mockCmd.EXPECT().
Stderr(&stderr)
u.Prepare(cfg)
u.Prepare()
want := fmt.Sprintf("Generated command: '%s --debug upgrade "+
"--install lewis_capaldi_someone_you_loved at40'\n", helmBin)