Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0157d9bc2 | ||
|
|
1f2da68bbb | ||
|
|
7e2f982af7 | ||
|
|
0502d76c63 | ||
|
|
591b084970 | ||
|
|
f24a8e44ca | ||
|
|
8cb8a5d95d | ||
|
|
a4834dd4f7 | ||
|
|
8b6a8fdd4b | ||
|
|
dbcef2699e | ||
|
|
22aa1df894 | ||
|
|
8f7b481934 | ||
|
|
e843b26759 | ||
|
|
713dcd8317 | ||
|
|
18313eeb5c |
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +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_ca_file | string | | TLS certificate for a chart repository certificate authority. |
|
||||
| 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. |
|
||||
|
||||
@@ -94,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.
|
||||
|
||||
33
internal/env/config.go
vendored
33
internal/env/config.go
vendored
@@ -2,11 +2,12 @@ package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,7 +25,8 @@ type Config struct {
|
||||
DroneEvent string `envconfig:"drone_build_event"` // Drone event that invoked this plugin.
|
||||
UpdateDependencies bool `split_words:"true"` // Call `helm dependency update` before the main command
|
||||
AddRepos []string `split_words:"true"` // Call `helm repo add` before the main command
|
||||
RepoCAFile string `envconfig:"repo_ca_file"` // CA certificate for `helm repo add`
|
||||
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
|
||||
@@ -88,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()
|
||||
}
|
||||
@@ -97,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)"
|
||||
|
||||
36
internal/env/config_test.go
vendored
36
internal/env/config_test.go
vendored
@@ -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 {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
type AddRepo struct {
|
||||
*config
|
||||
repo string
|
||||
caFile string
|
||||
certs *repoCerts
|
||||
cmd cmd
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func NewAddRepo(cfg env.Config, repo string) *AddRepo {
|
||||
return &AddRepo{
|
||||
config: newConfig(cfg),
|
||||
repo: repo,
|
||||
caFile: cfg.RepoCAFile,
|
||||
certs: newRepoCerts(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +38,16 @@ func (a *AddRepo) Prepare() error {
|
||||
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 := a.globalFlags()
|
||||
args = append(args, "repo", "add")
|
||||
if a.caFile != "" {
|
||||
args = append(args, "--ca-file", a.caFile)
|
||||
}
|
||||
args = append(args, a.certs.flags()...)
|
||||
args = append(args, name, url)
|
||||
|
||||
a.cmd = command(helmBin, args...)
|
||||
|
||||
@@ -43,6 +43,7 @@ func (suite *AddRepoTestSuite) TestNewAddRepo() {
|
||||
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() {
|
||||
@@ -100,10 +101,11 @@ func (suite *AddRepoTestSuite) TestPrepareWithEqualSignInURL() {
|
||||
func (suite *AddRepoTestSuite) TestRepoAddFlags() {
|
||||
suite.mockCmd.EXPECT().Stdout(gomock.Any()).AnyTimes()
|
||||
suite.mockCmd.EXPECT().Stderr(gomock.Any()).AnyTimes()
|
||||
cfg := env.Config{
|
||||
RepoCAFile: "./helm/reporepo.cert",
|
||||
}
|
||||
cfg := env.Config{}
|
||||
a := NewAddRepo(cfg, "machine=https://github.com/harold_finch/themachine")
|
||||
|
||||
// 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)
|
||||
|
||||
77
internal/run/repocerts.go
Normal file
77
internal/run/repocerts.go
Normal 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
|
||||
}
|
||||
80
internal/run/repocerts_test.go
Normal file
80
internal/run/repocerts_test.go
Normal 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))
|
||||
}
|
||||
@@ -22,7 +22,7 @@ type Upgrade struct {
|
||||
force bool
|
||||
atomic bool
|
||||
cleanupOnFail bool
|
||||
caFile string
|
||||
certs *repoCerts
|
||||
|
||||
cmd cmd
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func NewUpgrade(cfg env.Config) *Upgrade {
|
||||
force: cfg.Force,
|
||||
atomic: cfg.AtomicUpgrade,
|
||||
cleanupOnFail: cfg.CleanupOnFail,
|
||||
caFile: cfg.RepoCAFile,
|
||||
certs: newRepoCerts(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,7 @@ func (u *Upgrade) Prepare() error {
|
||||
for _, vFile := range u.valuesFiles {
|
||||
args = append(args, "--values", vFile)
|
||||
}
|
||||
if u.caFile != "" {
|
||||
args = append(args, "--ca-file", u.caFile)
|
||||
}
|
||||
args = append(args, u.certs.flags()...)
|
||||
|
||||
args = append(args, u.release, u.chart)
|
||||
u.cmd = command(helmBin, args...)
|
||||
|
||||
@@ -64,6 +64,7 @@ func (suite *UpgradeTestSuite) TestNewUpgrade() {
|
||||
suite.Equal(true, up.atomic)
|
||||
suite.Equal(true, up.cleanupOnFail)
|
||||
suite.NotNil(up.config)
|
||||
suite.NotNil(up.certs)
|
||||
}
|
||||
|
||||
func (suite *UpgradeTestSuite) TestPrepareAndExecute() {
|
||||
@@ -136,9 +137,10 @@ func (suite *UpgradeTestSuite) TestPrepareWithUpgradeFlags() {
|
||||
Force: true,
|
||||
AtomicUpgrade: true,
|
||||
CleanupOnFail: true,
|
||||
RepoCAFile: "local_ca.cert",
|
||||
}
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user