From 18313eeb5c971fc0baea3d81d66bb9aac0dae237 Mon Sep 17 00:00:00 2001 From: Erin Call Date: Mon, 20 Jan 2020 15:40:36 -0800 Subject: [PATCH] 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. --- docs/parameter_reference.md | 3 +- internal/env/config.go | 3 +- internal/run/addrepo.go | 16 ++++--- internal/run/addrepo_test.go | 8 ++-- internal/run/repocerts.go | 77 ++++++++++++++++++++++++++++++++ internal/run/repocerts_test.go | 80 ++++++++++++++++++++++++++++++++++ internal/run/upgrade.go | 8 ++-- internal/run/upgrade_test.go | 4 +- 8 files changed, 181 insertions(+), 18 deletions(-) create mode 100644 internal/run/repocerts.go create mode 100644 internal/run/repocerts_test.go diff --git a/docs/parameter_reference.md b/docs/parameter_reference.md index 773ca9f..244460f 100644 --- a/docs/parameter_reference.md +++ b/docs/parameter_reference.md @@ -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\ | 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. | diff --git a/internal/env/config.go b/internal/env/config.go index dad997a..6c4f43c 100644 --- a/internal/env/config.go +++ b/internal/env/config.go @@ -24,7 +24,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 diff --git a/internal/run/addrepo.go b/internal/run/addrepo.go index cfb87c2..ced9805 100644 --- a/internal/run/addrepo.go +++ b/internal/run/addrepo.go @@ -9,9 +9,9 @@ import ( // AddRepo is an execution step that calls `helm repo add` when executed. type AddRepo struct { *config - repo string - caFile string - cmd cmd + repo string + certs *repoCerts + cmd cmd } // NewAddRepo creates an AddRepo for the given repo-spec. No validation is performed at this time. @@ -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...) diff --git a/internal/run/addrepo_test.go b/internal/run/addrepo_test.go index 6633981..a100b19 100644 --- a/internal/run/addrepo_test.go +++ b/internal/run/addrepo_test.go @@ -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) diff --git a/internal/run/repocerts.go b/internal/run/repocerts.go new file mode 100644 index 0000000..d5d3207 --- /dev/null +++ b/internal/run/repocerts.go @@ -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 +} diff --git a/internal/run/repocerts_test.go b/internal/run/repocerts_test.go new file mode 100644 index 0000000..f276534 --- /dev/null +++ b/internal/run/repocerts_test.go @@ -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)) +} diff --git a/internal/run/upgrade.go b/internal/run/upgrade.go index 80ccac4..b76948e 100644 --- a/internal/run/upgrade.go +++ b/internal/run/upgrade.go @@ -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...) diff --git a/internal/run/upgrade_test.go b/internal/run/upgrade_test.go index 770a15e..aeba26d 100644 --- a/internal/run/upgrade_test.go +++ b/internal/run/upgrade_test.go @@ -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)