diff --git a/.gitignore b/.gitignore index 3f1c658b5740a24005b0d3f7fffef9d7e493bae9..4613d316ad555a89d9d338f74be87c732d22fdb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ +.DS_Store .coverage/ +.vimrc +_dist/ +_proto/*.pb.go bin/ rootfs/tiller vendor/ -_proto/*.pb.go -.vimrc -.DS_Store diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index 95f926430e32f24cb9ed5a7dd3ecab0620a6e310..ba3252e3ca86e725a3fe57c33db537b1396d72c7 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -162,6 +162,9 @@ message UpdateReleaseRequest { hapi.chart.Config values = 3; // dry_run, if true, will run through the release logic, but neither create bool dry_run = 4; + + // DisableHooks causes the server to skip running any hooks for the upgrade. + bool disable_hooks = 5; } // UpdateReleaseResponse is the response to an update request. diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index c47a13e6a6f23af6ce618b8c09aff3a21aaa48cc..ad104926758942ad07e583736cac4170329d2095 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -84,12 +84,13 @@ func newRootCmd(out io.Writer) *cobra.Command { cmd.AddCommand( newCreateCmd(out), + newDeleteCmd(nil, out), newGetCmd(nil, out), + newInitCmd(out), + newInspectCmd(nil, out), + newInstallCmd(nil, out), newListCmd(nil, out), newStatusCmd(nil, out), - newInstallCmd(nil, out), - newDeleteCmd(nil, out), - newInspectCmd(nil, out), newUpgradeCmd(nil, out), ) return cmd diff --git a/cmd/helm/init.go b/cmd/helm/init.go index 565022a89e8639ef853414caaddd470e3098246a..21852729554e356769c9f6199cc25ae4a15a06f8 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -19,6 +19,7 @@ package main import ( "errors" "fmt" + "io" "os" "github.com/spf13/cobra" @@ -32,58 +33,54 @@ Kubernetes Cluster and sets up local configuration in $HELM_HOME (default: ~/.he ` var ( - tillerImg string - clientOnly bool defaultRepository = "kubernetes-charts" defaultRepositoryURL = "http://storage.googleapis.com/kubernetes-charts" ) -func init() { - f := initCmd.Flags() - f.StringVarP(&tillerImg, "tiller-image", "i", "", "override tiller image") - f.BoolVarP(&clientOnly, "client-only", "c", false, "If set does not install tiller") - RootCommand.AddCommand(initCmd) +type initCmd struct { + image string + clientOnly bool + out io.Writer } -var initCmd = &cobra.Command{ - Use: "init", - Short: "initialize Helm on both client and server", - Long: initDesc, - RunE: runInit, +func newInitCmd(out io.Writer) *cobra.Command { + i := &initCmd{ + out: out, + } + cmd := &cobra.Command{ + Use: "init", + Short: "initialize Helm on both client and server", + Long: initDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 0 { + return errors.New("This command does not accept arguments") + } + return i.run() + }, + } + cmd.Flags().StringVarP(&i.image, "tiller-image", "i", "", "override tiller image") + cmd.Flags().BoolVarP(&i.clientOnly, "client-only", "c", false, "If set does not install tiller") + return cmd } // runInit initializes local config and installs tiller to Kubernetes Cluster -func runInit(cmd *cobra.Command, args []string) error { - if len(args) != 0 { - return errors.New("This command does not accept arguments. \n") - } - +func (i *initCmd) run() error { if err := ensureHome(); err != nil { return err } - if !clientOnly { - if err := installTiller(); err != nil { - return err + if !i.clientOnly { + if err := client.Install(tillerNamespace, i.image, flagDebug); err != nil { + return fmt.Errorf("error installing: %s", err) } + fmt.Fprintln(i.out, "\nTiller (the helm server side component) has been installed into your Kubernetes Cluster.") } else { - fmt.Println("Not installing tiller due to 'client-only' flag having been set") + fmt.Fprintln(i.out, "Not installing tiller due to 'client-only' flag having been set") } - - fmt.Println("Happy Helming!") - return nil -} - -func installTiller() error { - if err := client.Install(tillerNamespace, tillerImg, flagDebug); err != nil { - return fmt.Errorf("error installing: %s", err) - } - fmt.Println("\nTiller (the helm server side component) has been installed into your Kubernetes Cluster.") - + fmt.Fprintln(i.out, "Happy Helming!") return nil } -// requireHome checks to see if $HELM_HOME exists, and returns an error if it does not. func requireHome() error { dirs := []string{homePath(), repositoryDirectory(), cacheDirectory(), localRepoDirectory()} for _, d := range dirs { diff --git a/cmd/helm/inspect.go b/cmd/helm/inspect.go index 2e43f0e7aeaf93b88541e75641a1b97094f51094..8d9948b6f772766a469641ed89cfe2c22a794634 100644 --- a/cmd/helm/inspect.go +++ b/cmd/helm/inspect.go @@ -130,11 +130,10 @@ func (i *inspectCmd) run() error { fmt.Fprintln(i.out, string(cf)) } - if i.output == both { - fmt.Fprintln(i.out, "---") - } - - if i.output == valuesOnly || i.output == both { + if (i.output == valuesOnly || i.output == both) && chrt.Values != nil { + if i.output == both { + fmt.Fprintln(i.out, "---") + } fmt.Fprintln(i.out, chrt.Values.Raw) } diff --git a/cmd/helm/inspect_test.go b/cmd/helm/inspect_test.go index 24c4a895e26520ac48793f6fc32185b0a2d62357..bbd9c61998bfc23080d9a4c9dbd71dc0d7d8de4f 100644 --- a/cmd/helm/inspect_test.go +++ b/cmd/helm/inspect_test.go @@ -61,4 +61,17 @@ func TestInspect(t *testing.T) { t.Errorf("Expected\n%q\nGot\n%q\n", expect[i], got) } } + + // Regression tests for missing values. See issue #1024. + b.Reset() + insp = &inspectCmd{ + chartpath: "testdata/testcharts/novals", + output: "values", + out: b, + } + insp.run() + if b.Len() != 0 { + t.Errorf("expected empty values buffer, got %q", b.String()) + } + } diff --git a/cmd/helm/install.go b/cmd/helm/install.go index e80527b48949860d1580c201624a88d9fdd9cfcc..b526ea090c6dd132094efa5d0d0e1d0873a217ae 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "bytes" "fmt" "io" "io/ioutil" @@ -24,6 +25,9 @@ import ( "path/filepath" "strings" + "text/template" + + "github.com/Masterminds/sprig" "github.com/ghodss/yaml" "github.com/spf13/cobra" @@ -59,10 +63,11 @@ type installCmd struct { chartPath string dryRun bool disableHooks bool - reuseName bool + replace bool out io.Writer client helm.Interface values *values + nameTemplate string } func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { @@ -98,8 +103,9 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { f.StringVar(&inst.namespace, "namespace", "default", "the namespace to install the release into") f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install") f.BoolVar(&inst.disableHooks, "no-hooks", false, "prevent hooks from running during install") - f.BoolVar(&inst.reuseName, "reuse-name", false, "force Tiller to re-use the given name, even if that name is already used. This is unsafe in production") + f.BoolVar(&inst.replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production") f.Var(inst.values, "set", "set values on the command line. Separate values with commas: key1=val1,key2=val2") + f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release") return cmd } @@ -113,13 +119,23 @@ func (i *installCmd) run() error { return err } + // If template is specified, try to run the template. + if i.nameTemplate != "" { + i.name, err = generateName(i.nameTemplate) + if err != nil { + return err + } + // Print the final name so the user knows what the final name of the release is. + fmt.Printf("final name: %s\n", i.name) + } + res, err := i.client.InstallRelease( i.chartPath, i.namespace, helm.ValueOverrides(rawVals), helm.ReleaseName(i.name), helm.InstallDryRun(i.dryRun), - helm.InstallReuseName(i.reuseName), + helm.InstallReuseName(i.replace), helm.InstallDisableHooks(i.disableHooks)) if err != nil { return prettyError(err) @@ -249,3 +265,16 @@ func locateChartPath(name string) (string, error) { return name, fmt.Errorf("file %q not found", origname) } + +func generateName(nameTemplate string) (string, error) { + t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) + if err != nil { + return "", err + } + var b bytes.Buffer + err = t.Execute(&b, nil) + if err != nil { + return "", err + } + return b.String(), nil +} diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 41663a337c8a52e4979eae336d7f0c82a755b133..7043eee6d1fc559eb3add6478802e9b4e8b0e7dc 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -19,6 +19,7 @@ package main import ( "fmt" "io" + "regexp" "strings" "testing" @@ -59,12 +60,20 @@ func TestInstall(t *testing.T) { }, // Install, re-use name { - name: "install and reuse name", + name: "install and replace release", args: []string{"testdata/testcharts/alpine"}, - flags: strings.Split("--name aeneas --reuse-name", " "), + flags: strings.Split("--name aeneas --replace", " "), expected: "aeneas", resp: releaseMock(&releaseOptions{name: "aeneas"}), }, + // Install, using the name-template + { + name: "install with name-template", + args: []string{"testdata/testcharts/alpine"}, + flags: []string{"--name-template", "{{upper \"foobar\"}}"}, + expected: "FOOBAR", + resp: releaseMock(&releaseOptions{name: "FOOBAR"}), + }, } runReleaseCases(t, tests, func(c *fakeReleaseClient, out io.Writer) *cobra.Command { @@ -113,3 +122,78 @@ sailor: sinbad t.Errorf("Expected String() to be \n%s\nGot\n%s\n", y, out) } } + +type nameTemplateTestCase struct { + tpl string + expected string + expectedErrorStr string +} + +func TestNameTemplate(t *testing.T) { + testCases := []nameTemplateTestCase{ + // Just a straight up nop please + { + tpl: "foobar", + expected: "foobar", + expectedErrorStr: "", + }, + // Random numbers at the end for fun & profit + { + tpl: "foobar-{{randNumeric 6}}", + expected: "foobar-[0-9]{6}$", + expectedErrorStr: "", + }, + // Random numbers in the middle for fun & profit + { + tpl: "foobar-{{randNumeric 4}}-baz", + expected: "foobar-[0-9]{4}-baz$", + expectedErrorStr: "", + }, + // No such function + { + tpl: "foobar-{{randInt}}", + expected: "", + expectedErrorStr: "function \"randInt\" not defined", + }, + // Invalid template + { + tpl: "foobar-{{", + expected: "", + expectedErrorStr: "unexpected unclosed action", + }, + } + + for _, tc := range testCases { + + n, err := generateName(tc.tpl) + if err != nil { + if tc.expectedErrorStr == "" { + t.Errorf("Was not expecting error, but got: %v", err) + continue + } + re, compErr := regexp.Compile(tc.expectedErrorStr) + if compErr != nil { + t.Errorf("Expected error string failed to compile: %v", compErr) + continue + } + if !re.MatchString(err.Error()) { + t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err) + continue + } + } + if err == nil && tc.expectedErrorStr != "" { + t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr) + } + + if tc.expected != "" { + re, err := regexp.Compile(tc.expected) + if err != nil { + t.Errorf("Expected string failed to compile: %v", err) + continue + } + if !re.MatchString(n) { + t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n) + } + } + } +} diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index d3e496cdab41a3534da2a99686c37e53719961a3..75a0f30828d9916eeb6bd8d1747b5f1dbce07299 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -47,7 +47,10 @@ var lintCommand = &cobra.Command{ RunE: lintCmd, } +var flagStrict bool + func init() { + lintCommand.Flags().BoolVarP(&flagStrict, "strict", "", false, "fail on lint warnings") RootCommand.AddCommand(lintCommand) } @@ -59,6 +62,13 @@ func lintCmd(cmd *cobra.Command, args []string) error { paths = args } + var lowestTolerance int + if flagStrict { + lowestTolerance = support.WarningSev + } else { + lowestTolerance = support.ErrorSev + } + var total int var failures int for _, path := range paths { @@ -77,7 +87,7 @@ func lintCmd(cmd *cobra.Command, args []string) error { } total = total + 1 - if linter.HighestSeverity >= support.ErrorSev { + if linter.HighestSeverity >= lowestTolerance { failures = failures + 1 } } diff --git a/cmd/helm/serve.go b/cmd/helm/serve.go index ae60833abae6444fea95bb0fd8cf34e478ff9398..3fca0bd8aa1d4120d8b8b8e81ea90dcce17b2420 100644 --- a/cmd/helm/serve.go +++ b/cmd/helm/serve.go @@ -17,17 +17,19 @@ limitations under the License. package main import ( + "os" + "path/filepath" + "github.com/spf13/cobra" "k8s.io/helm/pkg/repo" ) -var serveDesc = `This command starts a local chart repository server that serves the charts saved in your $HELM_HOME/local/ directory.` - -//TODO: add repoPath flag to be passed in in case you want -// to serve charts from a different local dir +var serveDesc = `This command starts a local chart repository server that serves charts from a local directory.` +var repoPath string func init() { + serveCmd.Flags().StringVar(&repoPath, "repo-path", localRepoDirectory(), "The local directory path from which to serve charts.") RootCommand.AddCommand(serveCmd) } @@ -35,9 +37,19 @@ var serveCmd = &cobra.Command{ Use: "serve", Short: "start a local http web server", Long: serveDesc, - Run: serve, + RunE: serve, } -func serve(cmd *cobra.Command, args []string) { - repo.StartLocalRepo(localRepoDirectory()) +func serve(cmd *cobra.Command, args []string) error { + + repoPath, err := filepath.Abs(repoPath) + if err != nil { + return err + } + if _, err := os.Stat(repoPath); os.IsNotExist(err) { + return err + } + + repo.StartLocalRepo(repoPath) + return nil } diff --git a/cmd/helm/testdata/testcharts/novals/Chart.yaml b/cmd/helm/testdata/testcharts/novals/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ce1a81da67927f021934d269ef9a6b9c186d16fd --- /dev/null +++ b/cmd/helm/testdata/testcharts/novals/Chart.yaml @@ -0,0 +1,6 @@ +description: Deploy a basic Alpine Linux pod +home: https://k8s.io/helm +name: novals +sources: +- https://github.com/kubernetes/helm +version: 0.2.0 diff --git a/cmd/helm/testdata/testcharts/novals/README.md b/cmd/helm/testdata/testcharts/novals/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3c32de5db6abcf366c2f162edcf822e7e1eb5e5b --- /dev/null +++ b/cmd/helm/testdata/testcharts/novals/README.md @@ -0,0 +1,13 @@ +#Alpine: A simple Helm chart + +Run a single pod of Alpine Linux. + +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.yaml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install docs/examples/alpine`. diff --git a/cmd/helm/testdata/testcharts/novals/templates/alpine-pod.yaml b/cmd/helm/testdata/testcharts/novals/templates/alpine-pod.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c15ab8efc0fe5c15b931ef97f52540fbfa2b623f --- /dev/null +++ b/cmd/helm/testdata/testcharts/novals/templates/alpine-pod.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{.Release.Name}}-{{.Values.Name}}" + labels: + # The "heritage" label is used to track which tool deployed a given chart. + # It is useful for admins who want to see what releases a particular tool + # is responsible for. + heritage: {{.Release.Service | quote }} + # The "release" convention makes it easy to tie a release to all of the + # Kubernetes resources that were created as part of that release. + release: {{.Release.Name | quote }} + # This makes it easy to audit chart usage. + chart: "{{.Chart.Name}}-{{.Chart.Version}}" + annotations: + "helm.sh/created": {{.Release.Time.Seconds | quote }} +spec: + # This shows how to use a simple value. This will look for a passed-in value + # called restartPolicy. If it is not found, it will use the default value. + # {{default "Never" .restartPolicy}} is a slightly optimized version of the + # more conventional syntax: {{.restartPolicy | default "Never"}} + restartPolicy: {{default "Never" .Values.restartPolicy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index c4f75310ffd43b69401d13def448dfe0becfb9fe..07eeae03b559633b5df13181d21480c07a536c63 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -34,12 +34,13 @@ argument can be a relative path to a packaged or unpackaged chart. ` type upgradeCmd struct { - release string - chart string - out io.Writer - client helm.Interface - dryRun bool - valuesFile string + release string + chart string + out io.Writer + client helm.Interface + dryRun bool + disableHooks bool + valuesFile string } func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -70,6 +71,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { f := cmd.Flags() f.StringVarP(&upgrade.valuesFile, "values", "f", "", "path to a values YAML file") f.BoolVar(&upgrade.dryRun, "dry-run", false, "simulate an upgrade") + f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks") return cmd } @@ -88,12 +90,13 @@ func (u *upgradeCmd) run() error { } } - _, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun)) + _, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun), helm.UpgradeDisableHooks(u.disableHooks)) if err != nil { return prettyError(err) } - fmt.Fprintf(u.out, "It's not you. It's me\nYour upgrade looks valid but this command is still under active development.\nHang tight.\n") + success := u.release + " has been upgraded. Happy Helming!\n" + fmt.Fprintf(u.out, success) return nil diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index 341670cd860fa98b5096b1b5660f8f9d4b39d974..f51049e28388220ff9455cdda40188f6e62070e5 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -52,18 +52,22 @@ func TestUpgradeCmd(t *testing.T) { Description: "A Helm chart for Kubernetes", Version: "0.1.2", } + chartPath, err = chartutil.Create(cfile, tmpChart) if err != nil { t.Errorf("Error creating chart: %v", err) } - ch, _ = chartutil.Load(chartPath) + ch, err = chartutil.Load(chartPath) + if err != nil { + t.Errorf("Error loading updated chart: %v", err) + } tests := []releaseCase{ { name: "upgrade a release", args: []string{"funny-bunny", chartPath}, resp: releaseMock(&releaseOptions{name: "funny-bunny", version: 2, chart: ch}), - expected: "It's not you. It's me\nYour upgrade looks valid but this command is still under active development.\nHang tight.\n", + expected: "funny-bunny has been upgraded. Happy Helming!\n", }, } diff --git a/cmd/tiller/environment/environment.go b/cmd/tiller/environment/environment.go index c40e37a73454621b6fe1b897ff023532e89e4483..7957b4224a190297a4c12d4047fa5f9f87dfd884 100644 --- a/cmd/tiller/environment/environment.go +++ b/cmd/tiller/environment/environment.go @@ -117,6 +117,15 @@ type KubeClient interface { // For all other kinds, it means the kind was created or modified without // error. WatchUntilReady(namespace string, reader io.Reader) error + + // Update updates one or more resources or creates the resource + // if it doesn't exist + // + // namespace must contain a valid existing namespace + // + // reader must contain a YAML stream (one or more YAML documents separated + // by "\n---\n"). + Update(namespace string, originalReader, modifiedReader io.Reader) error } // PrintingKubeClient implements KubeClient, but simply prints the reader to @@ -145,6 +154,12 @@ func (p *PrintingKubeClient) WatchUntilReady(ns string, r io.Reader) error { return err } +// Update implements KubeClient Update. +func (p *PrintingKubeClient) Update(ns string, currentReader, modifiedReader io.Reader) error { + _, err := io.Copy(p.Out, modifiedReader) + return err +} + // Environment provides the context for executing a client request. // // All services in a context are concurrency safe. diff --git a/cmd/tiller/environment/environment_test.go b/cmd/tiller/environment/environment_test.go index 98af40edeb608bf0c3185f427b4bd14e752b8a2b..690587f324afc0e28f1937772f0a1af8b1ad24e9 100644 --- a/cmd/tiller/environment/environment_test.go +++ b/cmd/tiller/environment/environment_test.go @@ -87,6 +87,9 @@ func (k *mockKubeClient) Create(ns string, r io.Reader) error { func (k *mockKubeClient) Delete(ns string, r io.Reader) error { return nil } +func (k *mockKubeClient) Update(ns string, currentReader, modifiedReader io.Reader) error { + return nil +} func (k *mockKubeClient) WatchUntilReady(ns string, r io.Reader) error { return nil } diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 0431b832d475aae96e45dbb7bdfc8a7dedb5fac8..7a4dab3dedc6cad976facd76d5048310d86a6b43 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -24,7 +24,6 @@ import ( "regexp" "sort" - "github.com/Masterminds/semver" "github.com/ghodss/yaml" "github.com/technosophos/moniker" ctx "golang.org/x/net/context" @@ -171,65 +170,107 @@ func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleas } func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { - rel, err := s.prepareUpdate(req) + currentRelease, updatedRelease, err := s.prepareUpdate(req) if err != nil { return nil, err } - // TODO: perform update + res, err := s.performUpdate(currentRelease, updatedRelease, req) + if err != nil { + return nil, err + } + + if err := s.env.Releases.Update(updatedRelease); err != nil { + return nil, err + } + + return res, nil +} + +func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.Release, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { + res := &services.UpdateReleaseResponse{Release: updatedRelease} + + if req.DryRun { + log.Printf("Dry run for %s", updatedRelease.Name) + return res, nil + } + + // pre-ugrade hooks + if !req.DisableHooks { + if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, preUpgrade); err != nil { + return res, err + } + } + + kubeCli := s.env.KubeClient + original := bytes.NewBufferString(originalRelease.Manifest) + modified := bytes.NewBufferString(updatedRelease.Manifest) + if err := kubeCli.Update(updatedRelease.Namespace, original, modified); err != nil { + return nil, fmt.Errorf("Update of %s failed: %s", updatedRelease.Name, err) + } + + // post-upgrade hooks + if !req.DisableHooks { + if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, postUpgrade); err != nil { + return res, err + } + } - return &services.UpdateReleaseResponse{Release: rel}, nil + updatedRelease.Info.Status.Code = release.Status_DEPLOYED + + return res, nil } -// prepareUpdate builds a release for an update operation. -func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*release.Release, error) { +// prepareUpdate builds an updated release for an update operation. +func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*release.Release, *release.Release, error) { if req.Name == "" { - return nil, errMissingRelease + return nil, nil, errMissingRelease } if req.Chart == nil { - return nil, errMissingChart + return nil, nil, errMissingChart } // finds the non-deleted release with the given name - rel, err := s.env.Releases.Get(req.Name) + currentRelease, err := s.env.Releases.Get(req.Name) if err != nil { - return nil, err + return nil, nil, err } - //validate chart name is same as previous release - givenChart := req.Chart.Metadata.Name - releasedChart := rel.Chart.Metadata.Name - if givenChart != releasedChart { - return nil, fmt.Errorf("Given chart, %s, does not match chart originally released, %s", givenChart, releasedChart) + ts := timeconv.Now() + options := chartutil.ReleaseOptions{ + Name: req.Name, + Time: ts, + Namespace: currentRelease.Namespace, } - // validate new chart version is higher than old - - givenChartVersion := req.Chart.Metadata.Version - releasedChartVersion := rel.Chart.Metadata.Version - c, err := semver.NewConstraint("> " + releasedChartVersion) + valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options) if err != nil { - return nil, err + return nil, nil, err } - v, err := semver.NewVersion(givenChartVersion) + hooks, manifestDoc, err := s.renderResources(req.Chart, valuesToRender) if err != nil { - return nil, err - } - - if a := c.Check(v); !a { - return nil, fmt.Errorf("Given chart (%s-%v) must be a higher version than released chart (%s-%v)", givenChart, givenChartVersion, releasedChart, releasedChartVersion) + return nil, nil, err } // Store an updated release. updatedRelease := &release.Release{ - Name: req.Name, - Chart: req.Chart, - Config: req.Values, - Version: rel.Version + 1, + Name: req.Name, + Namespace: currentRelease.Namespace, + Chart: req.Chart, + Config: req.Values, + Info: &release.Info{ + FirstDeployed: currentRelease.Info.FirstDeployed, + LastDeployed: ts, + Status: &release.Status{Code: release.Status_UNKNOWN}, + }, + Version: currentRelease.Version + 1, + Manifest: manifestDoc.String(), + Hooks: hooks, } - return updatedRelease, nil + + return currentRelease, updatedRelease, nil } func (s *releaseServer) uniqName(start string, reuse bool) (string, error) { @@ -240,10 +281,12 @@ func (s *releaseServer) uniqName(start string, reuse bool) (string, error) { if start != "" { if rel, err := s.env.Releases.Get(start); err == driver.ErrReleaseNotFound { return start, nil - } else if reuse && rel.Info.Status.Code == release.Status_DELETED { + } else if st := rel.Info.Status.Code; reuse && (st == release.Status_DELETED || st == release.Status_FAILED) { // Allowe re-use of names if the previous release is marked deleted. log.Printf("reusing name %q", start) return start, nil + } else if reuse { + return "", errors.New("cannot re-use a name that is still in use") } return "", fmt.Errorf("a release named %q already exists", start) @@ -306,12 +349,36 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re return nil, err } - renderer := s.engine(req.Chart) - files, err := renderer.Render(req.Chart, valuesToRender) + hooks, manifestDoc, err := s.renderResources(req.Chart, valuesToRender) if err != nil { return nil, err } + // Store a release. + rel := &release.Release{ + Name: name, + Namespace: req.Namespace, + Chart: req.Chart, + Config: req.Values, + Info: &release.Info{ + FirstDeployed: ts, + LastDeployed: ts, + Status: &release.Status{Code: release.Status_UNKNOWN}, + }, + Manifest: manifestDoc.String(), + Hooks: hooks, + Version: 1, + } + return rel, nil +} + +func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values) ([]*release.Hook, *bytes.Buffer, error) { + renderer := s.engine(ch) + files, err := renderer.Render(ch, values) + if err != nil { + return nil, nil, err + } + // Sort hooks, manifests, and partials. Only hooks and manifests are returned, // as partials are not used after renderer.Render. Empty manifests are also // removed here. @@ -319,7 +386,7 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re if err != nil { // By catching parse errors here, we can prevent bogus releases from going // to Kubernetes. - return nil, err + return nil, nil, err } // Aggregate all valid manifests into one big doc. @@ -329,22 +396,7 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re b.WriteString(file) } - // Store a release. - rel := &release.Release{ - Name: name, - Namespace: req.Namespace, - Chart: req.Chart, - Config: req.Values, - Info: &release.Info{ - FirstDeployed: ts, - LastDeployed: ts, - Status: &release.Status{Code: release.Status_UNKNOWN}, - }, - Manifest: b.String(), - Hooks: hooks, - Version: 1, - } - return rel, nil + return hooks, b, nil } // validateYAML checks to see if YAML is well-formed. diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 37edffac6c6de5b09e729ff28b57b50d091e316e..f2ba5c69aa8f2ce8bf32ebd938cd2661593a3211 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -45,6 +45,16 @@ data: name: value ` +var manifestWithUpgradeHooks = `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm + annotations: + "helm.sh/hook": post-upgrade,pre-upgrade +data: + name: value +` + func rsFixture() *releaseServer { return &releaseServer{ env: mockEnvironment(), @@ -80,8 +90,9 @@ func releaseStub() *release.Release { LastDeployed: &date, Status: &release.Status{Code: release.Status_DEPLOYED}, }, - Chart: chartStub(), - Config: &chart.Config{Raw: `name = "value"`}, + Chart: chartStub(), + Config: &chart.Config{Raw: `name = "value"`}, + Version: 1, Hooks: []*release.Hook{ { Name: "test-cm", @@ -290,6 +301,105 @@ func TestInstallReleaseReuseName(t *testing.T) { } } +func TestUpdateRelease(t *testing.T) { + c := context.Background() + rs := rsFixture() + rel := releaseStub() + rs.env.Releases.Create(rel) + + req := &services.UpdateReleaseRequest{ + Name: rel.Name, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{Name: "hello"}, + Templates: []*chart.Template{ + {Name: "hello", Data: []byte("hello: world")}, + {Name: "hooks", Data: []byte(manifestWithUpgradeHooks)}, + }, + }, + } + res, err := rs.UpdateRelease(c, req) + if err != nil { + t.Errorf("Failed updated: %s", err) + } + + if res.Release.Name == "" { + t.Errorf("Expected release name.") + } + + if res.Release.Name != rel.Name { + t.Errorf("Updated release name does not match previous release name. Expected %s, got %s", rel.Name, res.Release.Name) + } + + if res.Release.Namespace != rel.Namespace { + t.Errorf("Expected release namespace '%s', got '%s'.", rel.Namespace, res.Release.Namespace) + } + + updated, err := rs.env.Releases.Get(res.Release.Name) + if err != nil { + t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) + } + + if len(updated.Hooks) != 1 { + t.Fatalf("Expected 1 hook, got %d", len(updated.Hooks)) + } + if updated.Hooks[0].Manifest != manifestWithUpgradeHooks { + t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest) + } + + if updated.Hooks[0].Events[0] != release.Hook_POST_UPGRADE { + t.Errorf("Expected event 0 to be post upgrade") + } + + if updated.Hooks[0].Events[1] != release.Hook_PRE_UPGRADE { + t.Errorf("Expected event 0 to be pre upgrade") + } + + if len(res.Release.Manifest) == 0 { + t.Errorf("No manifest returned: %v", res.Release) + } + + if len(updated.Manifest) == 0 { + t.Errorf("Expected manifest in %v", res) + } + + if !strings.Contains(updated.Manifest, "---\n# Source: hello/hello\nhello: world") { + t.Errorf("unexpected output: %s", rel.Manifest) + } + + if res.Release.Version != 2 { + t.Errorf("Expected release version to be %v, got %v", 2, res.Release.Version) + } +} + +func TestUpdateReleaseNoHooks(t *testing.T) { + c := context.Background() + rs := rsFixture() + rel := releaseStub() + rs.env.Releases.Create(rel) + + req := &services.UpdateReleaseRequest{ + Name: rel.Name, + DisableHooks: true, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{Name: "hello"}, + Templates: []*chart.Template{ + {Name: "hello", Data: []byte("hello: world")}, + {Name: "hooks", Data: []byte(manifestWithUpgradeHooks)}, + }, + }, + } + + res, err := rs.UpdateRelease(c, req) + if err != nil { + t.Errorf("Failed updated: %s", err) + } + + if hl := res.Release.Hooks[0].LastRun; hl != nil { + t.Errorf("Expected that no hooks were run. Got %d", hl) + } + +} + func TestUninstallRelease(t *testing.T) { c := context.Background() rs := rsFixture() diff --git a/docs/architecture.md b/docs/architecture.md index de69b75538c5aee7f61d8823cfcd6214447a7c18..272431eb7d70a4821cd5ce2e4fb3ebde7e9f0dc3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -75,9 +75,8 @@ The Go files generated from the `proto` definitions are stored in Docker images are built by cross-compiling Linux binaries and then building a Docker image from the files in `rootfs`. -The `scripts/` directory contains a number of utility scripts, including -`local-cluster.sh`, which can start a full Kubernetes instance inside of -a Docker container. +The `scripts/` directory contains a number of utility scripts. Most of these +are used by the CI/CD pipeline. Go dependencies are managed with [Glide](https://github.com/Masterminds/glide) and stored in the diff --git a/docs/charts.md b/docs/charts.md index 7ac3dd2c661cc628d5ea33df00fac17e015fc2b3..03eecba043c6fc0f398a69da3b748ed4fb159bef 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -202,6 +202,11 @@ sensitive_. - `Chart`: The contents of the `Chart.yaml`. Thus, the chart version is obtainable as `Chart.Version` and the maintainers are in `Chart.Maintainers`. +- `Files`: A map-like object containing all non-special files in the chart. This + will not give you access to templates, but will give you access to additional + files that are present. Files can be accessed using `{{index .Files "file.name"}}` + or using the `{{.Files.Get name}}` or `{{.Files.GetString name}}` functions. Note that + file data is returned as a `[]byte` unless `{{.Files.GetString}}` is used. **NOTE:** Any unknown Chart.yaml fields will be dropped. They will not be accessible inside of the `Chart` object. Thus, Chart.yaml cannot be diff --git a/docs/developers.md b/docs/developers.md index 79ece066abc719129b3fe0c496f233ed26d604b4..5ebdc58fe6b962a30245596648c645be6c49b0d0 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -66,11 +66,31 @@ GCR registry. ## Running a Local Cluster -You can run tests locally using the `scripts/local-cluster.sh` script to -start Kubernetes inside of a Docker container. For OS X, you will need -to be running `docker-machine`. +For development, we highly recommend using the +[Kubernetes Minikube](https://github.com/kubernetes/minikube) +developer-oriented distribution. Once this is installed, you can use +`helm init` to install into the cluster. -Tiller should run on any >= 1.2 Kubernetes cluster with beta extensions. +For developing on Tiller, it is sometimes more expedient to run Tiller locally +instead of packaging it into an image and running it in-cluster. You can do +this by telling the Helm client to us a local instance. + +```console +$ make build +$ bin/tiller +``` + +And to configure the Helm client, use the `--host` flag or export the `HELM_HOST` +environment variable: + +```console +$ export HELM_HOST=localhost:44134 +$ helm install foo +``` + +(Note that you do not need to use `helm init` when you are running Tiller directly) + +Tiller should run on any >= 1.3 Kubernetes cluster. ## Contribution Guidelines diff --git a/glide.lock b/glide.lock index 84dbc29a65acfb5d6982e10c85686db5fcc49f69..ba850229855e462640840125e14a9c6ea52aac12 100644 --- a/glide.lock +++ b/glide.lock @@ -1,10 +1,10 @@ -hash: 141ef5b9c491c91b026ab4007e48502c9a6df9f173c40e1406233dd44f065190 -updated: 2016-07-05T16:51:52.631048739-07:00 +hash: d3f3df18316dca3703f5d073e8f9b1e6bfdb27e8d7fc9c5d742afeddebb022db +updated: 2016-07-30T23:52:42.581826208-07:00 imports: - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 - name: github.com/asaskevich/govalidator - version: df81827fdd59d8b4fb93d8910b286ab7a3919520 + version: 7664702784775e51966f0885f5cd27435916517b - name: github.com/beorn7/perks version: 3ac7bf7a47d159a033b107610db8a1b6575507a4 subpackages: @@ -133,7 +133,7 @@ imports: - ptypes/any - ptypes/timestamp - name: github.com/google/cadvisor - version: 4dbefc9b671b81257973a33211fb12370c1a526e + version: c2ea32971ae033041f0fb0f309b1dee94fd1d55f subpackages: - api - cache/memory @@ -267,15 +267,15 @@ imports: - name: google.golang.org/appengine version: 12d5545dc1cfa6047a286d5e853841b6471f4c19 subpackages: + - urlfetch - internal + - internal/urlfetch - internal/app_identity + - internal/modules - internal/base - internal/datastore - internal/log - - internal/modules - internal/remote_api - - urlfetch - - internal/urlfetch - name: google.golang.org/cloud version: eb47ba841d53d93506cfbfbc03927daf9cc48f88 subpackages: @@ -297,40 +297,67 @@ imports: - name: gopkg.in/yaml.v2 version: a83829b6f1293c91addabc89d0571c246397bbf4 - name: k8s.io/kubernetes - version: 283137936a498aed572ee22af6774b6fb6e9fd94 + version: e7f022c926583ed8e755a52f23abc4cf8b532d12 subpackages: - pkg/api - pkg/api/meta + - pkg/api/error - pkg/client/restclient - pkg/client/unversioned + - pkg/apis/batch - pkg/client/unversioned/clientcmd - pkg/client/unversioned/fake - pkg/client/unversioned/portforward - pkg/client/unversioned/remotecommand + - pkg/kubectl - pkg/kubectl/cmd/util - pkg/kubectl/resource - pkg/labels + - pkg/runtime + - pkg/watch - pkg/api/errors + - pkg/client/unversioned/testclient - pkg/api/meta/metatypes - pkg/api/resource - pkg/api/unversioned - pkg/auth/user - pkg/conversion - pkg/fields - - pkg/runtime - pkg/runtime/serializer - pkg/types - pkg/util - pkg/util/intstr - pkg/util/rand - pkg/util/sets + - pkg/api/install + - pkg/apimachinery/registered + - pkg/apis/apps + - pkg/apis/apps/install + - pkg/apis/authentication.k8s.io/install + - pkg/apis/authorization/install + - pkg/apis/autoscaling + - pkg/apis/autoscaling/install + - pkg/apis/batch/install + - pkg/apis/batch/v2alpha1 + - pkg/apis/componentconfig/install + - pkg/apis/extensions + - pkg/apis/extensions/install + - pkg/apis/policy + - pkg/apis/policy/install + - pkg/apis/rbac + - pkg/apis/rbac/install + - pkg/client/typed/discovery + - pkg/util/net + - pkg/util/wait + - pkg/version + - plugin/pkg/client/auth + - pkg/util/validation + - pkg/util/validation/field - pkg/client/unversioned/auth - pkg/client/unversioned/clientcmd/api - pkg/client/unversioned/clientcmd/api/latest - pkg/util/errors - pkg/util/homedir - - pkg/util/validation - - pkg/util/validation/field - pkg/kubelet/server/portforward - pkg/util/httpstream - pkg/util/runtime @@ -339,42 +366,53 @@ imports: - pkg/util/httpstream/spdy - federation/apis/federation - federation/client/clientset_generated/federation_internalclientset - - pkg/api/service + - pkg/api/annotations + - pkg/api/util + - pkg/api/v1 - pkg/api/validation - - pkg/apimachinery - - pkg/apimachinery/registered - - pkg/apis/apps - - pkg/apis/autoscaling - - pkg/apis/batch - - pkg/apis/extensions - - pkg/apis/policy - - pkg/apis/rbac - - pkg/client/typed/discovery + - pkg/apis/batch/v1 + - pkg/client/clientset_generated/internalclientset - pkg/client/unversioned/adapters/internalclientset + - pkg/credentialprovider + - pkg/fieldpath + - pkg/kubelet/qos/util + - pkg/util/deployment + - pkg/util/integer + - pkg/util/jsonpath + - pkg/util/slice + - pkg/api/service + - pkg/apimachinery - pkg/controller - - pkg/kubectl - pkg/registry/thirdpartyresourcedata - pkg/runtime/serializer/json - pkg/util/flag - pkg/util/strategicpatch - - pkg/watch - pkg/util/yaml - - pkg/api/testapi - - third_party/forked/reflect - pkg/conversion/queryparams - pkg/util/json + - pkg/api/testapi + - third_party/forked/reflect - pkg/runtime/serializer/protobuf - pkg/runtime/serializer/recognizer - pkg/runtime/serializer/versioning - - pkg/util/wait - - pkg/api/v1 + - pkg/watch/versioned + - pkg/apis/apps/v1alpha1 + - pkg/apis/authentication.k8s.io + - pkg/apis/authentication.k8s.io/v1beta1 + - pkg/apis/authorization + - pkg/apis/authorization/v1beta1 + - pkg/apis/autoscaling/v1 + - pkg/apis/componentconfig + - pkg/apis/componentconfig/v1alpha1 + - pkg/apis/extensions/v1beta1 + - pkg/apis/policy/v1alpha1 + - pkg/apis/rbac/v1alpha1 - pkg/client/metrics - pkg/runtime/serializer/streaming - pkg/util/crypto - pkg/util/flowcontrol - - pkg/util/net - - pkg/version - - pkg/watch/versioned + - plugin/pkg/client/auth/gcp + - plugin/pkg/client/auth/oidc - pkg/client/unversioned/clientcmd/api/v1 - pkg/httplog - pkg/util/wsstream @@ -382,71 +420,35 @@ imports: - federation/apis/federation/install - federation/client/clientset_generated/federation_internalclientset/typed/core/unversioned - federation/client/clientset_generated/federation_internalclientset/typed/federation/unversioned - - pkg/util/net/sets + - pkg/util/parsers - pkg/api/endpoints - pkg/api/pod - pkg/api/unversioned/validation - - pkg/api/util - pkg/capabilities - - pkg/api/install - - pkg/apis/apps/install - - pkg/apis/authentication.k8s.io/install - - pkg/apis/authorization/install - - pkg/apis/autoscaling/install - - pkg/apis/batch/install - - pkg/apis/batch/v2alpha1 - - pkg/apis/componentconfig/install - - pkg/apis/extensions/install - - pkg/apis/policy/install - - pkg/apis/rbac/install - - plugin/pkg/client/auth - - pkg/client/clientset_generated/internalclientset + - pkg/client/clientset_generated/internalclientset/typed/autoscaling/unversioned - pkg/client/clientset_generated/internalclientset/typed/batch/unversioned - pkg/client/clientset_generated/internalclientset/typed/core/unversioned - pkg/client/clientset_generated/internalclientset/typed/extensions/unversioned + - pkg/client/clientset_generated/internalclientset/typed/rbac/unversioned + - pkg/util/labels + - pkg/util/pod + - pkg/util/replicaset + - third_party/golang/template + - pkg/util/net/sets - pkg/client/cache - pkg/client/record - pkg/controller/framework - pkg/util/hash - - pkg/util/integer - - pkg/api/annotations - - pkg/apis/batch/v1 - - pkg/credentialprovider - - pkg/fieldpath - - pkg/kubelet/qos/util - - pkg/util/deployment - - pkg/util/jsonpath - - pkg/util/slice - pkg/api/rest - - pkg/apis/extensions/v1beta1 - pkg/apis/extensions/validation - pkg/registry/generic - pkg/util/framer - third_party/forked/json - - pkg/util/parsers + - pkg/kubelet/qos + - pkg/master/ports - federation/apis/federation/v1beta1 - - pkg/apis/apps/v1alpha1 - - pkg/apis/authentication.k8s.io - - pkg/apis/authentication.k8s.io/v1beta1 - - pkg/apis/authorization - - pkg/apis/authorization/v1beta1 - - pkg/apis/autoscaling/v1 - - pkg/apis/componentconfig - - pkg/apis/componentconfig/v1alpha1 - - pkg/apis/policy/v1alpha1 - - pkg/apis/rbac/v1alpha1 - - plugin/pkg/client/auth/gcp - - plugin/pkg/client/auth/oidc - - pkg/client/clientset_generated/internalclientset/typed/autoscaling/unversioned - - pkg/client/clientset_generated/internalclientset/typed/rbac/unversioned - - pkg/util/labels - - pkg/util/pod - - pkg/util/replicaset - - third_party/golang/template - pkg/security/podsecuritypolicy/util - pkg/storage - - pkg/kubelet/qos - - pkg/master/ports - name: speter.net/go/exp/math/dec/inf version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 repo: https://github.com/go-inf/inf.git diff --git a/glide.yaml b/glide.yaml index aa7271cdec836b3a36f289b5936c94010f8c76eb..03917acf4c3a5c9390a03233b3d7be41a6db7bbe 100644 --- a/glide.yaml +++ b/glide.yaml @@ -22,22 +22,32 @@ import: - package: google.golang.org/grpc version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 - package: k8s.io/kubernetes - version: v1.3.0 + version: ~1.3 subpackages: - pkg/api - pkg/api/meta + - pkg/api/error + - pkg/api/unversioned + - pkg/apimachinery/registered - pkg/client/restclient - pkg/client/unversioned + - pkg/apis/batch - pkg/client/unversioned/clientcmd - pkg/client/unversioned/fake - pkg/client/unversioned/portforward - pkg/client/unversioned/remotecommand + - pkg/kubectl - pkg/kubectl/cmd/util - pkg/kubectl/resource - pkg/labels + - pkg/runtime + - pkg/watch + - pkg/util/strategicpatch + - pkg/util/yaml - package: github.com/gosuri/uitable - package: speter.net/go/exp/math/dec/inf + version: ^0.9.0 repo: https://github.com/go-inf/inf.git vcs: git - package: github.com/asaskevich/govalidator -- package: github.com/satori/go.uuid + version: ^4.0.0 diff --git a/pkg/chartutil/create.go b/pkg/chartutil/create.go index f2b4c300e45211c84e97cc98e4a6eacfccefe171..8d0990e0682e8cb4a33a0300d1865592ccf34757 100644 --- a/pkg/chartutil/create.go +++ b/pkg/chartutil/create.go @@ -48,7 +48,23 @@ const defaultIgnore = `# Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store -.git +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj ` // Create creates a new chart in a directory. diff --git a/pkg/chartutil/files.go b/pkg/chartutil/files.go index 45598de3a1cec84fa447b00a8e2649bbde736ee1..89120a42fbf248bdad060cd8a25c8b410fb0ce18 100644 --- a/pkg/chartutil/files.go +++ b/pkg/chartutil/files.go @@ -32,11 +32,14 @@ func NewFiles(from []*any.Any) Files { return files } -// Get a file by path. +// GetBytes gets a file by path. +// +// The returned data is raw. In a template context, this is identical to calling +// {{index .Files $path}}. // // This is intended to be accessed from within a template, so a missed key returns // an empty []byte. -func (f Files) Get(name string) []byte { +func (f Files) GetBytes(name string) []byte { v, ok := f[name] if !ok { return []byte{} @@ -44,10 +47,12 @@ func (f Files) Get(name string) []byte { return v } -// GetString returns a string representation of the given file. +// Get returns a string representation of the given file. +// +// Fetch the contents of a file as a string. It is designed to be called in a +// template. // -// This is a convenience for the otherwise cumbersome template logic -// for '{{.Files.Get "foo" | printf "%s"}}'. -func (f Files) GetString(name string) string { - return string(f.Get(name)) +// {{.Files.Get "foo"}} +func (f Files) Get(name string) string { + return string(f.GetBytes(name)) } diff --git a/pkg/chartutil/files_test.go b/pkg/chartutil/files_test.go index 97eb4ee0529f8866399e6e1e0011c17f0cb30216..5e162f19d820047a486ba2d295beb2c5bf50ba3b 100644 --- a/pkg/chartutil/files_test.go +++ b/pkg/chartutil/files_test.go @@ -43,10 +43,10 @@ func TestNewFiles(t *testing.T) { } for i, f := range cases { - if got := string(files.Get(f.path)); got != f.data { + if got := string(files.GetBytes(f.path)); got != f.data { t.Errorf("%d: expected %q, got %q", i, f.data, got) } - if got := files.GetString(f.path); got != f.data { + if got := files.Get(f.path); got != f.data { t.Errorf("%d: expected %q, got %q", i, f.data, got) } } diff --git a/pkg/chartutil/load.go b/pkg/chartutil/load.go index 296b11231c2cfcecc423e89f31534a078e0220be..911d883d2cf715796ebfa530a57d9eb96a3646fb 100644 --- a/pkg/chartutil/load.go +++ b/pkg/chartutil/load.go @@ -218,6 +218,11 @@ func LoadDir(dir string) (*chart.Chart, error) { return err } if fi.IsDir() { + // Directory-based ignore rules should involve skipping the entire + // contents of that directory. + if rules.Ignore(n, fi) { + return filepath.SkipDir + } return nil } diff --git a/pkg/chartutil/load_test.go b/pkg/chartutil/load_test.go index fa94acbc339c0ebc9756ed91a5e4055332a719f5..822e8d0783db554301d39f81e9ced6315c5c8252 100644 --- a/pkg/chartutil/load_test.go +++ b/pkg/chartutil/load_test.go @@ -49,8 +49,9 @@ func verifyChart(t *testing.T, c *chart.Chart) { t.Errorf("Expected 1 template, got %d", len(c.Templates)) } - if len(c.Files) != 5 { - t.Errorf("Expected 5 extra files, got %d", len(c.Files)) + numfiles := 6 + if len(c.Files) != numfiles { + t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files)) for _, n := range c.Files { t.Logf("\t%s", n.TypeUrl) } diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz index 31c855223046ed3bba9abb2a7ca707195ad6daa4..50d1ef01484094a445c867e612e363f4f527d604 100644 Binary files a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz and b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/.helmignore b/pkg/chartutil/testdata/frobnitz/.helmignore new file mode 100644 index 0000000000000000000000000000000000000000..9973a57b8035d98658162558e488aa24f7d8fa15 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz index 90c7979afe88fb36ef00f6fe09041217b874323f..27bd51f654f64fa50e431c35bd69c9e7d509a5c1 100644 Binary files a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/ignore/me.txt b/pkg/chartutil/testdata/frobnitz/ignore/me.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/pkg/chartutil/testdata/genfrob.sh b/pkg/chartutil/testdata/genfrob.sh index 38fc1b22c959826615ed5f6e244465a540d7b6d5..8f2cddec1a4da9ed6610f5d033d1835d5aaf9e13 100755 --- a/pkg/chartutil/testdata/genfrob.sh +++ b/pkg/chartutil/testdata/genfrob.sh @@ -9,4 +9,4 @@ tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner # Pack the frobnitz chart. echo "Packing frobnitz" -tar -zcvf frobnitz-1.2.3.tgz frobnitz +tar --exclude=ignore/* -zcvf frobnitz-1.2.3.tgz frobnitz diff --git a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz index 3d5b6a2420ffcdf46d8f9de74f056c4d7e47bb9e..c35e1b9701734924198f1739129a9563f75ba7cd 100644 Binary files a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz and b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz differ diff --git a/pkg/client/install.go b/pkg/client/install.go index 1ffbb5b775be5dfcbca663408c5dac612d2a3282..0b7a3b1fc4102e45954cc35c30c92b4e218e95d2 100644 --- a/pkg/client/install.go +++ b/pkg/client/install.go @@ -66,19 +66,13 @@ func Install(namespace, image string, verbose bool) error { // InstallYAML is the installation YAML for DM. const InstallYAML = ` --- -apiVersion: v1 -kind: ReplicationController +apiVersion: extensions/v1beta1 +kind: Deployment metadata: - labels: - app: helm - name: tiller - name: tiller-rc + name: tiller-deploy namespace: {{ .Namespace }} spec: replicas: 1 - selector: - app: helm - name: tiller template: metadata: labels: diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index cb88088e6e448aeb6499fd210735ac2f2f3a1db4..73b8dabad0b68bc3d35ccff6297e8fb1ff2165d9 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -88,6 +88,28 @@ type renderable struct { vals chartutil.Values } +// alterFuncMap takes the Engine's FuncMap and adds context-specific functions. +// +// The resulting FuncMap is only valid for the passed-in template. +func (e *Engine) alterFuncMap(t *template.Template) template.FuncMap { + // Clone the func map because we are adding context-specific functions. + var funcMap template.FuncMap = map[string]interface{}{} + for k, v := range e.FuncMap { + funcMap[k] = v + } + + // Add the 'include' function here so we can close over t. + funcMap["include"] = func(name string, data interface{}) string { + buf := bytes.NewBuffer(nil) + if err := t.ExecuteTemplate(buf, name, data); err != nil { + buf.WriteString(err.Error()) + } + return buf.String() + } + + return funcMap +} + // render takes a map of templates/values and renders them. func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) { // Basically, what we do here is start with an empty parent template and then @@ -105,10 +127,13 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) { // but will still emit <no value> for others. We mitigate that later. t.Option("missingkey=zero") } + + funcMap := e.alterFuncMap(t) + files := []string{} for fname, r := range tpls { log.Printf("Preparing template %s", fname) - t = t.New(fname).Funcs(e.FuncMap) + t = t.New(fname).Funcs(funcMap) if _, err := t.Parse(r.tpl); err != nil { return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err) } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 03ee083dc1ff5f13e74d16a9565e51e9d0d9b2d6..ec19f8ded62cd8601b4d68171d15e581d1f72b36 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -312,7 +312,7 @@ func TestRenderBuiltinValues(t *testing.T) { Metadata: &chart.Metadata{Name: "Latium"}, Templates: []*chart.Template{ {Name: "Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, - {Name: "From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.GetString "book/title.txt"}}`)}, + {Name: "From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, }, Values: &chart.Config{Raw: ``}, Dependencies: []*chart.Chart{}, @@ -358,3 +358,33 @@ func TestRenderBuiltinValues(t *testing.T) { } } + +func TestAlterFuncMap(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "conrad"}, + Templates: []*chart.Template{ + {Name: "quote", Data: []byte(`{{include "conrad/_partial" . | indent 2}} dead.`)}, + {Name: "_partial", Data: []byte(`{{.Release.Name}} - he`)}, + }, + Values: &chart.Config{Raw: ``}, + Dependencies: []*chart.Chart{}, + } + + v := chartutil.Values{ + "Values": &chart.Config{Raw: ""}, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "Mistah Kurtz", + }, + } + + out, err := New().Render(c, v) + if err != nil { + t.Fatal(err) + } + + expect := " Mistah Kurtz - he dead." + if got := out["conrad/quote"]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, out) + } +} diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 98a6efc19fe2515fe57e5825673977560b8387e4..2af4742da22cf84bb25eab46024de9637461b612 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -143,6 +143,13 @@ func DeleteDryRun(dry bool) DeleteOption { } } +// UpgradeDisableHooks will disable hooks for an upgrade operation. +func UpgradeDisableHooks(disable bool) UpdateOption { + return func(opts *options) { + opts.disableHooks = disable + } +} + // UpgradeDryRun will (if true) execute an upgrade as a dry run. func UpgradeDryRun(dry bool) UpdateOption { return func(opts *options) { @@ -237,9 +244,15 @@ func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, // Executes tiller.UpdateRelease RPC. func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.ReleaseServiceClient, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { - //TODO: handle dryRun + for _, opt := range opts { + opt(o) + } + + o.updateReq.Chart = chr + o.updateReq.DryRun = o.dryRun + o.updateReq.Name = rlsName - return rlc.UpdateRelease(context.TODO(), &rls.UpdateReleaseRequest{Name: rlsName, Chart: chr}) + return rlc.UpdateRelease(context.TODO(), &o.updateReq) } // Executes tiller.GetReleaseStatus RPC. diff --git a/pkg/ignore/rules.go b/pkg/ignore/rules.go index 0251268baf899ad974821c409590e4d3385b80aa..f5b08a4eebeff7d4fd7c494175c44de06bcfa831 100644 --- a/pkg/ignore/rules.go +++ b/pkg/ignore/rules.go @@ -65,7 +65,6 @@ func Parse(file io.Reader) (*Rules, error) { if err := s.Err(); err != nil { return r, err } - return r, nil } @@ -97,8 +96,10 @@ func (r *Rules) Ignore(path string, fi os.FileInfo) bool { continue } + // If the rule is looking for directories, and this is not a directory, + // skip it. if p.mustDir && !fi.IsDir() { - return false + continue } if p.match(path, fi) { return true diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 4e0c519086f67ab542865a7db8700e638111d928..da073d173f169d848c789e6e5f3a6a4f0925f6af 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -20,15 +20,21 @@ import ( "fmt" "io" "log" + "reflect" "time" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/strategicpatch" + "k8s.io/kubernetes/pkg/util/yaml" "k8s.io/kubernetes/pkg/watch" ) @@ -57,6 +63,77 @@ func (c *Client) Create(namespace string, reader io.Reader) error { return perform(c, namespace, reader, createResource) } +// Update reads in the current configuration and a modified configuration from io.reader +// and creates resources that don't already exists, updates resources that have been modified +// and deletes resources from the current configuration that are not present in the +// modified configuration +// +// Namespace will set the namespaces +func (c *Client) Update(namespace string, currentReader, modifiedReader io.Reader) error { + current := c.NewBuilder(includeThirdPartyAPIs). + ContinueOnError(). + NamespaceParam(namespace). + DefaultNamespace(). + Stream(currentReader, ""). + Flatten(). + Do() + + modified := c.NewBuilder(includeThirdPartyAPIs). + ContinueOnError(). + NamespaceParam(namespace). + DefaultNamespace(). + Stream(modifiedReader, ""). + Flatten(). + Do() + + currentInfos, err := current.Infos() + if err != nil { + return err + } + + modifiedInfos := []*resource.Info{} + + modified.Visit(func(info *resource.Info, err error) error { + modifiedInfos = append(modifiedInfos, info) + if err != nil { + return err + } + resourceName := info.Name + + helper := resource.NewHelper(info.Client, info.Mapping) + if _, err := helper.Get(info.Namespace, resourceName, info.Export); err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("Could not get information about the resource: err: %s", err) + } + + // Since the resource does not exist, create it. + if err := createResource(info); err != nil { + return err + } + + kind := info.Mapping.GroupVersionKind.Kind + log.Printf("Created a new %s called %s\n", kind, resourceName) + return nil + } + + currentObj, err := getCurrentObject(resourceName, currentInfos) + if err != nil { + return err + } + + if err := updateResource(info, currentObj); err != nil { + log.Printf("error updating the resource %s:\n\t %v", resourceName, err) + return err + } + + return err + }) + + deleteUnwantedResources(currentInfos, modifiedInfos) + + return nil +} + // Delete deletes kubernetes resources from an io.reader // // Namespace will set the namespace @@ -136,6 +213,51 @@ func createResource(info *resource.Info) error { return err } +func deleteResource(info *resource.Info) error { + return resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name) +} + +func updateResource(modified *resource.Info, currentObj runtime.Object) error { + + encoder := api.Codecs.LegacyCodec(registered.EnabledVersions()...) + originalSerialization, err := runtime.Encode(encoder, currentObj) + if err != nil { + return err + } + + editedSerialization, err := runtime.Encode(encoder, modified.Object) + if err != nil { + return err + } + + originalJS, err := yaml.ToJSON(originalSerialization) + if err != nil { + return err + } + + editedJS, err := yaml.ToJSON(editedSerialization) + if err != nil { + return err + } + + if reflect.DeepEqual(originalJS, editedJS) { + return fmt.Errorf("Looks like there are no changes for %s", modified.Name) + } + + patch, err := strategicpatch.CreateStrategicMergePatch(originalJS, editedJS, currentObj) + if err != nil { + return err + } + + // send patch to server + helper := resource.NewHelper(modified.Client, modified.Mapping) + if _, err = helper.Patch(modified.Namespace, modified.Name, api.StrategicMergePatchType, patch); err != nil { + return err + } + + return nil +} + func watchUntilReady(info *resource.Info) error { w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion) if err != nil { @@ -213,3 +335,37 @@ func (c *Client) ensureNamespace(namespace string) error { } return nil } + +func deleteUnwantedResources(currentInfos, modifiedInfos []*resource.Info) { + for _, cInfo := range currentInfos { + found := false + for _, m := range modifiedInfos { + if m.Name == cInfo.Name { + found = true + } + } + if !found { + log.Printf("Deleting %s...", cInfo.Name) + if err := deleteResource(cInfo); err != nil { + log.Printf("Failed to delete %s, err: %s", cInfo.Name, err) + } + } + } +} + +func getCurrentObject(targetName string, infos []*resource.Info) (runtime.Object, error) { + var curr *resource.Info + for _, currInfo := range infos { + if currInfo.Name == targetName { + curr = currInfo + } + } + + if curr == nil { + return nil, fmt.Errorf("No resource with the name %s found.", targetName) + } + + encoder := api.Codecs.LegacyCodec(registered.EnabledVersions()...) + defaultVersion := unversioned.GroupVersion{} + return resource.AsVersionedObject([]*resource.Info{curr}, false, defaultVersion, encoder) +} diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 5ef8b913eea738844ddd5ca2bf115a94ae4caad7..c59cc37b0acd0dbbc2e9dc7bdbca35d9f3cee5d3 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -17,15 +17,55 @@ limitations under the License. package kube import ( + "bytes" + "encoding/json" "io" + "io/ioutil" + "net/http" "strings" "testing" "k8s.io/kubernetes/pkg/api/meta" + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/api/unversioned" + api "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/client/unversioned/fake" "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/runtime" ) +func TestUpdateResource(t *testing.T) { + + tests := []struct { + name string + namespace string + modified *resource.Info + currentObj runtime.Object + err bool + errMessage string + }{ + { + name: "no changes when updating resources", + modified: createFakeInfo("nginx", nil), + currentObj: createFakePod("nginx", nil), + err: true, + errMessage: "Looks like there are no changes for nginx", + }, + //{ + //name: "valid update input", + //modified: createFakeInfo("nginx", map[string]string{"app": "nginx"}), + //currentObj: createFakePod("nginx", nil), + //}, + } + + for _, tt := range tests { + err := updateResource(tt.modified, tt.currentObj) + if err != nil && err.Error() != tt.errMessage { + t.Errorf("%q. expected error message: %v, got %v", tt.name, tt.errMessage, err) + } + } +} + func TestPerform(t *testing.T) { tests := []struct { name string @@ -214,3 +254,53 @@ spec: ports: - containerPort: 80 ` + +func createFakePod(name string, labels map[string]string) runtime.Object { + objectMeta := createObjectMeta(name, labels) + + object := &api.Pod{ + ObjectMeta: objectMeta, + } + + return object +} + +func createFakeInfo(name string, labels map[string]string) *resource.Info { + pod := createFakePod(name, labels) + marshaledObj, _ := json.Marshal(pod) + + mapping := &meta.RESTMapping{ + Resource: name, + Scope: meta.RESTScopeNamespace, + GroupVersionKind: unversioned.GroupVersionKind{ + Kind: "Pod", + Version: "v1", + }} + + client := &fake.RESTClient{ + Codec: testapi.Default.Codec(), + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + header := http.Header{} + header.Set("Content-Type", runtime.ContentTypeJSON) + return &http.Response{ + StatusCode: 200, + Header: header, + Body: ioutil.NopCloser(bytes.NewReader(marshaledObj)), + }, nil + })} + info := resource.NewInfo(client, mapping, "default", "nginx", false) + + info.Object = pod + + return info +} + +func createObjectMeta(name string, labels map[string]string) api.ObjectMeta { + objectMeta := api.ObjectMeta{Name: name, Namespace: "default"} + + if labels != nil { + objectMeta.Labels = labels + } + + return objectMeta +} diff --git a/pkg/kube/tunnel.go b/pkg/kube/tunnel.go index ff3b76b4202893789c0828d7a15df8f490e8e582..66406d512e27d570c97eec3e65a633c300d08753 100644 --- a/pkg/kube/tunnel.go +++ b/pkg/kube/tunnel.go @@ -51,7 +51,7 @@ func (c *Client) ForwardPort(namespace, podName string, remote int) (*Tunnel, er } // Build a url to the portforward endpoing - // example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-rc-9itlq/portforward + // example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-deploy-9itlq/portforward u := client.RESTClient.Post(). Resource("pods"). Namespace(namespace). diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index d1cc19ae4f50f95ad2d3ae070351f06ca63a55c7..838e83647e6919a02c24504eba4062bd6f4960ce 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -226,6 +226,8 @@ type UpdateReleaseRequest struct { Values *hapi_chart.Config `protobuf:"bytes,3,opt,name=values" json:"values,omitempty"` // dry_run, if true, will run through the release logic, but neither create DryRun bool `protobuf:"varint,4,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"` + // DisableHooks causes the server to skip running any hooks for the upgrade. + DisableHooks bool `protobuf:"varint,5,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"` } func (m *UpdateReleaseRequest) Reset() { *m = UpdateReleaseRequest{} } @@ -624,54 +626,54 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ } var fileDescriptor0 = []byte{ - // 769 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdb, 0x6e, 0xd3, 0x4a, - 0x14, 0xad, 0x93, 0xd4, 0x49, 0x76, 0x2f, 0x4a, 0xe7, 0xb4, 0x8d, 0x8f, 0x75, 0x0e, 0x42, 0x46, - 0x40, 0x29, 0xd4, 0x81, 0xf0, 0x8e, 0x94, 0xb6, 0x51, 0x5b, 0x35, 0xa4, 0xd2, 0x84, 0x82, 0xc4, - 0x03, 0x91, 0x9b, 0x4c, 0xa8, 0xc1, 0xb5, 0x83, 0x67, 0x52, 0xd1, 0x4f, 0xe0, 0x0f, 0xf8, 0x14, - 0x3e, 0x88, 0xbf, 0xe0, 0x85, 0xb9, 0xd8, 0x26, 0x17, 0x1b, 0x4c, 0x5f, 0x9c, 0x99, 0xd9, 0x6b, - 0xaf, 0x7d, 0xdf, 0x0a, 0x98, 0x97, 0xce, 0xd8, 0x6d, 0x50, 0x12, 0x5e, 0xbb, 0x03, 0x42, 0x1b, - 0xcc, 0xf5, 0x3c, 0x12, 0xda, 0xe3, 0x30, 0x60, 0x01, 0xda, 0x14, 0x32, 0x3b, 0x96, 0xd9, 0x4a, - 0x66, 0x6e, 0x4b, 0x8d, 0xc1, 0xa5, 0x13, 0x32, 0xf5, 0x55, 0x68, 0xb3, 0x3e, 0xfd, 0x1e, 0xf8, - 0x23, 0xf7, 0x7d, 0x24, 0x50, 0x26, 0x42, 0xe2, 0x11, 0x87, 0x92, 0xf8, 0x77, 0x46, 0x29, 0x96, - 0xb9, 0xfe, 0x28, 0x50, 0x02, 0xeb, 0xbb, 0x06, 0xff, 0x74, 0x5c, 0xca, 0xb0, 0x12, 0x51, 0x4c, - 0x3e, 0x4d, 0x08, 0x65, 0x68, 0x13, 0x96, 0x3d, 0xf7, 0xca, 0x65, 0x86, 0x76, 0x57, 0xdb, 0x29, - 0x62, 0x75, 0x41, 0xdb, 0xa0, 0x07, 0xa3, 0x11, 0x25, 0xcc, 0x28, 0xf0, 0xe7, 0x2a, 0x8e, 0x6e, - 0xe8, 0x05, 0x94, 0x69, 0x10, 0xb2, 0xfe, 0xc5, 0x8d, 0x51, 0xe4, 0x82, 0xf5, 0xe6, 0x7d, 0x3b, - 0x2d, 0x26, 0x5b, 0x58, 0xea, 0x71, 0xa0, 0x2d, 0x3e, 0xfb, 0x37, 0x58, 0xa7, 0xf2, 0x57, 0xf0, - 0x8e, 0x5c, 0x8f, 0x91, 0xd0, 0x28, 0x29, 0x5e, 0x75, 0x43, 0x47, 0x00, 0x92, 0x37, 0x08, 0x87, - 0x5c, 0xb6, 0x2c, 0xa9, 0x77, 0x72, 0x50, 0x9f, 0x09, 0x3c, 0xae, 0xd2, 0xf8, 0x68, 0xbd, 0x83, - 0x4a, 0x0c, 0xb0, 0x9a, 0xa0, 0x2b, 0xf3, 0x68, 0x05, 0xca, 0xe7, 0xdd, 0xd3, 0xee, 0xd9, 0x9b, - 0x6e, 0x6d, 0x09, 0x55, 0xa0, 0xd4, 0x6d, 0xbd, 0x6c, 0xd7, 0x34, 0xb4, 0x01, 0x6b, 0x9d, 0x56, - 0xef, 0x55, 0x1f, 0xb7, 0x3b, 0xed, 0x56, 0xaf, 0x7d, 0x58, 0x2b, 0x58, 0x77, 0xa0, 0x9a, 0xf0, - 0xa2, 0x32, 0x14, 0x5b, 0xbd, 0x03, 0xa5, 0x72, 0xd8, 0xe6, 0x27, 0xcd, 0xfa, 0xa2, 0xc1, 0xe6, - 0x6c, 0x1a, 0xe9, 0x38, 0xf0, 0x29, 0x11, 0x79, 0x1c, 0x04, 0x13, 0x3f, 0xc9, 0xa3, 0xbc, 0x20, - 0x04, 0x25, 0x9f, 0x7c, 0x8e, 0xb3, 0x28, 0xcf, 0x02, 0xc9, 0x02, 0xe6, 0x78, 0x32, 0x83, 0x1c, - 0x29, 0x2f, 0xe8, 0x19, 0x54, 0xa2, 0xaa, 0x51, 0x9e, 0x9b, 0xe2, 0xce, 0x4a, 0x73, 0x4b, 0xc5, - 0x1f, 0xd7, 0x37, 0xb2, 0x88, 0x13, 0x98, 0xb5, 0x07, 0xf5, 0x23, 0x12, 0x7b, 0xd2, 0x63, 0x0e, - 0x9b, 0x24, 0x55, 0x15, 0x76, 0x9d, 0x2b, 0x22, 0x9d, 0x11, 0x76, 0xf9, 0xd9, 0x7a, 0x0d, 0xc6, - 0x22, 0x3c, 0xf2, 0x3e, 0x05, 0x8f, 0x1e, 0x40, 0x49, 0xf4, 0x8f, 0xf4, 0x7d, 0xa5, 0x89, 0x66, - 0xbd, 0x39, 0xe1, 0x12, 0x2c, 0xe5, 0x96, 0x3d, 0xcd, 0x7b, 0x10, 0xf8, 0x8c, 0xf8, 0xec, 0x77, - 0x7e, 0x74, 0xe0, 0xdf, 0x14, 0x7c, 0xe4, 0x48, 0x03, 0xca, 0x91, 0x09, 0xa9, 0x93, 0x99, 0x85, - 0x18, 0x65, 0x7d, 0xe5, 0x05, 0x39, 0x1f, 0x0f, 0x1d, 0x46, 0x62, 0x51, 0xb6, 0x69, 0xf4, 0x90, - 0x17, 0x49, 0xcc, 0x53, 0x14, 0xd3, 0x86, 0xe2, 0x56, 0x43, 0x77, 0x20, 0xbe, 0x58, 0xc9, 0xd1, - 0x2e, 0xe8, 0xd7, 0x8e, 0xc7, 0x79, 0x64, 0x91, 0x92, 0xe8, 0x23, 0xa4, 0x1c, 0x46, 0x1c, 0x21, - 0x50, 0x1d, 0xca, 0xc3, 0xf0, 0xa6, 0x1f, 0x4e, 0x7c, 0xd9, 0xd4, 0x15, 0xac, 0xf3, 0x2b, 0x9e, - 0xf8, 0xd6, 0x31, 0x6c, 0xcd, 0x79, 0x76, 0xdb, 0x20, 0x7f, 0x68, 0xb0, 0x75, 0xe2, 0x53, 0xde, - 0x27, 0xde, 0x5c, 0x94, 0x49, 0x44, 0x5a, 0xee, 0x88, 0x0a, 0x7f, 0x13, 0x51, 0x71, 0x3a, 0xa2, - 0x24, 0xa7, 0xa5, 0xa9, 0x9c, 0xde, 0x83, 0xb5, 0xa1, 0x4b, 0x9d, 0x0b, 0x8f, 0xf4, 0x2f, 0x83, - 0xe0, 0x23, 0x95, 0xd3, 0x5b, 0xc1, 0xab, 0xd1, 0xe3, 0xb1, 0x78, 0x43, 0xff, 0x41, 0x55, 0x80, - 0xe9, 0xd8, 0x19, 0x10, 0x43, 0x97, 0xda, 0xbf, 0x1e, 0xd0, 0xff, 0x00, 0x21, 0x99, 0x50, 0xd2, - 0x97, 0xe4, 0x65, 0xa9, 0x5f, 0x95, 0x2f, 0x5d, 0xd1, 0x30, 0x27, 0xb0, 0x3d, 0x1f, 0xfc, 0x6d, - 0x13, 0x89, 0xa1, 0x7e, 0xee, 0xbb, 0xa9, 0x99, 0x4c, 0xeb, 0x97, 0x85, 0xd8, 0x0a, 0x8b, 0xb1, - 0x59, 0xa7, 0x60, 0x2c, 0x72, 0xde, 0xd2, 0xc1, 0xe6, 0xb7, 0x65, 0x58, 0x8f, 0x47, 0x54, 0x2d, - 0x3e, 0xe4, 0xc2, 0xea, 0xf4, 0xc6, 0x41, 0x8f, 0xb2, 0xf7, 0xe2, 0xdc, 0x72, 0x37, 0x77, 0xf3, - 0x40, 0x95, 0xab, 0xd6, 0xd2, 0x53, 0x0d, 0x51, 0xa8, 0xcd, 0xaf, 0x08, 0xb4, 0x97, 0xce, 0x91, - 0xb1, 0x79, 0x4c, 0x3b, 0x2f, 0x3c, 0x36, 0x8b, 0xae, 0x61, 0x63, 0x61, 0x1f, 0xa0, 0x3f, 0xd2, - 0xcc, 0x2e, 0x1a, 0xb3, 0x91, 0x1b, 0x9f, 0xd8, 0xfd, 0x00, 0x6b, 0x33, 0xe3, 0x89, 0x32, 0xb2, - 0x95, 0xb6, 0x5d, 0xcc, 0xc7, 0xb9, 0xb0, 0x89, 0xad, 0x2b, 0x58, 0x9f, 0x6d, 0x61, 0x94, 0x41, - 0x90, 0x3a, 0xe5, 0xe6, 0x93, 0x7c, 0xe0, 0xc4, 0x1c, 0xaf, 0xe3, 0x7c, 0x4b, 0x66, 0xd5, 0x31, - 0x63, 0x1c, 0xb2, 0xea, 0x98, 0xd5, 0xe9, 0xd6, 0xd2, 0x3e, 0xbc, 0xad, 0xc4, 0xe8, 0x0b, 0x5d, - 0xfe, 0xe9, 0x78, 0xfe, 0x33, 0x00, 0x00, 0xff, 0xff, 0xa6, 0x56, 0x6f, 0xa5, 0x0e, 0x09, 0x00, - 0x00, + // 774 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x56, 0xdb, 0x6e, 0xd3, 0x4c, + 0x10, 0xae, 0x93, 0x34, 0x87, 0xe9, 0x41, 0xe9, 0xfe, 0x6d, 0x93, 0xdf, 0xfa, 0x7f, 0x84, 0x8c, + 0x80, 0x52, 0xa8, 0x03, 0xe1, 0x1e, 0x29, 0x6d, 0xa3, 0xb6, 0x6a, 0x48, 0xa5, 0x0d, 0x05, 0x89, + 0x0b, 0x22, 0x37, 0xd9, 0x50, 0x83, 0x6b, 0x07, 0xef, 0xa6, 0xa2, 0x8f, 0xc0, 0x1b, 0x71, 0xc3, + 0xdb, 0xf0, 0x16, 0xdc, 0xb0, 0x07, 0xaf, 0xc9, 0xc1, 0x06, 0xd3, 0x1b, 0x67, 0x77, 0xe7, 0xdb, + 0x6f, 0x66, 0xbe, 0x99, 0x9d, 0x16, 0xcc, 0x4b, 0x67, 0xec, 0x36, 0x28, 0x09, 0xaf, 0xdd, 0x01, + 0xa1, 0x0d, 0xe6, 0x7a, 0x1e, 0x09, 0xed, 0x71, 0x18, 0xb0, 0x00, 0x6d, 0x0a, 0x9b, 0xad, 0x6d, + 0xb6, 0xb2, 0x99, 0xdb, 0xf2, 0xc6, 0xe0, 0xd2, 0x09, 0x99, 0xfa, 0x2a, 0xb4, 0x59, 0x9b, 0x3e, + 0x0f, 0xfc, 0x91, 0xfb, 0x3e, 0x32, 0x28, 0x17, 0x21, 0xf1, 0x88, 0x43, 0x89, 0xfe, 0x9d, 0xb9, + 0xa4, 0x6d, 0xae, 0x3f, 0x0a, 0x94, 0xc1, 0xfa, 0x6e, 0xc0, 0x3f, 0x1d, 0x97, 0x32, 0xac, 0x4c, + 0x14, 0x93, 0x4f, 0x13, 0x42, 0x19, 0xda, 0x84, 0x65, 0xcf, 0xbd, 0x72, 0x59, 0xdd, 0xb8, 0x6b, + 0xec, 0xe4, 0xb1, 0xda, 0xa0, 0x6d, 0x28, 0x06, 0xa3, 0x11, 0x25, 0xac, 0x9e, 0xe3, 0xc7, 0x15, + 0x1c, 0xed, 0xd0, 0x0b, 0x28, 0xd1, 0x20, 0x64, 0xfd, 0x8b, 0x9b, 0x7a, 0x9e, 0x1b, 0xd6, 0x9b, + 0xf7, 0xed, 0xa4, 0x9c, 0x6c, 0xe1, 0xa9, 0xc7, 0x81, 0xb6, 0xf8, 0xec, 0xdf, 0xe0, 0x22, 0x95, + 0xbf, 0x82, 0x77, 0xe4, 0x7a, 0x8c, 0x84, 0xf5, 0x82, 0xe2, 0x55, 0x3b, 0x74, 0x04, 0x20, 0x79, + 0x83, 0x70, 0xc8, 0x6d, 0xcb, 0x92, 0x7a, 0x27, 0x03, 0xf5, 0x99, 0xc0, 0xe3, 0x0a, 0xd5, 0x4b, + 0xeb, 0x1d, 0x94, 0x35, 0xc0, 0x6a, 0x42, 0x51, 0xb9, 0x47, 0x2b, 0x50, 0x3a, 0xef, 0x9e, 0x76, + 0xcf, 0xde, 0x74, 0xab, 0x4b, 0xa8, 0x0c, 0x85, 0x6e, 0xeb, 0x65, 0xbb, 0x6a, 0xa0, 0x0d, 0x58, + 0xeb, 0xb4, 0x7a, 0xaf, 0xfa, 0xb8, 0xdd, 0x69, 0xb7, 0x7a, 0xed, 0xc3, 0x6a, 0xce, 0xba, 0x03, + 0x95, 0x98, 0x17, 0x95, 0x20, 0xdf, 0xea, 0x1d, 0xa8, 0x2b, 0x87, 0x6d, 0xbe, 0x32, 0xac, 0x2f, + 0x06, 0x6c, 0xce, 0xca, 0x48, 0xc7, 0x81, 0x4f, 0x89, 0xd0, 0x71, 0x10, 0x4c, 0xfc, 0x58, 0x47, + 0xb9, 0x41, 0x08, 0x0a, 0x3e, 0xf9, 0xac, 0x55, 0x94, 0x6b, 0x81, 0x64, 0x01, 0x73, 0x3c, 0xa9, + 0x20, 0x47, 0xca, 0x0d, 0x7a, 0x06, 0xe5, 0xa8, 0x6a, 0x94, 0x6b, 0x93, 0xdf, 0x59, 0x69, 0x6e, + 0xa9, 0xfc, 0x75, 0x7d, 0x23, 0x8f, 0x38, 0x86, 0x59, 0x7b, 0x50, 0x3b, 0x22, 0x3a, 0x92, 0x1e, + 0x73, 0xd8, 0x24, 0xae, 0xaa, 0xf0, 0xeb, 0x5c, 0x11, 0x19, 0x8c, 0xf0, 0xcb, 0xd7, 0xd6, 0x6b, + 0xa8, 0x2f, 0xc2, 0xa3, 0xe8, 0x13, 0xf0, 0xe8, 0x01, 0x14, 0x44, 0xff, 0xc8, 0xd8, 0x57, 0x9a, + 0x68, 0x36, 0x9a, 0x13, 0x6e, 0xc1, 0xd2, 0x6e, 0xd9, 0xd3, 0xbc, 0x07, 0x81, 0xcf, 0x88, 0xcf, + 0x7e, 0x17, 0x47, 0x07, 0xfe, 0x4d, 0xc0, 0x47, 0x81, 0x34, 0xa0, 0x14, 0xb9, 0x90, 0x77, 0x52, + 0x55, 0xd0, 0x28, 0xeb, 0x1b, 0x2f, 0xc8, 0xf9, 0x78, 0xe8, 0x30, 0xa2, 0x4d, 0xe9, 0xae, 0xd1, + 0x43, 0x5e, 0x24, 0xf1, 0x9e, 0xa2, 0x9c, 0x36, 0x14, 0xb7, 0x7a, 0x74, 0x07, 0xe2, 0x8b, 0x95, + 0x1d, 0xed, 0x42, 0xf1, 0xda, 0xf1, 0x38, 0x8f, 0x2c, 0x52, 0x9c, 0x7d, 0x84, 0x94, 0x8f, 0x11, + 0x47, 0x08, 0x54, 0x83, 0xd2, 0x30, 0xbc, 0xe9, 0x87, 0x13, 0x5f, 0x36, 0x75, 0x19, 0x17, 0xf9, + 0x16, 0x4f, 0x7c, 0x74, 0x0f, 0xd6, 0x86, 0x2e, 0x75, 0x2e, 0x3c, 0xd2, 0xbf, 0x0c, 0x82, 0x8f, + 0x54, 0xf6, 0x75, 0x19, 0xaf, 0x46, 0x87, 0xc7, 0xe2, 0xcc, 0x3a, 0x86, 0xad, 0xb9, 0xf0, 0x6f, + 0xab, 0xc4, 0x0f, 0x03, 0xb6, 0x4e, 0x7c, 0xca, 0x9b, 0xc9, 0x9b, 0x93, 0x22, 0x4e, 0xdb, 0xc8, + 0x9c, 0x76, 0xee, 0x6f, 0xd2, 0xce, 0xcf, 0xa4, 0xad, 0x85, 0x2f, 0x4c, 0x09, 0x9f, 0x45, 0x0a, + 0xf4, 0x1f, 0x54, 0x04, 0x98, 0x8e, 0x9d, 0x01, 0xa9, 0x17, 0xe5, 0xed, 0x5f, 0x07, 0xe8, 0x7f, + 0x80, 0x90, 0x4c, 0x28, 0xe9, 0x4b, 0xf2, 0x92, 0xbc, 0x5f, 0x91, 0x27, 0x5d, 0xd1, 0x55, 0x27, + 0xb0, 0x3d, 0x9f, 0xfc, 0x6d, 0x85, 0xc4, 0x50, 0x3b, 0xf7, 0xdd, 0x44, 0x25, 0x93, 0x9a, 0x6a, + 0x21, 0xb7, 0x5c, 0x42, 0x99, 0x4f, 0xa1, 0xbe, 0xc8, 0x79, 0xcb, 0x00, 0x9b, 0x5f, 0x97, 0x61, + 0x5d, 0xbf, 0x63, 0x35, 0x1d, 0x91, 0x0b, 0xab, 0xd3, 0x63, 0x09, 0x3d, 0x4a, 0x1f, 0x9e, 0x73, + 0x7f, 0x01, 0xcc, 0xdd, 0x2c, 0x50, 0x15, 0xaa, 0xb5, 0xf4, 0xd4, 0x40, 0x14, 0xaa, 0xf3, 0x73, + 0x04, 0xed, 0x25, 0x73, 0xa4, 0x8c, 0x27, 0xd3, 0xce, 0x0a, 0xd7, 0x6e, 0xd1, 0x35, 0x6c, 0x2c, + 0x0c, 0x0d, 0xf4, 0x47, 0x9a, 0xd9, 0x69, 0x64, 0x36, 0x32, 0xe3, 0x63, 0xbf, 0x1f, 0x60, 0x6d, + 0xe6, 0x79, 0xa2, 0x14, 0xb5, 0x92, 0x46, 0x90, 0xf9, 0x38, 0x13, 0x36, 0xf6, 0x75, 0x05, 0xeb, + 0xb3, 0x2d, 0x8c, 0x52, 0x08, 0x12, 0x5f, 0xb9, 0xf9, 0x24, 0x1b, 0x38, 0x76, 0xc7, 0xeb, 0x38, + 0xdf, 0x92, 0x69, 0x75, 0x4c, 0x79, 0x0e, 0x69, 0x75, 0x4c, 0xeb, 0x74, 0x6b, 0x69, 0x1f, 0xde, + 0x96, 0x35, 0xfa, 0xa2, 0x28, 0xff, 0x33, 0x79, 0xfe, 0x33, 0x00, 0x00, 0xff, 0xff, 0x97, 0xd7, + 0x36, 0xd6, 0x33, 0x09, 0x00, 0x00, } diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index f8b4267f4b123ebbdc3ea54c76a3128e4f00a30f..c4ed13c52162044ed3953b8ecec9595bcedd5fc5 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -98,8 +98,7 @@ func LoadChartRepository(dir, url string) (*ChartRepository, error) { return nil } r.IndexFile = i - } else { - // TODO: check for tgz extension + } else if strings.HasSuffix(f.Name(), ".tgz") { r.ChartPaths = append(r.ChartPaths, path) } }