From d80df934147779b6337fa8026c8323767443c1b6 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 27 Jul 2016 16:23:27 -0600 Subject: [PATCH] feat(helm): add signature support to 'helm package' --- cmd/helm/helm.go | 1 + cmd/helm/list_test.go | 4 +- cmd/helm/package.go | 93 ++++++++++++---- cmd/helm/package_test.go | 148 +++++++++++++++++++++++++ cmd/helm/testdata/helm-test-key.secret | Bin 0 -> 2545 bytes pkg/provenance/sign.go | 48 +++++++- 6 files changed, 271 insertions(+), 23 deletions(-) create mode 100644 cmd/helm/package_test.go create mode 100644 cmd/helm/testdata/helm-test-key.secret diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index ebe139fb6..42e7c6c26 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -94,6 +94,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newStatusCmd(nil, out), newUpgradeCmd(nil, out), newRollbackCmd(nil, out), + newPackageCmd(nil, out), ) return cmd } diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 84fe65a47..0a2e88f3f 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -56,9 +56,7 @@ func TestListCmd(t *testing.T) { rels: tt.resp, } cmd := newListCmd(c, &buf) - for flag, value := range tt.flags { - cmd.Flags().Set(flag, value) - } + setFlags(cmd, tt.flags) err := cmd.RunE(cmd, tt.args) if (err != nil) != tt.err { t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) diff --git a/cmd/helm/package.go b/cmd/helm/package.go index beff30e31..81385224f 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -17,12 +17,18 @@ limitations under the License. package main import ( + "errors" "fmt" + "io" + "io/ioutil" "os" "path/filepath" "github.com/spf13/cobra" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/repo" ) @@ -37,30 +43,56 @@ Chart.yaml file, and (if found) build the current directory into a chart. Versioned chart archives are used by Helm package repositories. ` -var save bool +const ( + envSigningKey = "HELM_SIGNING_KEY" + envKeyring = "HELM_KEYRING" +) -func init() { - packageCmd.Flags().BoolVar(&save, "save", true, "save packaged chart to local chart repository") - RootCommand.AddCommand(packageCmd) +type packageCmd struct { + save bool + sign bool + path string + key string + keyring string + out io.Writer } -var packageCmd = &cobra.Command{ - Use: "package [CHART_PATH]", - Short: "package a chart directory into a chart archive", - Long: packageDesc, - RunE: runPackage, -} +func newPackageCmd(client helm.Interface, out io.Writer) *cobra.Command { + pkg := &packageCmd{ + out: out, + } + cmd := &cobra.Command{ + Use: "package [CHART_PATH]", + Short: "package a chart directory into a chart archive", + Long: packageDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("This command needs at least one argument, the path to the chart.") + } + pkg.path = args[0] + if pkg.sign { + if pkg.key == "" { + return errors.New("--key is required for signing a package") + } + if pkg.keyring == "" { + return errors.New("--keyring is required for signing a package") + } + } + return pkg.run(cmd, args) + }, + } -func runPackage(cmd *cobra.Command, args []string) error { - path := "." + f := cmd.Flags() + f.BoolVar(&pkg.save, "save", true, "save packaged chart to local chart repository") + f.BoolVar(&pkg.sign, "sign", false, "use a PGP private key to sign this package") + f.StringVar(&pkg.key, "key", "", "the name of the key to use when signing. Used if --sign is true.") + f.StringVar(&pkg.keyring, "keyring", os.ExpandEnv("$HOME/.gnupg/pubring.gpg"), "the location of a public keyring") - if len(args) > 0 { - path = args[0] - } else { - return fmt.Errorf("This command needs at least one argument, the path to the chart.") - } + return cmd +} - path, err := filepath.Abs(path) +func (p *packageCmd) run(cmd *cobra.Command, args []string) error { + path, err := filepath.Abs(p.path) if err != nil { return err } @@ -86,7 +118,7 @@ func runPackage(cmd *cobra.Command, args []string) error { // Save to $HELM_HOME/local directory. This is second, because we don't want // the case where we saved here, but didn't save to the default destination. - if save { + if p.save { if err := repo.AddChartToLocalRepo(ch, localRepoDirectory()); err != nil { return err } else if flagDebug { @@ -94,5 +126,28 @@ func runPackage(cmd *cobra.Command, args []string) error { } } + if p.sign { + err = p.clearsign(name) + } + return err } + +func (p *packageCmd) clearsign(filename string) error { + // Load keyring + signer, err := provenance.NewFromKeyring(p.keyring, p.key) + if err != nil { + return err + } + + sig, err := signer.ClearSign(filename) + if err != nil { + return err + } + + if flagDebug { + fmt.Fprintln(p.out, sig) + } + + return ioutil.WriteFile(filename+".prov", []byte(sig), 0755) +} diff --git a/cmd/helm/package_test.go b/cmd/helm/package_test.go new file mode 100644 index 000000000..b724ba2ea --- /dev/null +++ b/cmd/helm/package_test.go @@ -0,0 +1,148 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/spf13/cobra" +) + +func TestPackage(t *testing.T) { + + tests := []struct { + name string + flags map[string]string + args []string + expect string + hasfile string + err bool + }{ + { + name: "package without chart path", + args: []string{}, + flags: map[string]string{}, + expect: "This command needs at least one argument, the path to the chart.", + err: true, + }, + { + name: "package --sign, no --key", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"sign": "1"}, + expect: "key is required for signing a package", + err: true, + }, + { + name: "package --sign, no --keyring", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"sign": "1", "key": "nosuchkey", "keyring": ""}, + expect: "keyring is required for signing a package", + err: true, + }, + { + name: "package testdata/testcharts/alpine", + args: []string{"testdata/testcharts/alpine"}, + expect: "", + hasfile: "alpine-0.1.0.tgz", + }, + { + name: "package --sign --key=KEY --keyring=KEYRING testdata/testcharts/alpine", + args: []string{"testdata/testcharts/alpine"}, + flags: map[string]string{"sign": "1", "keyring": "testdata/helm-test-key.secret", "key": "helm-test"}, + expect: "", + hasfile: "alpine-0.1.0.tgz", + }, + } + + // Because these tests are destructive, we run them in a tempdir. + origDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + tmp, err := ioutil.TempDir("", "helm-package-test-") + if err != nil { + t.Fatal(err) + } + + t.Logf("Running tests in %s", tmp) + if err := os.Chdir(tmp); err != nil { + t.Fatal(err) + } + + defer func() { + os.Chdir(origDir) + os.RemoveAll(tmp) + }() + + for _, tt := range tests { + buf := bytes.NewBuffer(nil) + c := newPackageCmd(nil, buf) + + // This is an unfortunate byproduct of the tmpdir + if v, ok := tt.flags["keyring"]; ok && len(v) > 0 { + tt.flags["keyring"] = filepath.Join(origDir, v) + } + + setFlags(c, tt.flags) + re := regexp.MustCompile(tt.expect) + + adjustedArgs := make([]string, len(tt.args)) + for i, f := range tt.args { + adjustedArgs[i] = filepath.Join(origDir, f) + } + + err := c.RunE(c, adjustedArgs) + if err != nil { + if tt.err && re.MatchString(err.Error()) { + continue + } + t.Errorf("%q: expected error %q, got %q", tt.name, tt.expect, err) + continue + } + + if !re.Match(buf.Bytes()) { + t.Errorf("%q: expected output %q, got %q", tt.name, tt.expect, buf.String()) + } + + if len(tt.hasfile) > 0 { + if fi, err := os.Stat(tt.hasfile); err != nil { + t.Errorf("%q: expected file %q, got err %q", tt.name, tt.hasfile, err) + } else if fi.Size() == 0 { + t.Errorf("%q: file %q has zero bytes.", tt.name, tt.hasfile) + } + } + + if v, ok := tt.flags["sign"]; ok && v == "1" { + if fi, err := os.Stat(tt.hasfile + ".prov"); err != nil { + t.Errorf("%q: expected provenance file", tt.name) + } else if fi.Size() == 0 { + t.Errorf("%q: provenance file is empty", tt.name) + } + } + } +} + +func setFlags(cmd *cobra.Command, flags map[string]string) { + dest := cmd.Flags() + for f, v := range flags { + dest.Set(f, v) + } +} diff --git a/cmd/helm/testdata/helm-test-key.secret b/cmd/helm/testdata/helm-test-key.secret new file mode 100644 index 0000000000000000000000000000000000000000..a966aef93ed97d01d764f29940738df6df2d9d24 GIT binary patch literal 2545 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