diff --git a/README.md b/README.md index d27e0031f..a16bf2706 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Kubernetes Helm +[![CircleCI](https://circleci.com/gh/kubernetes/helm.svg?style=svg)](https://circleci.com/gh/kubernetes/helm) + Helm is a tool for managing Kubernetes charts. Charts are packages of pre-configured Kubernetes resources. diff --git a/_proto/hapi/release/hook.proto b/_proto/hapi/release/hook.proto new file mode 100644 index 000000000..56918230a --- /dev/null +++ b/_proto/hapi/release/hook.proto @@ -0,0 +1,45 @@ +// 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. + +syntax = "proto3"; + +package hapi.release; + +import "google/protobuf/timestamp.proto"; + +option go_package = "release"; + +// Hook defines a hook object. +message Hook { + enum Event { + UNKNOWN = 0; + PRE_INSTALL = 1; + POST_INSTALL = 2; + PRE_DELETE = 3; + POST_DELETE = 4; + PRE_UPGRADE = 5; + POST_UPGRADE = 6; + } + string name = 1; + // Kind is the Kubernetes kind. + string kind = 2; + // Path is the chart-relative path to the template. + string path = 3; + // Manifest is the manifest contents. + string manifest = 4; + // Events are the events that this hook fires on. + repeated Event events = 5; + // LastRun indicates the date/time this was last run. + google.protobuf.Timestamp last_run = 6; +} diff --git a/_proto/hapi/release/release.proto b/_proto/hapi/release/release.proto index 68559b5ad..a13c99274 100644 --- a/_proto/hapi/release/release.proto +++ b/_proto/hapi/release/release.proto @@ -16,6 +16,7 @@ syntax = "proto3"; package hapi.release; +import "hapi/release/hook.proto"; import "hapi/release/info.proto"; import "hapi/chart/config.proto"; import "hapi/chart/chart.proto"; @@ -40,4 +41,11 @@ message Release { // Manifest is the string representation of the rendered template. string manifest = 5; + + // Hooks are all of the hooks declared for this release. + repeated hapi.release.Hook hooks = 6; + + // Version is an int32 which represents the version of the release. + int32 version = 7; + } diff --git a/circle.yml b/circle.yml index 83b4dcadd..1c40610fb 100644 --- a/circle.yml +++ b/circle.yml @@ -1,43 +1,45 @@ machine: + pre: + - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 + environment: - GLIDE_VERSION: "0.10.1" - GO15VENDOREXPERIMENT: 1 - GOPATH: /usr/local/go_workspace - HOME: /home/ubuntu - IMPORT_PATH: "k8s.io/helm" - PATH: $HOME/go/bin:$PATH - GOROOT: $HOME/go + GOVERSION: "1.6.2" + GOPATH: "${HOME}/.go_workspace" + WORKDIR: "${GOPATH}/src/k8s.io/helm" services: - docker dependencies: + pre: + - sudo rm -rf /usr/local/go + - rm -rf "$GOPATH" + override: - - mkdir -p $HOME/go - - wget "https://storage.googleapis.com/golang/go1.6.linux-amd64.tar.gz" - - tar -C $HOME -xzf go1.6.linux-amd64.tar.gz - - go version + # install go + - wget "https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz" + - sudo tar -C /usr/local -xzf "go${GOVERSION}.linux-amd64.tar.gz" + + # move repository to the canonical import path + - mkdir -p "$(dirname ${WORKDIR})" + - cp -R "${HOME}/helm" "${WORKDIR}" + + # install dependencies + - cd "${WORKDIR}" && make bootstrap + + post: - go env - - sudo chown -R $(whoami):staff /usr/local - - cd $GOPATH - - mkdir -p $GOPATH/src/$IMPORT_PATH - - cd $HOME/helm - - rsync -az --delete ./ "$GOPATH/src/$IMPORT_PATH/" - - wget "https://github.com/Masterminds/glide/releases/download/$GLIDE_VERSION/glide-$GLIDE_VERSION-linux-amd64.tar.gz" - - mkdir -p $HOME/bin - - tar -vxz -C $HOME/bin --strip=1 -f glide-$GLIDE_VERSION-linux-amd64.tar.gz - - export PATH="$HOME/bin:$PATH" GLIDE_HOME="$HOME/.glide" test: override: - - cd $GOPATH/src/$IMPORT_PATH && make bootstrap test + - cd "${WORKDIR}" && ./scripts/ci.sh: + parallel: true deployment: - master-branch: + gcr: branch: master commands: - echo $GCLOUD_SERVICE_KEY | base64 --decode > ${HOME}/gcloud-service-key.json - - sudo docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io - - cd $GOPATH/src/$IMPORT_PATH + - docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io - make docker-build - - sudo docker push gcr.io/kubernetes-helm/tiller:canary + - docker push gcr.io/kubernetes-helm/tiller:canary diff --git a/cmd/helm/create.go b/cmd/helm/create.go index 74fa3f6ed..9f50be5f7 100644 --- a/cmd/helm/create.go +++ b/cmd/helm/create.go @@ -18,9 +18,12 @@ package main import ( "errors" + "fmt" + "io" "path/filepath" "github.com/spf13/cobra" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/chart" ) @@ -50,31 +53,40 @@ destination exists and there are files in that directory, conflicting files will be overwritten, but other files will be left alone. ` -func init() { - RootCommand.AddCommand(createCmd) +type createCmd struct { + name string + out io.Writer } -var createCmd = &cobra.Command{ - Use: "create NAME", - Short: "create a new chart with the given name", - Long: createDesc, - RunE: runCreate, +func newCreateCmd(out io.Writer) *cobra.Command { + cc := &createCmd{ + out: out, + } + cmd := &cobra.Command{ + Use: "create NAME", + Short: "create a new chart with the given name", + Long: createDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("the name of the new chart is required") + } + cc.name = args[0] + return cc.run() + }, + } + return cmd } -func runCreate(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errors.New("the name of the new chart is required") - } - cname := args[0] - cmd.Printf("Creating %s\n", cname) +func (c *createCmd) run() error { + fmt.Fprintf(c.out, "Creating %s\n", c.name) - chartname := filepath.Base(cname) + chartname := filepath.Base(c.name) cfile := &chart.Metadata{ Name: chartname, Description: "A Helm chart for Kubernetes", Version: "0.1.0", } - _, err := chartutil.Create(cfile, filepath.Dir(cname)) + _, err := chartutil.Create(cfile, filepath.Dir(c.name)) return err } diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 84217f947..954b76407 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -18,8 +18,8 @@ package main import ( "errors" - "fmt" - "os" + "io" + "text/template" "time" "github.com/spf13/cobra" @@ -42,70 +42,61 @@ By default, this prints a human readable collection of information about the chart, the supplied values, and the generated manifest file. ` -var getValuesHelp = ` -This command downloads a values file for a given release. - -To save the output to a file, use the -f flag. -` - -var getManifestHelp = ` -This command fetches the generated manifest for a given release. - -A manifest is a YAML-encoded representation of the Kubernetes resources that -were generated from this release's chart(s). If a chart is dependent on other -charts, those resources will also be included in the manifest. -` - -// getOut is the filename to direct output. -// -// If it is blank, output is sent to os.Stdout. -var getOut = "" - -var allValues = false - var errReleaseRequired = errors.New("release name is required") -var getCommand = &cobra.Command{ - Use: "get [flags] RELEASE_NAME", - Short: "download a named release", - Long: getHelp, - RunE: getCmd, - PersistentPreRunE: setupConnection, +type getCmd struct { + release string + out io.Writer + client helm.Interface } -var getValuesCommand = &cobra.Command{ - Use: "values [flags] RELEASE_NAME", - Short: "download the values file for a named release", - Long: getValuesHelp, - RunE: getValues, -} - -var getManifestCommand = &cobra.Command{ - Use: "manifest [flags] RELEASE_NAME", - Short: "download the manifest for a named release", - Long: getManifestHelp, - RunE: getManifest, +func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { + get := &getCmd{ + out: out, + client: client, + } + cmd := &cobra.Command{ + Use: "get [flags] RELEASE_NAME", + Short: "download a named release", + Long: getHelp, + PersistentPreRunE: setupConnection, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + get.release = args[0] + if get.client == nil { + get.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) + } + return get.run() + }, + } + cmd.AddCommand(newGetValuesCmd(nil, out)) + cmd.AddCommand(newGetManifestCmd(nil, out)) + cmd.AddCommand(newGetHooksCmd(nil, out)) + return cmd } -func init() { - // 'get' command flags. - getCommand.PersistentFlags().StringVarP(&getOut, "file", "f", "", "output file") - - // 'get values' flags. - getValuesCommand.PersistentFlags().BoolVarP(&allValues, "all", "a", false, "dump all (computed) values") - - getCommand.AddCommand(getValuesCommand) - getCommand.AddCommand(getManifestCommand) - RootCommand.AddCommand(getCommand) -} +var getTemplate = `VERSION: {{.Release.Version}} +RELEASED: {{.ReleaseDate}} +CHART: {{.Release.Chart.Metadata.Name}}-{{.Release.Chart.Metadata.Version}} +USER-SUPPLIED VALUES: +{{.Release.Config.Raw}} +COMPUTED VALUES: +{{.ComputedValues}} +HOOKS: +{{- range .Release.Hooks }} +--- +# {{.Name}} +{{.Manifest}} +{{- end }} +MANIFEST: +{{.Release.Manifest}} +` // getCmd is the command that implements 'helm get' -func getCmd(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired - } - - res, err := helm.GetReleaseContent(args[0]) +func (g *getCmd) run() error { + res, err := g.client.ReleaseContent(g.release) if err != nil { return prettyError(err) } @@ -119,67 +110,25 @@ func getCmd(cmd *cobra.Command, args []string) error { return err } - fmt.Printf("CHART: %s-%s\n", res.Release.Chart.Metadata.Name, res.Release.Chart.Metadata.Version) - fmt.Printf("RELEASED: %s\n", timeconv.Format(res.Release.Info.LastDeployed, time.ANSIC)) - fmt.Println("USER-SUPPLIED VALUES:") - fmt.Println(res.Release.Config.Raw) - fmt.Println("COMPUTED VALUES:") - fmt.Println(cfgStr) - fmt.Println("MANIFEST:") - fmt.Println(res.Release.Manifest) - return nil -} - -// getValues implements 'helm get values' -func getValues(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired - } - - res, err := helm.GetReleaseContent(args[0]) - if err != nil { - return prettyError(err) + data := map[string]interface{}{ + "Release": res.Release, + "ComputedValues": cfgStr, + "ReleaseDate": timeconv.Format(res.Release.Info.LastDeployed, time.ANSIC), } - - // If the user wants all values, compute the values and return. - if allValues { - cfg, err := chartutil.CoalesceValues(res.Release.Chart, res.Release.Config, nil) - if err != nil { - return err - } - cfgStr, err := cfg.YAML() - if err != nil { - return err - } - return getToFile(cfgStr) - } - - return getToFile(res.Release.Config.Raw) + return tpl(getTemplate, data, g.out) } -// getManifest implements 'helm get manifest' -func getManifest(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired - } - - res, err := helm.GetReleaseContent(args[0]) +func tpl(t string, vals map[string]interface{}, out io.Writer) error { + tt, err := template.New("_").Parse(t) if err != nil { - return prettyError(err) + return err } - return getToFile(res.Release.Manifest) + return tt.Execute(out, vals) } -func getToFile(v interface{}) error { - out := os.Stdout - if len(getOut) > 0 { - t, err := os.Create(getOut) - if err != nil { - return fmt.Errorf("failed to create %s: %s", getOut, err) - } - defer t.Close() - out = t +func ensureHelmClient(h helm.Interface) helm.Interface { + if h != nil { + return h } - fmt.Fprintln(out, v) - return nil + return helm.NewClient(helm.Host(helm.Config.ServAddr)) } diff --git a/cmd/helm/get_hooks.go b/cmd/helm/get_hooks.go new file mode 100644 index 000000000..f880bf789 --- /dev/null +++ b/cmd/helm/get_hooks.go @@ -0,0 +1,72 @@ +/* +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 ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +const getHooksHelp = ` +This command downloads hooks for a given release. + +Hooks are formatted in YAML and separated by the YAML '---\n' separator. +` + +type getHooksCmd struct { + release string + out io.Writer + client helm.Interface +} + +func newGetHooksCmd(client helm.Interface, out io.Writer) *cobra.Command { + ghc := &getHooksCmd{ + out: out, + client: client, + } + cmd := &cobra.Command{ + Use: "hooks [flags] RELEASE_NAME", + Short: "download all hooks for a named release", + Long: getHooksHelp, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + ghc.release = args[0] + ghc.client = ensureHelmClient(ghc.client) + return ghc.run() + }, + } + return cmd +} + +func (g *getHooksCmd) run() error { + res, err := g.client.ReleaseContent(g.release) + if err != nil { + fmt.Fprintln(g.out, g.release) + return prettyError(err) + } + + for _, hook := range res.Release.Hooks { + fmt.Fprintf(g.out, "---\n# %s\n%s", hook.Name, hook.Manifest) + } + return nil +} diff --git a/cmd/helm/get_hooks_test.go b/cmd/helm/get_hooks_test.go new file mode 100644 index 000000000..212da53bc --- /dev/null +++ b/cmd/helm/get_hooks_test.go @@ -0,0 +1,43 @@ +/* +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 ( + "io" + "testing" + + "github.com/spf13/cobra" +) + +func TestGetHooks(t *testing.T) { + tests := []releaseCase{ + { + name: "get hooks with release", + args: []string{"aeneas"}, + expected: mockHookTemplate, + resp: releaseMock("aeneas"), + }, + { + name: "get hooks without args", + args: []string{}, + err: true, + }, + } + runReleaseCases(t, tests, func(c *fakeReleaseClient, out io.Writer) *cobra.Command { + return newGetHooksCmd(c, out) + }) +} diff --git a/cmd/helm/get_manifest.go b/cmd/helm/get_manifest.go new file mode 100644 index 000000000..f3b9679bd --- /dev/null +++ b/cmd/helm/get_manifest.go @@ -0,0 +1,73 @@ +/* +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 ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +var getManifestHelp = ` +This command fetches the generated manifest for a given release. + +A manifest is a YAML-encoded representation of the Kubernetes resources that +were generated from this release's chart(s). If a chart is dependent on other +charts, those resources will also be included in the manifest. +` + +type getManifestCmd struct { + release string + out io.Writer + client helm.Interface +} + +func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { + get := &getManifestCmd{ + out: out, + client: client, + } + cmd := &cobra.Command{ + Use: "manifest [flags] RELEASE_NAME", + Short: "download the manifest for a named release", + Long: getManifestHelp, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + get.release = args[0] + if get.client == nil { + get.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) + } + return get.run() + }, + } + return cmd +} + +// getManifest implements 'helm get manifest' +func (g *getManifestCmd) run() error { + res, err := g.client.ReleaseContent(g.release) + if err != nil { + return prettyError(err) + } + fmt.Fprintln(g.out, res.Release.Manifest) + return nil +} diff --git a/cmd/helm/get_manifest_test.go b/cmd/helm/get_manifest_test.go new file mode 100644 index 000000000..f09ecd3c2 --- /dev/null +++ b/cmd/helm/get_manifest_test.go @@ -0,0 +1,43 @@ +/* +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 ( + "io" + "testing" + + "github.com/spf13/cobra" +) + +func TestGetManifest(t *testing.T) { + tests := []releaseCase{ + { + name: "get manifest with release", + args: []string{"juno"}, + expected: mockManifest, + resp: releaseMock("juno"), + }, + { + name: "get manifest without args", + args: []string{}, + err: true, + }, + } + runReleaseCases(t, tests, func(c *fakeReleaseClient, out io.Writer) *cobra.Command { + return newGetManifestCmd(c, out) + }) +} diff --git a/cmd/helm/get_test.go b/cmd/helm/get_test.go new file mode 100644 index 000000000..962b23c04 --- /dev/null +++ b/cmd/helm/get_test.go @@ -0,0 +1,44 @@ +/* +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 ( + "io" + "testing" + + "github.com/spf13/cobra" +) + +func TestGetCmd(t *testing.T) { + tests := []releaseCase{ + { + name: "get with a release", + resp: releaseMock("thomas-guide"), + args: []string{"thomas-guide"}, + expected: "VERSION: 1\nRELEASED: (.*)\nCHART: foo-0.1.0-beta.1\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nHOOKS:\n---\n# pre-install-hook\n" + mockHookTemplate + "\nMANIFEST:", + }, + { + name: "get requires release name arg", + err: true, + }, + } + + cmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command { + return newGetCmd(c, out) + } + runReleaseCases(t, tests, cmd) +} diff --git a/cmd/helm/get_values.go b/cmd/helm/get_values.go new file mode 100644 index 000000000..bd06e7fb2 --- /dev/null +++ b/cmd/helm/get_values.go @@ -0,0 +1,85 @@ +/* +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 ( + "fmt" + "io" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/helm" +) + +var getValuesHelp = ` +This command downloads a values file for a given release. +` + +type getValuesCmd struct { + release string + allValues bool + out io.Writer + client helm.Interface +} + +func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { + get := &getValuesCmd{ + out: out, + client: client, + } + cmd := &cobra.Command{ + Use: "values [flags] RELEASE_NAME", + Short: "download the values file for a named release", + Long: getValuesHelp, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + get.release = args[0] + get.client = ensureHelmClient(get.client) + return get.run() + }, + } + cmd.Flags().BoolVarP(&get.allValues, "all", "a", false, "dump all (computed) values") + return cmd +} + +// getValues implements 'helm get values' +func (g *getValuesCmd) run() error { + res, err := g.client.ReleaseContent(g.release) + if err != nil { + return prettyError(err) + } + + // If the user wants all values, compute the values and return. + if g.allValues { + cfg, err := chartutil.CoalesceValues(res.Release.Chart, res.Release.Config, nil) + if err != nil { + return err + } + cfgStr, err := cfg.YAML() + if err != nil { + return err + } + fmt.Fprintln(g.out, cfgStr) + return nil + } + + fmt.Fprintln(g.out, res.Release.Config.Raw) + return nil +} diff --git a/cmd/helm/get_values_test.go b/cmd/helm/get_values_test.go new file mode 100644 index 000000000..71aef1eb9 --- /dev/null +++ b/cmd/helm/get_values_test.go @@ -0,0 +1,43 @@ +/* +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 ( + "io" + "testing" + + "github.com/spf13/cobra" +) + +func TestGetValuesCmd(t *testing.T) { + tests := []releaseCase{ + { + name: "get values with a release", + resp: releaseMock("thomas-guide"), + args: []string{"thomas-guide"}, + expected: "name: \"value\"", + }, + { + name: "get values requires release name arg", + err: true, + }, + } + cmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command { + return newGetValuesCmd(c, out) + } + runReleaseCases(t, tests, cmd) +} diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index e54ceadb5..7a9ab74fc 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package main // import "k8s.io/helm/cmd/helm" import ( "errors" "fmt" + "io" "os" "strings" @@ -63,29 +64,42 @@ Environment: $HELM_HOST Set an alternative Tiller host. The format is host:port (default ":44134"). ` -// RootCommand is the top-level command for Helm. -var RootCommand = &cobra.Command{ - Use: "helm", - Short: "The Helm package manager for Kubernetes.", - Long: globalUsage, - PersistentPostRun: teardown, -} - -func init() { +func newRootCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "helm", + Short: "The Helm package manager for Kubernetes.", + Long: globalUsage, + SilenceUsage: true, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + teardown() + }, + } home := os.Getenv(homeEnvVar) if home == "" { home = "$HOME/.helm" } thost := os.Getenv(hostEnvVar) - p := RootCommand.PersistentFlags() + p := cmd.PersistentFlags() p.StringVar(&helmHome, "home", home, "location of your Helm config. Overrides $HELM_HOME.") p.StringVar(&tillerHost, "host", thost, "address of tiller. Overrides $HELM_HOST.") p.StringVarP(&tillerNamespace, "namespace", "", "", "kubernetes namespace") p.BoolVarP(&flagDebug, "debug", "", false, "enable verbose output") + + cmd.AddCommand( + newCreateCmd(out), + newGetCmd(nil, out), + newListCmd(nil, out), + newStatusCmd(nil, out), + ) + return cmd } +// RootCommand is the top-level command for Helm. +var RootCommand = newRootCmd(os.Stdout) + func main() { - if err := RootCommand.Execute(); err != nil { + cmd := RootCommand + if err := cmd.Execute(); err != nil { os.Exit(1) } } @@ -111,7 +125,7 @@ func setupConnection(c *cobra.Command, args []string) error { return nil } -func teardown(c *cobra.Command, args []string) { +func teardown() { if tunnel != nil { tunnel.Close() } diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go new file mode 100644 index 000000000..05fae24ad --- /dev/null +++ b/cmd/helm/helm_test.go @@ -0,0 +1,150 @@ +/* +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" + "regexp" + "testing" + + "github.com/golang/protobuf/ptypes/timestamp" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/proto/hapi/release" + rls "k8s.io/helm/pkg/proto/hapi/services" +) + +var mockHookTemplate = `apiVersion: v1 +kind: Job +metadata: + annotations: + "helm.sh/hooks": pre-install +` + +var mockManifest = `apiVersion: v1 +kind: Secret +metadata: + name: fixture +` + +func releaseMock(name string) *release.Release { + date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0} + return &release.Release{ + Name: name, + Info: &release.Info{ + FirstDeployed: &date, + LastDeployed: &date, + Status: &release.Status{Code: release.Status_DEPLOYED}, + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "foo", + Version: "0.1.0-beta.1", + }, + Templates: []*chart.Template{ + {Name: "foo.tpl", Data: []byte(mockManifest)}, + }, + }, + Config: &chart.Config{Raw: `name: "value"`}, + Version: 1, + Hooks: []*release.Hook{ + { + Name: "pre-install-hook", + Kind: "Job", + Path: "pre-install-hook.yaml", + Manifest: mockHookTemplate, + LastRun: &date, + Events: []release.Hook_Event{release.Hook_PRE_INSTALL}, + }, + }, + Manifest: mockManifest, + } +} + +type fakeReleaseClient struct { + rels []*release.Release + err error +} + +func (c *fakeReleaseClient) ListReleases(opts ...helm.ReleaseListOption) (*rls.ListReleasesResponse, error) { + resp := &rls.ListReleasesResponse{ + Count: int64(len(c.rels)), + Releases: c.rels, + } + return resp, c.err +} + +func (c *fakeReleaseClient) InstallRelease(chStr string, opts ...helm.InstallOption) (*rls.InstallReleaseResponse, error) { + return nil, nil +} + +func (c *fakeReleaseClient) DeleteRelease(rlsName string, opts ...helm.DeleteOption) (*rls.UninstallReleaseResponse, error) { + return nil, nil +} + +func (c *fakeReleaseClient) ReleaseStatus(rlsName string, opts ...helm.StatusOption) (*rls.GetReleaseStatusResponse, error) { + return nil, nil +} + +func (c *fakeReleaseClient) UpdateRelease(rlsName string, opts ...helm.UpdateOption) (*rls.UpdateReleaseResponse, error) { + return nil, nil +} + +func (c *fakeReleaseClient) ReleaseContent(rlsName string, opts ...helm.ContentOption) (resp *rls.GetReleaseContentResponse, err error) { + if len(c.rels) > 0 { + resp = &rls.GetReleaseContentResponse{ + Release: c.rels[0], + } + } + return resp, c.err +} + +// releaseCmd is a command that works with a fakeReleaseClient +type releaseCmd func(c *fakeReleaseClient, out io.Writer) *cobra.Command + +// runReleaseCases runs a set of release cases through the given releaseCmd. +func runReleaseCases(t *testing.T, tests []releaseCase, rcmd releaseCmd) { + var buf bytes.Buffer + for _, tt := range tests { + c := &fakeReleaseClient{ + rels: []*release.Release{tt.resp}, + } + cmd := rcmd(c, &buf) + err := cmd.RunE(cmd, tt.args) + if (err != nil) != tt.err { + t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) + } + re := regexp.MustCompile(tt.expected) + if !re.Match(buf.Bytes()) { + t.Errorf("%q. expected\n%q\ngot\n%q", tt.name, tt.expected, buf.String()) + } + buf.Reset() + } +} + +// releaseCase describes a test case that works with releases. +type releaseCase struct { + name string + args []string + // expected is the string to be matched. This supports regular expressions. + expected string + err bool + resp *release.Release +} diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 9168325c0..056b89b9f 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -135,6 +135,7 @@ func locateChartPath(name string) (string, error) { } // Try fetching the chart from a remote repo into a tmpdir + origname := name if filepath.Ext(name) != ".tgz" { name += ".tgz" } @@ -143,9 +144,9 @@ func locateChartPath(name string) (string, error) { if err != nil { return lname, err } - fmt.Printf("Fetched %s to %s\n", name, lname) + fmt.Printf("Fetched %s to %s\n", origname, lname) return lname, nil } - return name, fmt.Errorf("file %q not found", name) + return name, fmt.Errorf("file %q not found", origname) } diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index 7a52ea597..d3e496cda 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -19,12 +19,16 @@ package main import ( "errors" "fmt" + "io/ioutil" "os" "path/filepath" + "strings" "github.com/spf13/cobra" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/lint" + "k8s.io/helm/pkg/lint/support" ) var longLintHelp = ` @@ -47,27 +51,80 @@ func init() { RootCommand.AddCommand(lintCommand) } -var errLintNoChart = errors.New("no chart found for linting (missing Chart.yaml)") +var errLintNoChart = errors.New("No chart found for linting (missing Chart.yaml)") func lintCmd(cmd *cobra.Command, args []string) error { - path := "." + paths := []string{"."} if len(args) > 0 { - path = args[0] + paths = args } - // Guard: Error out of this is not a chart. - if _, err := os.Stat(filepath.Join(path, "Chart.yaml")); err != nil { - return errLintNoChart + var total int + var failures int + for _, path := range paths { + if linter, err := lintChart(path); err != nil { + fmt.Println("==> Skipping", path) + fmt.Println(err) + } else { + fmt.Println("==> Linting", path) + + if len(linter.Messages) == 0 { + fmt.Println("Lint OK") + } + + for _, msg := range linter.Messages { + fmt.Println(msg) + } + + total = total + 1 + if linter.HighestSeverity >= support.ErrorSev { + failures = failures + 1 + } + } + fmt.Println("") + } + + msg := fmt.Sprintf("%d chart(s) linted", total) + if failures > 0 { + return fmt.Errorf("%s, %d chart(s) failed", msg, failures) } - issues := lint.All(path) + fmt.Printf("%s, no failures\n", msg) - if len(issues) == 0 { - fmt.Println("Lint OK") + return nil +} + +func lintChart(path string) (support.Linter, error) { + var chartPath string + linter := support.Linter{} + + if strings.HasSuffix(path, ".tgz") { + tempDir, err := ioutil.TempDir("", "helm-lint") + if err != nil { + return linter, err + } + defer os.RemoveAll(tempDir) + + file, err := os.Open(path) + if err != nil { + return linter, err + } + defer file.Close() + + if err = chartutil.Expand(tempDir, file); err != nil { + return linter, err + } + + base := strings.Split(filepath.Base(path), "-")[0] + chartPath = filepath.Join(tempDir, base) + } else { + chartPath = path } - for _, i := range issues { - fmt.Printf("%s\n", i) + // Guard: Error out of this is not a chart. + if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { + return linter, errLintNoChart } - return nil + + return lint.All(chartPath), nil } diff --git a/cmd/helm/lint_test.go b/cmd/helm/lint_test.go new file mode 100644 index 000000000..9f71c0231 --- /dev/null +++ b/cmd/helm/lint_test.go @@ -0,0 +1,37 @@ +/* +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 ( + "testing" +) + +var ( + archivedChartPath = "testdata/testcharts/compressedchart-0.1.0.tgz" + chartDirPath = "testdata/testcharts/decompressedchart/" +) + +func TestLintChart(t *testing.T) { + if _, err := lintChart(chartDirPath); err != nil { + t.Errorf("%s", err) + } + + if _, err := lintChart(archivedChartPath); err != nil { + t.Errorf("%s", err) + } + +} diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 01e22f4e7..7314a4a22 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -18,6 +18,7 @@ package main import ( "fmt" + "io" "strings" "github.com/gosuri/uitable" @@ -52,51 +53,66 @@ server's default, which may be much higher than 256. Pairing the '--max' flag with the '--offset' flag allows you to page through results. ` -var listCommand = &cobra.Command{ - Use: "list [flags] [FILTER]", - Short: "list releases", - Long: listHelp, - RunE: listCmd, - Aliases: []string{"ls"}, - PersistentPreRunE: setupConnection, +type listCmd struct { + filter string + long bool + limit int + offset string + byDate bool + sortDesc bool + out io.Writer + client helm.Interface } -var ( - listLong bool - listMax int - listOffset string - listByDate bool - listSortDesc bool -) - -func init() { - f := listCommand.Flags() - f.BoolVarP(&listLong, "long", "l", false, "output long listing format") - f.BoolVarP(&listByDate, "date", "d", false, "sort by release date") - f.BoolVarP(&listSortDesc, "reverse", "r", false, "reverse the sort order") - f.IntVarP(&listMax, "max", "m", 256, "maximum number of releases to fetch") - f.StringVarP(&listOffset, "offset", "o", "", "the next release name in the list, used to offset from start value") - - RootCommand.AddCommand(listCommand) -} - -func listCmd(cmd *cobra.Command, args []string) error { - var filter string - if len(args) > 0 { - filter = strings.Join(args, " ") +func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { + list := &listCmd{ + out: out, + client: client, + } + cmd := &cobra.Command{ + Use: "list [flags] [FILTER]", + Short: "list releases", + Long: listHelp, + Aliases: []string{"ls"}, + PersistentPreRunE: setupConnection, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + list.filter = strings.Join(args, " ") + } + if list.client == nil { + list.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) + } + return list.run() + }, } + f := cmd.Flags() + f.BoolVarP(&list.long, "long", "l", false, "output long listing format") + f.BoolVarP(&list.byDate, "date", "d", false, "sort by release date") + f.BoolVarP(&list.sortDesc, "reverse", "r", false, "reverse the sort order") + f.IntVarP(&list.limit, "max", "m", 256, "maximum number of releases to fetch") + f.StringVarP(&list.offset, "offset", "o", "", "the next release name in the list, used to offset from start value") + return cmd +} +func (l *listCmd) run() error { sortBy := services.ListSort_NAME - if listByDate { + if l.byDate { sortBy = services.ListSort_LAST_RELEASED } sortOrder := services.ListSort_ASC - if listSortDesc { + if l.sortDesc { sortOrder = services.ListSort_DESC } - res, err := helm.ListReleases(listMax, listOffset, sortBy, sortOrder, filter) + res, err := l.client.ListReleases( + helm.ReleaseListLimit(l.limit), + helm.ReleaseListOffset(l.offset), + helm.ReleaseListFilter(l.filter), + helm.ReleaseListSort(int32(sortBy)), + helm.ReleaseListOrder(int32(sortOrder)), + ) + if err != nil { return prettyError(err) } @@ -106,31 +122,32 @@ func listCmd(cmd *cobra.Command, args []string) error { } if res.Next != "" { - fmt.Printf("\tnext: %s", res.Next) + fmt.Fprintf(l.out, "\tnext: %s", res.Next) } rels := res.Releases - if listLong { - return formatList(rels) + + if l.long { + fmt.Fprintln(l.out, formatList(rels)) + return nil } for _, r := range rels { - fmt.Println(r.Name) + fmt.Fprintln(l.out, r.Name) } return nil } -func formatList(rels []*release.Release) error { +func formatList(rels []*release.Release) string { table := uitable.New() table.MaxColWidth = 30 - table.AddRow("NAME", "UPDATED", "STATUS", "CHART") + table.AddRow("NAME", "VERSION", "UPDATED", "STATUS", "CHART") for _, r := range rels { c := fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version) t := timeconv.String(r.Info.LastDeployed) s := r.Info.Status.Code.String() - table.AddRow(r.Name, t, s, c) + v := r.Version + table.AddRow(r.Name, v, t, s, c) } - fmt.Println(table) - - return nil + return table.String() } diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go new file mode 100644 index 000000000..6d44173cb --- /dev/null +++ b/cmd/helm/list_test.go @@ -0,0 +1,72 @@ +/* +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" + "regexp" + "testing" + + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestListCmd(t *testing.T) { + tests := []struct { + name string + args []string + flags map[string]string + resp []*release.Release + expected string + err bool + }{ + { + name: "with a release", + resp: []*release.Release{ + releaseMock("thomas-guide"), + }, + expected: "thomas-guide", + }, + { + name: "list --long", + flags: map[string]string{"long": "1"}, + resp: []*release.Release{ + releaseMock("atlas"), + }, + expected: "NAME \tVERSION\tUPDATED \tSTATUS \tCHART \natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\n", + }, + } + + var buf bytes.Buffer + for _, tt := range tests { + c := &fakeReleaseClient{ + rels: tt.resp, + } + cmd := newListCmd(c, &buf) + for flag, value := range tt.flags { + cmd.Flags().Set(flag, value) + } + err := cmd.RunE(cmd, tt.args) + if (err != nil) != tt.err { + t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) + } + re := regexp.MustCompile(tt.expected) + if !re.Match(buf.Bytes()) { + t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, buf.String()) + } + buf.Reset() + } +} diff --git a/cmd/helm/package.go b/cmd/helm/package.go index 770bb5d3f..beff30e31 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -71,7 +71,7 @@ func runPackage(cmd *cobra.Command, args []string) error { } if filepath.Base(path) != ch.Metadata.Name { - return fmt.Errorf("directory name (%s) and Chart.yaml name (%s) must match.", filepath.Base(path), ch.Metadata.Name) + return fmt.Errorf("directory name (%s) and Chart.yaml name (%s) must match", filepath.Base(path), ch.Metadata.Name) } // Save to the current working directory. diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go index d53dfa3fd..0869551aa 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -22,8 +22,8 @@ import ( "k8s.io/helm/pkg/repo" ) -const testDir = "testdata/" -const testFile = "testdata/local-index.yaml" +const testDir = "testdata/testcache" +const testFile = "testdata/testcache/local-index.yaml" type searchTestCase struct { in string diff --git a/cmd/helm/status.go b/cmd/helm/status.go index e69774043..b60748dea 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -18,6 +18,7 @@ package main import ( "fmt" + "io" "github.com/spf13/cobra" @@ -29,33 +30,46 @@ var statusHelp = ` This command shows the status of a named release. ` -var statusCommand = &cobra.Command{ - Use: "status [flags] RELEASE_NAME", - Short: "displays the status of the named release", - Long: statusHelp, - RunE: status, - PersistentPreRunE: setupConnection, +type statusCmd struct { + release string + out io.Writer + client helm.Interface } -func init() { - RootCommand.AddCommand(statusCommand) -} - -func status(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired +func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { + status := &statusCmd{ + out: out, + client: client, + } + cmd := &cobra.Command{ + Use: "status [flags] RELEASE_NAME", + Short: "displays the status of the named release", + Long: statusHelp, + PersistentPreRunE: setupConnection, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + status.release = args[0] + if status.client == nil { + status.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) + } + return status.run() + }, } + return cmd +} - res, err := helm.GetReleaseStatus(args[0]) +func (s *statusCmd) run() error { + res, err := s.client.ReleaseStatus(s.release) if err != nil { return prettyError(err) } - fmt.Printf("Last Deployed: %s\n", timeconv.String(res.Info.LastDeployed)) - fmt.Printf("Status: %s\n", res.Info.Status.Code) + fmt.Fprintf(s.out, "Last Deployed: %s\n", timeconv.String(res.Info.LastDeployed)) + fmt.Fprintf(s.out, "Status: %s\n", res.Info.Status.Code) if res.Info.Status.Details != nil { - fmt.Printf("Details: %s\n", res.Info.Status.Details) + fmt.Fprintf(s.out, "Details: %s\n", res.Info.Status.Details) } - return nil } diff --git a/cmd/helm/testdata/foobar-index.yaml b/cmd/helm/testdata/testcache/foobar-index.yaml similarity index 100% rename from cmd/helm/testdata/foobar-index.yaml rename to cmd/helm/testdata/testcache/foobar-index.yaml diff --git a/cmd/helm/testdata/local-index.yaml b/cmd/helm/testdata/testcache/local-index.yaml similarity index 100% rename from cmd/helm/testdata/local-index.yaml rename to cmd/helm/testdata/testcache/local-index.yaml diff --git a/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz b/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz new file mode 100644 index 000000000..575b27128 Binary files /dev/null and b/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/decompressedchart/.helmignore b/cmd/helm/testdata/testcharts/decompressedchart/.helmignore new file mode 100644 index 000000000..435b756d8 --- /dev/null +++ b/cmd/helm/testdata/testcharts/decompressedchart/.helmignore @@ -0,0 +1,5 @@ +# 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 diff --git a/cmd/helm/testdata/testcharts/decompressedchart/Chart.yaml b/cmd/helm/testdata/testcharts/decompressedchart/Chart.yaml new file mode 100755 index 000000000..3e65afdfa --- /dev/null +++ b/cmd/helm/testdata/testcharts/decompressedchart/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: decompressedchart +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/decompressedchart/values.yaml b/cmd/helm/testdata/testcharts/decompressedchart/values.yaml new file mode 100644 index 000000000..a940d1fd9 --- /dev/null +++ b/cmd/helm/testdata/testcharts/decompressedchart/values.yaml @@ -0,0 +1,4 @@ +# Default values for decompressedchart. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. + name: my-decompressed-chart diff --git a/cmd/tiller/environment/environment.go b/cmd/tiller/environment/environment.go index 8a531d94c..91786ddec 100644 --- a/cmd/tiller/environment/environment.go +++ b/cmd/tiller/environment/environment.go @@ -157,6 +157,13 @@ type KubeClient interface { // reader must contain a YAML stream (one or more YAML documents separated // by "\n---\n"). Delete(namespace string, reader io.Reader) error + + // Watch the resource in reader until it is "ready". + // + // For Jobs, "ready" means the job ran to completion (excited without error). + // For all other kinds, it means the kind was created or modified without + // error. + WatchUntilReady(namespace string, reader io.Reader) error } // PrintingKubeClient implements KubeClient, but simply prints the reader to @@ -179,6 +186,12 @@ func (p *PrintingKubeClient) Delete(ns string, r io.Reader) error { return err } +// WatchUntilReady implements KubeClient WatchUntilReady. +func (p *PrintingKubeClient) WatchUntilReady(ns string, r io.Reader) error { + _, err := io.Copy(p.Out, r) + 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 98c10a37e..cfcbc8ca4 100644 --- a/cmd/tiller/environment/environment_test.go +++ b/cmd/tiller/environment/environment_test.go @@ -83,6 +83,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) WatchUntilReady(ns string, r io.Reader) error { + return nil +} var _ Engine = &mockEngine{} var _ ReleaseStorage = &mockReleaseStorage{} diff --git a/cmd/tiller/hooks.go b/cmd/tiller/hooks.go new file mode 100644 index 000000000..762272986 --- /dev/null +++ b/cmd/tiller/hooks.go @@ -0,0 +1,107 @@ +package main + +import ( + "log" + "strings" + + "github.com/ghodss/yaml" + "k8s.io/helm/pkg/proto/hapi/release" +) + +// hookAnno is the label name for a hook +const hookAnno = "helm.sh/hook" + +const ( + preInstall = "pre-install" + postInstall = "post-install" + preDelete = "pre-delete" + postDelete = "post-delete" + preUpgrade = "pre-upgrade" + postUpgrade = "post-upgrade" +) + +var events = map[string]release.Hook_Event{ + preInstall: release.Hook_PRE_INSTALL, + postInstall: release.Hook_POST_INSTALL, + preDelete: release.Hook_PRE_DELETE, + postDelete: release.Hook_POST_DELETE, + preUpgrade: release.Hook_PRE_UPGRADE, + postUpgrade: release.Hook_POST_UPGRADE, +} + +type simpleHead struct { + Kind string `json:"kind,omitempty"` + Metadata *struct { + Name string `json:"name"` + Annotations map[string]string `json:"annotations"` + } `json:"metadata,omitempty"` +} + +// sortHooks takes a map of filename/YAML contents and sorts them into hook types. +// +// The resulting hooks struct will be populated with all of the generated hooks. +// Any file that does not declare one of the hook types will be placed in the +// 'generic' bucket. +// +// To determine hook type, this looks for a YAML structure like this: +// +// kind: SomeKind +// metadata: +// annotations: +// helm.sh/hook: pre-install +// +// Where HOOK_NAME is one of the known hooks. +// +// If a file declares more than one hook, it will be copied into all of the applicable +// hook buckets. (Note: label keys are not unique within the labels section). +// +// Files that do not parse into the expected format are simply placed into a map and +// returned. +func sortHooks(files map[string]string) (hs []*release.Hook, generic map[string]string) { + hs = []*release.Hook{} + generic = map[string]string{} + + for n, c := range files { + var sh simpleHead + err := yaml.Unmarshal([]byte(c), &sh) + + if err != nil { + log.Printf("YAML parse error on %s: %s (skipping)", n, err) + } + + if sh.Metadata == nil || sh.Metadata.Annotations == nil || len(sh.Metadata.Annotations) == 0 { + generic[n] = c + continue + } + + hookTypes, ok := sh.Metadata.Annotations[hookAnno] + if !ok { + generic[n] = c + continue + } + h := &release.Hook{ + Name: sh.Metadata.Name, + Kind: sh.Kind, + Path: n, + Manifest: c, + Events: []release.Hook_Event{}, + } + + isHook := false + for _, hookType := range strings.Split(hookTypes, ",") { + hookType = strings.ToLower(strings.TrimSpace(hookType)) + e, ok := events[hookType] + if ok { + isHook = true + h.Events = append(h.Events, e) + } + } + + if !isHook { + log.Printf("info: skipping unknown hook: %q", hookTypes) + continue + } + hs = append(hs, h) + } + return +} diff --git a/cmd/tiller/hooks_test.go b/cmd/tiller/hooks_test.go new file mode 100644 index 000000000..a33880525 --- /dev/null +++ b/cmd/tiller/hooks_test.go @@ -0,0 +1,121 @@ +package main + +import ( + "testing" + + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestSortHooks(t *testing.T) { + + data := []struct { + name string + path string + kind string + hooks []release.Hook_Event + manifest string + }{ + { + name: "first", + path: "one", + kind: "Job", + hooks: []release.Hook_Event{release.Hook_PRE_INSTALL}, + manifest: `apiVersion: v1 +kind: Job +metadata: + name: first + labels: + doesnt: matter + annotations: + "helm.sh/hook": pre-install +`, + }, + { + name: "second", + path: "two", + kind: "ReplicaSet", + hooks: []release.Hook_Event{release.Hook_POST_INSTALL}, + manifest: `kind: ReplicaSet +metadata: + name: second + annotations: + "helm.sh/hook": post-install +`, + }, { + name: "third", + path: "three", + kind: "ReplicaSet", + hooks: []release.Hook_Event{}, + manifest: `kind: ReplicaSet +metadata: + name: third + annotations: + "helm.sh/hook": no-such-hook +`, + }, { + name: "fourth", + path: "four", + kind: "Pod", + hooks: []release.Hook_Event{}, + manifest: `kind: Pod +metadata: + name: fourth + annotations: + nothing: here +`, + }, { + name: "fifth", + path: "five", + kind: "ReplicaSet", + hooks: []release.Hook_Event{release.Hook_POST_DELETE, release.Hook_POST_INSTALL}, + manifest: `kind: ReplicaSet +metadata: + name: fifth + annotations: + "helm.sh/hook": post-delete, post-install +`, + }, + } + + manifests := make(map[string]string, len(data)) + for _, o := range data { + manifests[o.path] = o.manifest + } + + hs, generic := sortHooks(manifests) + + if len(generic) != 1 { + t.Errorf("Expected 1 generic manifest, got %d", len(generic)) + } + + if len(hs) != 3 { + t.Errorf("Expected 3 hooks, got %d", len(hs)) + } + + for _, out := range hs { + found := false + for _, expect := range data { + if out.Path == expect.path { + found = true + if out.Path != expect.path { + t.Errorf("Expected path %s, got %s", expect.path, out.Path) + } + if out.Name != expect.name { + t.Errorf("Expected name %s, got %s", expect.name, out.Name) + } + if out.Kind != expect.kind { + t.Errorf("Expected kind %s, got %s", expect.kind, out.Kind) + } + for i := 0; i < len(out.Events); i++ { + if out.Events[i] != expect.hooks[i] { + t.Errorf("Expected event %d, got %d", expect.hooks[i], out.Events[i]) + } + } + } + } + if !found { + t.Errorf("Result not found: %v", out) + } + } + +} diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index c422cb0a7..c628ff8b7 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -216,43 +216,42 @@ func (s *releaseServer) engine(ch *chart.Chart) environment.Engine { } func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { + rel, err := s.prepareRelease(req) + if err != nil { + return nil, err + } + + return s.performRelease(rel, req) +} + +// prepareRelease builds a release for an install operation. +func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*release.Release, error) { if req.Chart == nil { return nil, errMissingChart } - ts := timeconv.Now() name, err := s.uniqName(req.Name) if err != nil { return nil, err } - overrides := map[string]interface{}{ - "Release": map[string]interface{}{ - "Name": name, - "Time": ts, - "Namespace": s.env.Namespace, - "Service": "Tiller", - }, - "Chart": req.Chart.Metadata, - } - - // Render the templates - // TODO: Fix based on whether chart has `engine: SOMETHING` set. - vals, err := chartutil.CoalesceValues(req.Chart, req.Values, nil) + ts := timeconv.Now() + options := chartutil.ReleaseOptions{Name: name, Time: ts, Namespace: s.env.Namespace} + valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options) if err != nil { return nil, err } - overrides["Values"] = vals - renderer := s.engine(req.Chart) - files, err := renderer.Render(req.Chart, overrides) + files, err := renderer.Render(req.Chart, valuesToRender) if err != nil { return nil, err } + hooks, manifests := sortHooks(files) + // Aggregate all non-hooks into one big doc. b := bytes.NewBuffer(nil) - for name, file := range files { + for name, file := range manifests { // Ignore templates that starts with underscore to handle them as partials if strings.HasPrefix(path.Base(name), "_") { continue @@ -267,7 +266,7 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea } // Store a release. - r := &release.Release{ + rel := &release.Release{ Name: name, Chart: req.Chart, Config: req.Values, @@ -277,22 +276,41 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea Status: &release.Status{Code: release.Status_UNKNOWN}, }, Manifest: b.String(), + Hooks: hooks, + Version: 1, } + return rel, nil +} +// performRelease runs a release. +func (s *releaseServer) performRelease(r *release.Release, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { res := &services.InstallReleaseResponse{Release: r} if req.DryRun { - log.Printf("Dry run for %s", name) + log.Printf("Dry run for %s", r.Name) return res, nil } - if err := s.env.KubeClient.Create(s.env.Namespace, b); err != nil { + // pre-install hooks + if err := s.execHook(r.Hooks, r.Name, preInstall); err != nil { + return res, err + } + + // regular manifests + kubeCli := s.env.KubeClient + b := bytes.NewBufferString(r.Manifest) + if err := kubeCli.Create(s.env.Namespace, b); err != nil { r.Info.Status.Code = release.Status_FAILED - log.Printf("warning: Release %q failed: %s", name, err) + log.Printf("warning: Release %q failed: %s", r.Name, err) if err := s.env.Releases.Create(r); err != nil { - log.Printf("warning: Failed to record release %q: %s", name, err) + log.Printf("warning: Failed to record release %q: %s", r.Name, err) } - return res, fmt.Errorf("release %s failed: %s", name, err) + return res, fmt.Errorf("release %s failed: %s", r.Name, err) + } + + // post-install hooks + if err := s.execHook(r.Hooks, r.Name, postInstall); err != nil { + return res, err } // This is a tricky case. The release has been created, but the result @@ -302,15 +320,51 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea // // One possible strategy would be to do a timed retry to see if we can get // this stored in the future. + r.Info.Status.Code = release.Status_DEPLOYED if err := s.env.Releases.Create(r); err != nil { - log.Printf("warning: Failed to record release %q: %s", name, err) - return res, nil + log.Printf("warning: Failed to record release %q: %s", r.Name, err) } - - r.Info.Status.Code = release.Status_DEPLOYED return res, nil } +func (s *releaseServer) execHook(hs []*release.Hook, name, hook string) error { + kubeCli := s.env.KubeClient + code, ok := events[hook] + if !ok { + return fmt.Errorf("unknown hook %q", hook) + } + + log.Printf("Executing %s hooks for %s", hook, name) + for _, h := range hs { + found := false + for _, e := range h.Events { + if e == code { + found = true + } + } + // If this doesn't implement the hook, skip it. + if !found { + continue + } + + b := bytes.NewBufferString(h.Manifest) + if err := kubeCli.Create(s.env.Namespace, b); err != nil { + log.Printf("wrning: Release %q pre-install %s failed: %s", name, h.Path, err) + return err + } + // No way to rewind a bytes.Buffer()? + b.Reset() + b.WriteString(h.Manifest) + if err := kubeCli.WatchUntilReady(s.env.Namespace, b); err != nil { + log.Printf("warning: Release %q pre-install %s could not complete: %s", name, h.Path, err) + return err + } + h.LastRun = timeconv.Now() + } + log.Printf("Hooks complete for %s %s", hook, name) + return nil +} + func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { if req.Name == "" { log.Printf("uninstall: Release not found: %s", req.Name) @@ -326,20 +380,27 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR log.Printf("uninstall: Deleting %s", req.Name) rel.Info.Status.Code = release.Status_DELETED rel.Info.Deleted = timeconv.Now() + res := &services.UninstallReleaseResponse{Release: rel} - b := bytes.NewBuffer([]byte(rel.Manifest)) + if err := s.execHook(rel.Hooks, rel.Name, preDelete); err != nil { + return res, err + } + b := bytes.NewBuffer([]byte(rel.Manifest)) if err := s.env.KubeClient.Delete(s.env.Namespace, b); err != nil { log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) return nil, err } + if err := s.execHook(rel.Hooks, rel.Name, postDelete); err != nil { + return res, err + } + if err := s.env.Releases.Update(rel); err != nil { log.Printf("uninstall: Failed to store updated release: %s", err) } - res := services.UninstallReleaseResponse{Release: rel} - return &res, nil + return res, nil } // byName implements the sort.Interface for []*release.Release. diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index ec2335291..47981e640 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -34,6 +34,16 @@ import ( "k8s.io/helm/pkg/timeconv" ) +var manifestWithHook = `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm + annotations: + "helm.sh/hook": post-install,pre-delete +data: + name: value +` + func rsFixture() *releaseServer { return &releaseServer{ env: mockEnvironment(), @@ -59,6 +69,18 @@ func releaseMock() *release.Release { }, }, Config: &chart.Config{Raw: `name = "value"`}, + Hooks: []*release.Hook{ + { + Name: "test-cm", + Kind: "ConfigMap", + Path: "test-cm", + Manifest: manifestWithHook, + Events: []release.Hook_Event{ + release.Hook_POST_INSTALL, + release.Hook_PRE_DELETE, + }, + }, + }, } } @@ -71,6 +93,7 @@ func TestInstallRelease(t *testing.T) { Metadata: &chart.Metadata{Name: "hello"}, Templates: []*chart.Template{ {Name: "hello", Data: []byte("hello: world")}, + {Name: "hooks", Data: []byte(manifestWithHook)}, }, }, } @@ -89,6 +112,20 @@ func TestInstallRelease(t *testing.T) { t.Logf("rel: %v", rel) + if len(rel.Hooks) != 1 { + t.Fatalf("Expected 1 hook, got %d", len(rel.Hooks)) + } + if rel.Hooks[0].Manifest != manifestWithHook { + t.Errorf("Unexpected manifest: %v", rel.Hooks[0].Manifest) + } + + if rel.Hooks[0].Events[0] != release.Hook_POST_INSTALL { + t.Errorf("Expected event 0 is post install") + } + if rel.Hooks[0].Events[1] != release.Hook_PRE_DELETE { + t.Errorf("Expected event 0 is pre-delete") + } + if len(res.Release.Manifest) == 0 { t.Errorf("No manifest returned: %v", res.Release) } @@ -97,7 +134,7 @@ func TestInstallRelease(t *testing.T) { t.Errorf("Expected manifest in %v", res) } - if !strings.Contains(rel.Manifest, "---\n# Source: hello\nhello: world") { + if !strings.Contains(rel.Manifest, "---\n# Source: hello/hello\nhello: world") { t.Errorf("unexpected output: %s", rel.Manifest) } } @@ -113,8 +150,9 @@ func TestInstallReleaseDryRun(t *testing.T) { {Name: "hello", Data: []byte("hello: world")}, {Name: "goodbye", Data: []byte("goodbye: world")}, {Name: "empty", Data: []byte("")}, - {Name: "with-partials", Data: []byte("hello: {{ template \"partials/_planet\" . }}")}, - {Name: "partials/_planet", Data: []byte("Earth")}, + {Name: "with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, + {Name: "partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, + {Name: "hooks", Data: []byte(manifestWithHook)}, }, }, DryRun: true, @@ -127,11 +165,11 @@ func TestInstallReleaseDryRun(t *testing.T) { t.Errorf("Expected release name.") } - if !strings.Contains(res.Release.Manifest, "---\n# Source: hello\nhello: world") { + if !strings.Contains(res.Release.Manifest, "---\n# Source: hello/hello\nhello: world") { t.Errorf("unexpected output: %s", res.Release.Manifest) } - if !strings.Contains(res.Release.Manifest, "---\n# Source: goodbye\ngoodbye: world") { + if !strings.Contains(res.Release.Manifest, "---\n# Source: hello/goodbye\ngoodbye: world") { t.Errorf("unexpected output: %s", res.Release.Manifest) } @@ -139,7 +177,7 @@ func TestInstallReleaseDryRun(t *testing.T) { t.Errorf("Should contain partial content. %s", res.Release.Manifest) } - if strings.Contains(res.Release.Manifest, "hello: {{ template \"partials/_planet\" . }}") { + if strings.Contains(res.Release.Manifest, "hello: {{ template \"_planet\" . }}") { t.Errorf("Should not contain partial templates itself. %s", res.Release.Manifest) } @@ -150,6 +188,14 @@ func TestInstallReleaseDryRun(t *testing.T) { if _, err := rs.env.Releases.Read(res.Release.Name); err == nil { t.Errorf("Expected no stored release.") } + + if l := len(res.Release.Hooks); l != 1 { + t.Fatalf("Expected 1 hook, got %d", l) + } + + if res.Release.Hooks[0].LastRun != nil { + t.Error("Expected hook to not be marked as run.") + } } func TestUninstallRelease(t *testing.T) { @@ -163,6 +209,18 @@ func TestUninstallRelease(t *testing.T) { Code: release.Status_DEPLOYED, }, }, + Hooks: []*release.Hook{ + { + Name: "test-cm", + Kind: "ConfigMap", + Path: "test-cm", + Manifest: manifestWithHook, + Events: []release.Hook_Event{ + release.Hook_POST_INSTALL, + release.Hook_PRE_DELETE, + }, + }, + }, }) req := &services.UninstallReleaseRequest{ @@ -182,6 +240,10 @@ func TestUninstallRelease(t *testing.T) { t.Errorf("Expected status code to be DELETED, got %d", res.Release.Info.Status.Code) } + if res.Release.Hooks[0].LastRun.Seconds == 0 { + t.Error("Expected LastRun to be greater than zero.") + } + if res.Release.Info.Deleted.Seconds <= 0 { t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds) } diff --git a/cmd/tiller/tiller.go b/cmd/tiller/tiller.go index 3fdb86b6e..3a429169d 100644 --- a/cmd/tiller/tiller.go +++ b/cmd/tiller/tiller.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package main // import "k8s.io/helm/cmd/tiller" import ( "fmt" diff --git a/docs/charts.md b/docs/charts.md index f1d119d87..68d84b430 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -142,7 +142,9 @@ When a user supplies custom values, these values will override the values in the chart's `values.yaml` file. ### Template Files -Template files follow the standard conventions for writing Go templates. +Template files follow the standard conventions for writing Go templates +(see [the text/template Go package documentation](https://golang.org/pkg/text/template/) +for details). An example template file might look something like this: ```yaml @@ -302,9 +304,9 @@ apache: ``` The above adds a `global` section with the value `app: MyWordpress`. -This value is available to _all_ charts as `.global.app`. +This value is available to _all_ charts as `.Values.global.app`. -For example, the `mysql` templates may access `app` as `{{.global.app}}`, and +For example, the `mysql` templates may access `app` as `{{.Values.global.app}}`, and so can the `apache` chart. Effectively, the values file above is regenerated like this: diff --git a/docs/developers.md b/docs/developers.md index 61c65d4c0..79ece066a 100644 --- a/docs/developers.md +++ b/docs/developers.md @@ -16,7 +16,7 @@ Helm and Tiller. We use Make to build our programs. The simplest way to get started is: ```console -$ make boostrap build +$ make bootstrap build ``` This will build both Helm and Tiller. `make bootstrap` will attempt to diff --git a/docs/examples/alpine/values.yaml b/docs/examples/alpine/values.yaml index bb6c06ae4..879d760f9 100644 --- a/docs/examples/alpine/values.yaml +++ b/docs/examples/alpine/values.yaml @@ -1,2 +1,2 @@ # The pod name -name: my-alpine +Name: my-alpine diff --git a/docs/examples/nginx/templates/post-install-job.yaml b/docs/examples/nginx/templates/post-install-job.yaml new file mode 100644 index 000000000..7a21b2408 --- /dev/null +++ b/docs/examples/nginx/templates/post-install-job.yaml @@ -0,0 +1,31 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "{{template "fullname" . }}" + labels: + heritage: {{.Release.Service | quote }} + release: {{.Release.Name | quote }} + chart: "{{.Chart.Name}}-{{.Chart.Version}}" + annotations: + # This is what defines this resource as a hook. Without this line, the + # job is considered part of the release. + "helm.sh/hook": post-install +spec: + template: + metadata: + name: "{{template "fullname" . }}" + labels: + heritage: {{.Release.Service | quote }} + release: {{.Release.Name | quote }} + chart: "{{.Chart.Name}}-{{.Chart.Version}}" + 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: Never + containers: + - name: {{template "fullname" .}}-job + image: "alpine:3.3" + # All we're going to do is sleep for a minute, then exit. + command: ["/bin/sleep","{{default "10" .Values.sleepyTime}}"] diff --git a/docs/examples/nginx/templates/pre-install-secret.yaml b/docs/examples/nginx/templates/pre-install-secret.yaml new file mode 100644 index 000000000..c2ca3e1d2 --- /dev/null +++ b/docs/examples/nginx/templates/pre-install-secret.yaml @@ -0,0 +1,14 @@ +# This shows a secret as a pre-install hook. +# A pre-install hook is run before the rest of the chart is loaded. +apiVersion: v1 +kind: Secret +metadata: + name: "{{.Release.Name}}-secret" + # This declares the resource to be a hook. By convention, we also name the + # file "pre-install-XXX.yaml", but Helm itself doesn't care about file names. + annotations: + "helm.sh/hook": pre-install +type: Opaque +data: + password: {{ b64enc "secret" }} + username: {{ b64enc "user1" }} diff --git a/docs/examples/nginx/values.yaml b/docs/examples/nginx/values.yaml index b32a82965..e0aff99b4 100644 --- a/docs/examples/nginx/values.yaml +++ b/docs/examples/nginx/values.yaml @@ -11,6 +11,9 @@ httpPort: 8888 # Number of nginx instances to run replicaCount: 1 +# Evaluated by the post-install hook +sleepyTime: "10" + index: >-

Hello

This is a test

diff --git a/glide.lock b/glide.lock index 402e12b86..84dbc29a6 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: b84a1c02841aebb58710da5c7fe6865ab5d4e43d6c51fa8cd9ec68ceb1ebe37e -updated: 2016-06-26T14:51:02.02536382-07:00 +hash: 141ef5b9c491c91b026ab4007e48502c9a6df9f173c40e1406233dd44f065190 +updated: 2016-07-05T16:51:52.631048739-07:00 imports: - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 @@ -133,7 +133,7 @@ imports: - ptypes/any - ptypes/timestamp - name: github.com/google/cadvisor - version: 7d22cf63253c17bad8ab64b8eef679718d00342e + version: 4dbefc9b671b81257973a33211fb12370c1a526e subpackages: - api - cache/memory @@ -238,8 +238,6 @@ imports: version: 490cc6eb5fa45bf8a8b7b73c8bc82a8160e8531d - name: github.com/spf13/cobra version: 6a8bd97bdb1fc0d08a83459940498ea49d3e8c93 - subpackages: - - cobra - name: github.com/spf13/pflag version: 367864438f1b1a3c7db4da06a2f55b144e6784e0 - name: github.com/technosophos/moniker @@ -286,11 +284,11 @@ imports: - name: google.golang.org/grpc version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 subpackages: + - metadata - codes - credentials - grpclog - internal - - metadata - naming - transport - peer @@ -299,7 +297,7 @@ imports: - name: gopkg.in/yaml.v2 version: a83829b6f1293c91addabc89d0571c246397bbf4 - name: k8s.io/kubernetes - version: caf9a4d87700ba034a7b39cced19bd5628ca6aa3 + version: 283137936a498aed572ee22af6774b6fb6e9fd94 subpackages: - pkg/api - pkg/api/meta @@ -326,13 +324,13 @@ imports: - pkg/util/intstr - pkg/util/rand - pkg/util/sets - - 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 @@ -361,6 +359,7 @@ imports: - pkg/util/strategicpatch - pkg/watch - pkg/util/yaml + - pkg/api/testapi - third_party/forked/reflect - pkg/conversion/queryparams - pkg/util/json @@ -425,7 +424,7 @@ imports: - pkg/util/framer - third_party/forked/json - pkg/util/parsers - - federation/apis/federation/v1alpha1 + - federation/apis/federation/v1beta1 - pkg/apis/apps/v1alpha1 - pkg/apis/authentication.k8s.io - pkg/apis/authentication.k8s.io/v1beta1 @@ -452,4 +451,4 @@ imports: version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 repo: https://github.com/go-inf/inf.git vcs: git -devImports: [] +testImports: [] diff --git a/glide.yaml b/glide.yaml index 52bc471be..79020c590 100644 --- a/glide.yaml +++ b/glide.yaml @@ -5,8 +5,8 @@ import: subpackages: - context - package: github.com/spf13/cobra - subpackages: - - cobra +- package: github.com/spf13/pflag + version: 367864438f1b1a3c7db4da06a2f55b144e6784e0 - package: github.com/Masterminds/sprig version: ^2.3 - package: gopkg.in/yaml.v2 @@ -22,7 +22,7 @@ import: - package: google.golang.org/grpc version: dec33edc378cf4971a2741cfd86ed70a644d6ba3 - package: k8s.io/kubernetes - version: v1.3.0-beta.2 + version: v1.3.0 subpackages: - pkg/api - pkg/api/meta diff --git a/pkg/chartutil/doc.go b/pkg/chartutil/doc.go index 09f4d4f5d..b03109a3b 100644 --- a/pkg/chartutil/doc.go +++ b/pkg/chartutil/doc.go @@ -41,4 +41,4 @@ into a Chart. When creating charts in memory, use the 'k8s.io/helm/pkg/proto/happy/chart' package directly. */ -package chartutil +package chartutil // import "k8s.io/helm/pkg/chartutil" diff --git a/pkg/chartutil/expand.go b/pkg/chartutil/expand.go index 4dc19d71a..45bb9e474 100644 --- a/pkg/chartutil/expand.go +++ b/pkg/chartutil/expand.go @@ -40,6 +40,16 @@ func Expand(dir string, r io.Reader) error { return err } + //split header name and create missing directories + d, _ := filepath.Split(header.Name) + fullDir := filepath.Join(dir, d) + _, err = os.Stat(fullDir) + if err != nil && d != "" { + if err := os.MkdirAll(fullDir, 0700); err != nil { + return err + } + } + path := filepath.Clean(filepath.Join(dir, header.Name)) info := header.FileInfo() if info.IsDir() { diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 3987c5a16..d135f41df 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -17,18 +17,19 @@ limitations under the License. package chartutil import ( - "errors" + "fmt" "io" "io/ioutil" "log" "strings" "github.com/ghodss/yaml" + "github.com/golang/protobuf/ptypes/timestamp" "k8s.io/helm/pkg/proto/hapi/chart" ) // ErrNoTable indicates that a chart does not have a matching table. -var ErrNoTable = errors.New("no table") +type ErrNoTable error // GlobalKey is the name of the Values key that is used for storing global vars. const GlobalKey = "global" @@ -92,7 +93,7 @@ func (v Values) Encode(w io.Writer) error { func tableLookup(v Values, simple string) (Values, error) { v2, ok := v[simple] if !ok { - return v, ErrNoTable + return v, ErrNoTable(fmt.Errorf("no table named %q (%v)", simple, v)) } if vv, ok := v2.(map[string]interface{}); ok { return vv, nil @@ -105,14 +106,15 @@ func tableLookup(v Values, simple string) (Values, error) { return vv, nil } - return map[string]interface{}{}, ErrNoTable + var e ErrNoTable = fmt.Errorf("no table named %q", simple) + return map[string]interface{}{}, e } // ReadValues will parse YAML byte data into a Values. func ReadValues(data []byte) (vals Values, err error) { - vals = make(map[string]interface{}) - if len(data) > 0 { - err = yaml.Unmarshal(data, &vals) + err = yaml.Unmarshal(data, &vals) + if len(vals) == 0 { + vals = Values{} } return } @@ -138,7 +140,7 @@ func ReadValuesFile(filename string) (Values, error) { // - A chart has access to all of the variables for it, as well as all of // the values destined for its dependencies. func CoalesceValues(chrt *chart.Chart, vals *chart.Config, overrides map[string]interface{}) (Values, error) { - var cvals Values + cvals := Values{} // Parse values if not nil. We merge these at the top level because // the passed-in values are in the same namespace as the parent chart. if vals != nil { @@ -288,6 +290,35 @@ func coalesceTables(dst, src map[string]interface{}) map[string]interface{} { return dst } +// ReleaseOptions represents the additional release options needed +// for the composition of the final values struct +type ReleaseOptions struct { + Name string + Time *timestamp.Timestamp + Namespace string +} + +// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files +func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions) (Values, error) { + overrides := map[string]interface{}{ + "Release": map[string]interface{}{ + "Name": options.Name, + "Time": options.Time, + "Namespace": options.Namespace, + "Service": "Tiller", + }, + "Chart": chrt.Metadata, + } + + vals, err := CoalesceValues(chrt, chrtVals, nil) + if err != nil { + return overrides, err + } + + overrides["Values"] = vals + return overrides, nil +} + // istable is a special-purpose function to see if the present thing matches the definition of a YAML table. func istable(v interface{}) bool { _, ok := v.(map[string]interface{}) diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go index bd55046fb..75d0d2e7d 100644 --- a/pkg/chartutil/values_test.go +++ b/pkg/chartutil/values_test.go @@ -53,6 +53,18 @@ water: t.Fatalf("Error parsing bytes: %s", err) } matchValues(t, data) + + tests := []string{`poet: "Coleridge"`, "# Just a comment", ""} + + for _, tt := range tests { + data, err = ReadValues([]byte(tt)) + if err != nil { + t.Fatalf("Error parsing bytes: %s", err) + } + if data == nil { + t.Errorf(`YAML string "%s" gave a nil map`, tt) + } + } } func TestReadValuesFile(t *testing.T) { diff --git a/pkg/client/install.go b/pkg/client/install.go index 9d088cfab..10d216b0d 100644 --- a/pkg/client/install.go +++ b/pkg/client/install.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package client +package client // import "k8s.io/helm/pkg/client" import ( "bytes" diff --git a/pkg/engine/doc.go b/pkg/engine/doc.go index 8407bf209..53c4084b0 100644 --- a/pkg/engine/doc.go +++ b/pkg/engine/doc.go @@ -20,4 +20,4 @@ Tiller provides a simple interface for taking a Chart and rendering its template The 'engine' package implements this interface using Go's built-in 'text/template' package. */ -package engine +package engine // import "k8s.io/helm/pkg/engine" diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 0a30437e4..934917701 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -19,6 +19,9 @@ package engine import ( "bytes" "fmt" + "log" + "path" + "strings" "text/template" "github.com/Masterminds/sprig" @@ -31,6 +34,9 @@ type Engine struct { // FuncMap contains the template functions that will be passed to each // render call. This may only be modified before the first call to Render. FuncMap template.FuncMap + // If strict is enabled, template rendering will fail if a template references + // a value that was not passed in. + Strict bool } // New creates a new Go template Engine instance. @@ -92,8 +98,16 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) { // to share common blocks, but to make the entire thing feel like a file-based // template engine. t := template.New("gotpl") + if e.Strict { + t.Option("missingkey=error") + } else { + // Not that zero will attempt to add default values for types it knows, + // but will still emit for others. We mitigate that later. + t.Option("missingkey=zero") + } files := []string{} for fname, r := range tpls { + log.Printf("Preparing template %s", fname) t = t.New(fname).Funcs(e.FuncMap) if _, err := t.Parse(r.tpl); err != nil { return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err) @@ -104,10 +118,17 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) { rendered := make(map[string]string, len(files)) var buf bytes.Buffer for _, file := range files { - if err := t.ExecuteTemplate(&buf, file, tpls[file].vals); err != nil { + // At render time, add information about the template that is being rendered. + vals := tpls[file].vals + vals["Template"] = map[string]interface{}{"Name": file} + if err := t.ExecuteTemplate(&buf, file, vals); err != nil { return map[string]string{}, fmt.Errorf("render error in %q: %s", file, err) } - rendered[file] = buf.String() + + // Work around the issue where Go will emit "" even if Options(missing=zero) + // is set. Since missing=error will never get here, we do not need to handle + // the Strict case. + rendered[file] = strings.Replace(buf.String(), "", "", -1) buf.Reset() } @@ -119,7 +140,7 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) { // As it goes, it also prepares the values in a scope-sensitive manner. func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { templates := map[string]renderable{} - recAllTpls(c, templates, vals, true) + recAllTpls(c, templates, vals, true, "") return templates } @@ -127,39 +148,44 @@ func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable { // // As it recurses, it also sets the values to be appropriate for the template // scope. -func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values, top bool) { - var cvals chartutil.Values +func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals chartutil.Values, top bool, parentID string) { + // This should never evaluate to a nil map. That will cause problems when + // values are appended later. + cvals := chartutil.Values{} if top { // If this is the top of the rendering tree, assume that parentVals // is already resolved to the authoritative values. cvals = parentVals } else if c.Metadata != nil && c.Metadata.Name != "" { - // An error indicates that the table doesn't exist. So we leave it as - // an empty map. - - var tmp chartutil.Values - vs, err := parentVals.Table("Values") - if err == nil { - tmp, err = vs.Table(c.Metadata.Name) - } else { - tmp, err = parentVals.Table(c.Metadata.Name) + // If there is a {{.Values.ThisChart}} in the parent metadata, + // copy that into the {{.Values}} for this template. + newVals := chartutil.Values{} + if vs, err := parentVals.Table("Values"); err == nil { + if tmp, err := vs.Table(c.Metadata.Name); err == nil { + newVals = tmp + } } - //tmp, err := parentVals["Values"].(chartutil.Values).Table(c.Metadata.Name) - if err == nil { - cvals = map[string]interface{}{ - "Values": tmp, - "Release": parentVals["Release"], - "Chart": c, - } + cvals = map[string]interface{}{ + "Values": newVals, + "Release": parentVals["Release"], + "Chart": c.Metadata, } } + newParentID := c.Metadata.Name + if parentID != "" { + // We artificially reconstruct the chart path to child templates. This + // creates a namespaced filename that can be used to track down the source + // of a particular template declaration. + newParentID = path.Join(parentID, "charts", newParentID) + } + for _, child := range c.Dependencies { - recAllTpls(child, templates, cvals, false) + recAllTpls(child, templates, cvals, false, newParentID) } for _, t := range c.Templates { - templates[t.Name] = renderable{ + templates[path.Join(newParentID, t.Name)] = renderable{ tpl: string(t.Data), vals: cvals, } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index e8c8e54cc..46ec43e54 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -46,6 +46,7 @@ func TestRender(t *testing.T) { Templates: []*chart.Template{ {Name: "test1", Data: []byte("{{.outer | title }} {{.inner | title}}")}, {Name: "test2", Data: []byte("{{.global.callme | lower }}")}, + {Name: "test3", Data: []byte("{{.noValue}}")}, }, Values: &chart.Config{ Raw: "outer: DEFAULT\ninner: DEFAULT", @@ -74,14 +75,18 @@ func TestRender(t *testing.T) { } expect := "Spouter Inn" - if out["test1"] != expect { + if out["moby/test1"] != expect { t.Errorf("Expected %q, got %q", expect, out["test1"]) } expect = "ishmael" - if out["test2"] != expect { + if out["moby/test2"] != expect { t.Errorf("Expected %q, got %q", expect, out["test2"]) } + expect = "" + if out["moby/test3"] != expect { + t.Errorf("Expected %q, got %q", expect, out["test3"]) + } if _, err := e.Render(c, v); err != nil { t.Errorf("Unexpected error: %s", err) @@ -149,18 +154,21 @@ func TestParallelRenderInternals(t *testing.T) { func TestAllTemplates(t *testing.T) { ch1 := &chart.Chart{ + Metadata: &chart.Metadata{Name: "ch1"}, Templates: []*chart.Template{ {Name: "foo", Data: []byte("foo")}, {Name: "bar", Data: []byte("bar")}, }, Dependencies: []*chart.Chart{ { + Metadata: &chart.Metadata{Name: "laboratory mice"}, Templates: []*chart.Template{ {Name: "pinky", Data: []byte("pinky")}, {Name: "brain", Data: []byte("brain")}, }, - Dependencies: []*chart.Chart{ - {Templates: []*chart.Template{ + Dependencies: []*chart.Chart{{ + Metadata: &chart.Metadata{Name: "same thing we do every night"}, + Templates: []*chart.Template{ {Name: "innermost", Data: []byte("innermost")}, }}, }, @@ -180,11 +188,13 @@ func TestRenderDependency(t *testing.T) { deptpl := `{{define "myblock"}}World{{end}}` toptpl := `Hello {{template "myblock"}}` ch := &chart.Chart{ + Metadata: &chart.Metadata{Name: "outerchart"}, Templates: []*chart.Template{ {Name: "outer", Data: []byte(toptpl)}, }, Dependencies: []*chart.Chart{ { + Metadata: &chart.Metadata{Name: "innerchart"}, Templates: []*chart.Template{ {Name: "inner", Data: []byte(deptpl)}, }, @@ -203,7 +213,7 @@ func TestRenderDependency(t *testing.T) { } expect := "Hello World" - if out["outer"] != expect { + if out["outerchart/outer"] != expect { t.Errorf("Expected %q, got %q", expect, out["outer"]) } @@ -212,10 +222,11 @@ func TestRenderDependency(t *testing.T) { func TestRenderNestedValues(t *testing.T) { e := New() - innerpath := "charts/inner/templates/inner.tpl" + innerpath := "templates/inner.tpl" outerpath := "templates/outer.tpl" - deepestpath := "charts/inner/charts/deepest/templates/deepest.tpl" - checkrelease := "charts/inner/charts/deepest/templates/release.tpl" + // Ensure namespacing rules are working. + deepestpath := "templates/inner.tpl" + checkrelease := "templates/release.tpl" deepest := &chart.Chart{ Metadata: &chart.Metadata{Name: "deepest"}, @@ -280,19 +291,65 @@ global: t.Fatalf("failed to render templates: %s", err) } - if out[outerpath] != "Gather ye rosebuds while ye may" { + if out["top/"+outerpath] != "Gather ye rosebuds while ye may" { t.Errorf("Unexpected outer: %q", out[outerpath]) } - if out[innerpath] != "Old time is still a-flyin'" { + if out["top/charts/herrick/"+innerpath] != "Old time is still a-flyin'" { t.Errorf("Unexpected inner: %q", out[innerpath]) } - if out[deepestpath] != "And this same flower that smiles to-day" { + if out["top/charts/herrick/charts/deepest/"+deepestpath] != "And this same flower that smiles to-day" { t.Errorf("Unexpected deepest: %q", out[deepestpath]) } - if out[checkrelease] != "Tomorrow will be dyin" { + if out["top/charts/herrick/charts/deepest/"+checkrelease] != "Tomorrow will be dyin" { t.Errorf("Unexpected release: %q", out[checkrelease]) } } + +func TestRenderBuiltinValues(t *testing.T) { + inner := &chart.Chart{ + Metadata: &chart.Metadata{Name: "Latium"}, + Templates: []*chart.Template{ + {Name: "Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + }, + Values: &chart.Config{Raw: ``}, + Dependencies: []*chart.Chart{}, + } + + outer := &chart.Chart{ + Metadata: &chart.Metadata{Name: "Troy"}, + Templates: []*chart.Template{ + {Name: "Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + }, + Values: &chart.Config{Raw: ``}, + Dependencies: []*chart.Chart{inner}, + } + + inject := chartutil.Values{ + "Values": &chart.Config{Raw: ""}, + "Chart": outer.Metadata, + "Release": chartutil.Values{ + "Name": "Aeneid", + }, + } + + t.Logf("Calculated values: %v", outer) + + out, err := New().Render(outer, inject) + if err != nil { + t.Fatalf("failed to render templates: %s", err) + } + + expects := map[string]string{ + "Troy/charts/Latium/Lavinia": "Troy/charts/Latium/LaviniaLatiumAeneid", + "Troy/Aeneas": "Troy/AeneasTroyAeneid", + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } + +} diff --git a/pkg/helm/client.go b/pkg/helm/client.go index bcfd41e08..b1efcac2d 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package helm // import "k8s.io/helm/pkg/helm" import ( "google.golang.org/grpc" @@ -24,29 +24,30 @@ import ( ) const ( - // $HELM_HOST envvar + // HelmHostEnvVar is the $HELM_HOST envvar HelmHostEnvVar = "HELM_HOST" - // $HELM_HOME envvar + // HelmHomeEnvVar is the $HELM_HOME envvar HelmHomeEnvVar = "HELM_HOME" - // Default tiller server host address. + // DefaultHelmHost is the default tiller server host address. DefaultHelmHost = ":44134" - // Default $HELM_HOME envvar value + // DefaultHelmHome is the default $HELM_HOME envvar value DefaultHelmHome = "$HOME/.helm" ) -// Helm client manages client side of the helm-tiller protocol +// Client manages client side of the helm-tiller protocol type Client struct { opts options } +// NewClient creates a new client. func NewClient(opts ...Option) *Client { return new(Client).Init().Option(opts...) } -// Configure the helm client with the provided options +// Option configures the helm client with the provided options func (h *Client) Option(opts ...Option) *Client { for _, opt := range opts { opt(&h.opts) @@ -54,10 +55,10 @@ func (h *Client) Option(opts ...Option) *Client { return h } -// Initializes the helm client with default options +// Init initializes the helm client with default options func (h *Client) Init() *Client { - return h.Option(HelmHost(DefaultHelmHost)). - Option(HelmHome(os.ExpandEnv(DefaultHelmHome))) + return h.Option(Host(DefaultHelmHost)). + Option(Home(os.ExpandEnv(DefaultHelmHome))) } // ListReleases lists the current releases. @@ -87,7 +88,7 @@ func (h *Client) InstallRelease(chStr string, opts ...InstallOption) (*rls.Insta return h.opts.rpcInstallRelease(chart, rls.NewReleaseServiceClient(c), opts...) } -// UninstallRelease uninstalls a named release and returns the response. +// DeleteRelease uninstalls a named release and returns the response. // // Note: there aren't currently any supported DeleteOptions, but they are // kept in the API signature as a placeholder for future additions. diff --git a/pkg/helm/compat.go b/pkg/helm/compat.go index 9d4b7a3c5..d398f83ae 100644 --- a/pkg/helm/compat.go +++ b/pkg/helm/compat.go @@ -23,10 +23,13 @@ import ( // These APIs are a temporary abstraction layer that captures the interaction between the current cmd/helm and old // pkg/helm implementations. Post refactor the cmd/helm package will use the APIs exposed on helm.Client directly. +// Config is the base configuration var Config struct { ServAddr string } +// ListReleases lists releases. DEPRECATED. +// // Soon to be deprecated helm ListReleases API. func ListReleases(limit int, offset string, sort rls.ListSort_SortBy, order rls.ListSort_SortOrder, filter string) (*rls.ListReleasesResponse, error) { opts := []ReleaseListOption{ @@ -36,36 +39,42 @@ func ListReleases(limit int, offset string, sort rls.ListSort_SortBy, order rls. ReleaseListSort(int32(sort)), ReleaseListOrder(int32(order)), } - return NewClient(HelmHost(Config.ServAddr)).ListReleases(opts...) + return NewClient(Host(Config.ServAddr)).ListReleases(opts...) } +// GetReleaseStatus gets a release status. DEPRECATED +// // Soon to be deprecated helm GetReleaseStatus API. func GetReleaseStatus(rlsName string) (*rls.GetReleaseStatusResponse, error) { - return NewClient(HelmHost(Config.ServAddr)).ReleaseStatus(rlsName) + return NewClient(Host(Config.ServAddr)).ReleaseStatus(rlsName) } +// GetReleaseContent gets the content of a release. // Soon to be deprecated helm GetReleaseContent API. func GetReleaseContent(rlsName string) (*rls.GetReleaseContentResponse, error) { - return NewClient(HelmHost(Config.ServAddr)).ReleaseContent(rlsName) + return NewClient(Host(Config.ServAddr)).ReleaseContent(rlsName) } +// UpdateRelease updates a release. // Soon to be deprecated helm UpdateRelease API. func UpdateRelease(rlsName string) (*rls.UpdateReleaseResponse, error) { - return NewClient(HelmHost(Config.ServAddr)).UpdateRelease(rlsName) + return NewClient(Host(Config.ServAddr)).UpdateRelease(rlsName) } +// InstallRelease runs an install for a release. // Soon to be deprecated helm InstallRelease API. func InstallRelease(vals []byte, rlsName, chStr string, dryRun bool) (*rls.InstallReleaseResponse, error) { - client := NewClient(HelmHost(Config.ServAddr)) + client := NewClient(Host(Config.ServAddr)) if dryRun { client.Option(DryRun()) } return client.InstallRelease(chStr, ValueOverrides(vals), ReleaseName(rlsName)) } +// UninstallRelease destroys an existing release. // Soon to be deprecated helm UninstallRelease API. func UninstallRelease(rlsName string, dryRun bool) (*rls.UninstallReleaseResponse, error) { - client := NewClient(HelmHost(Config.ServAddr)) + client := NewClient(Host(Config.ServAddr)) if dryRun { client.Option(DryRun()) } diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go new file mode 100644 index 000000000..4cba4db43 --- /dev/null +++ b/pkg/helm/interface.go @@ -0,0 +1,31 @@ +/* +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 helm + +import ( + rls "k8s.io/helm/pkg/proto/hapi/services" +) + +// Interface for helm client for mocking in tests +type Interface interface { + ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) + InstallRelease(chStr string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) + DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) + ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) + UpdateRelease(rlsName string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) + ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) +} diff --git a/pkg/helm/option.go b/pkg/helm/option.go index cfc43769a..efc3f35c1 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -51,15 +51,15 @@ func DryRun() Option { } } -// HelmHome specifies the location of helm home, (default = "$HOME/.helm"). -func HelmHome(home string) Option { +// Home specifies the location of helm home, (default = "$HOME/.helm"). +func Home(home string) Option { return func(opts *options) { opts.home = home } } -// HelmHost specifies the host address of the Tiller release server, (default = ":44134"). -func HelmHost(host string) Option { +// Host specifies the host address of the Tiller release server, (default = ":44134"). +func Host(host string) Option { return func(opts *options) { opts.host = host } diff --git a/pkg/ignore/doc.go b/pkg/ignore/doc.go index 6cb4dcdb6..7281c33a9 100644 --- a/pkg/ignore/doc.go +++ b/pkg/ignore/doc.go @@ -64,4 +64,4 @@ Notable differences from .gitignore: - The evaluation of escape sequences has not been tested for compatibility - There is no support for '\!' as a special leading sequence. */ -package ignore +package ignore // import "k8s.io/helm/pkg/ignore" diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 0ed34932e..f589cc75a 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -14,17 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube +package kube // import "k8s.io/helm/pkg/kube" import ( "fmt" "io" + "log" + "time" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/errors" + "k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/watch" ) // Client represents a client capable of communicating with the Kubernetes API. @@ -59,6 +63,24 @@ func (c *Client) Delete(namespace string, reader io.Reader) error { return perform(c, namespace, reader, deleteResource) } +// WatchUntilReady watches the resource given in the reader, and waits until it is ready. +// +// This function is mainly for hook implementations. It watches for a resource to +// hit a particular milestone. The milestone depends on the Kind. +// +// For most kinds, it checks to see if the resource is marked as Added or Modified +// by the Kubernetes event stream. For some kinds, it does more: +// +// - Jobs: A job is marked "Ready" when it has successfully completed. This is +// ascertained by watching the Status fields in a job's output. +// +// Handling for other kinds will be added as necessary. +func (c *Client) WatchUntilReady(namespace string, reader io.Reader) error { + // For jobs, there's also the option to do poll c.Jobs(namespace).Get(): + // https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300 + return perform(c, namespace, reader, watchUntilReady) +} + const includeThirdPartyAPIs = false func perform(c *Client, namespace string, reader io.Reader, fn ResourceActorFunc) error { @@ -105,6 +127,69 @@ func deleteResource(info *resource.Info) error { return resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name) } +func watchUntilReady(info *resource.Info) error { + w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion) + if err != nil { + return err + } + + kind := info.Mapping.GroupVersionKind.Kind + log.Printf("Watching for changes to %s %s", kind, info.Name) + timeout := time.Minute * 5 + + // What we watch for depends on the Kind. + // - For a Job, we watch for completion. + // - For all else, we watch until Ready. + // In the future, we might want to add some special logic for types + // like Ingress, Volume, etc. + + _, err = watch.Until(timeout, w, func(e watch.Event) (bool, error) { + switch e.Type { + case watch.Added, watch.Modified: + // For things like a secret or a config map, this is the best indicator + // we get. We care mostly about jobs, where what we want to see is + // the status go into a good state. For other types, like ReplicaSet + // we don't really do anything to support these as hooks. + log.Printf("Add/Modify event for %s: %v", info.Name, e.Type) + if kind == "Job" { + return waitForJob(e, info.Name) + } + return true, nil + case watch.Deleted: + log.Printf("Deleted event for %s", info.Name) + return true, nil + case watch.Error: + // Handle error and return with an error. + log.Printf("Error event for %s", info.Name) + return true, fmt.Errorf("Failed to deploy %s", info.Name) + default: + return false, nil + } + }) + return err +} + +// waitForJob is a helper that waits for a job to complete. +// +// This operates on an event returned from a watcher. +func waitForJob(e watch.Event, name string) (bool, error) { + o, ok := e.Object.(*batch.Job) + if !ok { + return true, fmt.Errorf("Expected %s to be a *batch.Job, got %T", name, o) + } + + for _, c := range o.Status.Conditions { + if c.Type == batch.JobComplete && c.Status == api.ConditionTrue { + return true, nil + } else if c.Type == batch.JobFailed && c.Status == api.ConditionTrue { + return true, fmt.Errorf("Job failed: %s", c.Reason) + } + } + + log.Printf("%s: Jobs active: %d, jobs failed: %d, jobs succeeded: %d", name, o.Status.Active, o.Status.Failed, o.Status.Succeeded) + return false, nil +} + func (c *Client) ensureNamespace(namespace string) error { client, err := c.Client() if err != nil { diff --git a/pkg/lint/lint.go b/pkg/lint/lint.go index dbe980606..7903d215c 100644 --- a/pkg/lint/lint.go +++ b/pkg/lint/lint.go @@ -14,24 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -package lint +package lint // import "k8s.io/helm/pkg/lint" import ( + "path/filepath" + "k8s.io/helm/pkg/lint/rules" "k8s.io/helm/pkg/lint/support" - "os" - "path/filepath" ) // All runs all of the available linters on the given base directory. -func All(basedir string) []support.Message { +func All(basedir string) support.Linter { // Using abs path to get directory context - current, _ := os.Getwd() - chartDir := filepath.Join(current, basedir) + chartDir, _ := filepath.Abs(basedir) linter := support.Linter{ChartDir: chartDir} rules.Chartfile(&linter) rules.Values(&linter) rules.Templates(&linter) - return linter.Messages + return linter } diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go index c37aa17c2..66541c179 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -17,9 +17,10 @@ limitations under the License. package lint import ( - "k8s.io/helm/pkg/lint/support" "strings" + "k8s.io/helm/pkg/lint/support" + "testing" ) @@ -29,7 +30,7 @@ const badYamlFileDir = "rules/testdata/albatross" const goodChartDir = "rules/testdata/goodone" func TestBadChart(t *testing.T) { - m := All(badChartDir) + m := All(badChartDir).Messages if len(m) != 4 { t.Errorf("Number of errors %v", len(m)) t.Errorf("All didn't fail with expected errors, got %#v", m) @@ -38,18 +39,18 @@ func TestBadChart(t *testing.T) { var w, e, e2, e3 bool for _, msg := range m { if msg.Severity == support.WarningSev { - if strings.Contains(msg.Text, "Templates directory not found") { + if strings.Contains(msg.Err.Error(), "directory not found") { w = true } } if msg.Severity == support.ErrorSev { - if strings.Contains(msg.Text, "'version' 0.0.0 is less than or equal to 0") { + if strings.Contains(msg.Err.Error(), "version 0.0.0 is less than or equal to 0") { e = true } - if strings.Contains(msg.Text, "'name' is required") { + if strings.Contains(msg.Err.Error(), "name is required") { e2 = true } - if strings.Contains(msg.Text, "'name' and directory do not match") { + if strings.Contains(msg.Err.Error(), "directory name (badchartfile) and chart name () must be the same") { e3 = true } } @@ -60,27 +61,27 @@ func TestBadChart(t *testing.T) { } func TestInvalidYaml(t *testing.T) { - m := All(badYamlFileDir) + m := All(badYamlFileDir).Messages if len(m) != 1 { t.Errorf("All didn't fail with expected errors, got %#v", m) } - if !strings.Contains(m[0].Text, "deliberateSyntaxError") { + if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") { t.Errorf("All didn't have the error for deliberateSyntaxError") } } func TestBadValues(t *testing.T) { - m := All(badValuesFileDir) + m := All(badValuesFileDir).Messages if len(m) != 1 { t.Errorf("All didn't fail with expected errors, got %#v", m) } - if !strings.Contains(m[0].Text, "cannot unmarshal") { - t.Errorf("All didn't have the error for invalid key format: %s", m[0].Text) + if !strings.Contains(m[0].Err.Error(), "cannot unmarshal") { + t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err) } } func TestGoodChart(t *testing.T) { - m := All(goodChartDir) + m := All(goodChartDir).Messages if len(m) != 0 { t.Errorf("All failed but shouldn't have: %#v", m) } diff --git a/pkg/lint/rules/chartfile.go b/pkg/lint/rules/chartfile.go index b0333fc27..8763af0de 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/lint/rules/chartfile.go @@ -14,15 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rules +package rules // import "k8s.io/helm/pkg/lint/rules" import ( + "errors" "fmt" "os" "path/filepath" "strings" "github.com/Masterminds/semver" + "github.com/asaskevich/govalidator" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/lint/support" @@ -31,95 +33,83 @@ import ( // Chartfile runs a set of linter rules related to Chart.yaml file func Chartfile(linter *support.Linter) { - chartPath := filepath.Join(linter.ChartDir, "Chart.yaml") + chartFileName := "Chart.yaml" + chartPath := filepath.Join(linter.ChartDir, chartFileName) - linter.RunLinterRule(support.ErrorSev, validateChartYamlFileExistence(chartPath)) - linter.RunLinterRule(support.ErrorSev, validateChartYamlNotDirectory(chartPath)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath)) chartFile, err := chartutil.LoadChartfile(chartPath) - validChartFile := linter.RunLinterRule(support.ErrorSev, validateChartYamlFormat(err)) + validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err)) // Guard clause. Following linter rules require a parseable ChartFile if !validChartFile { return } - linter.RunLinterRule(support.ErrorSev, validateChartName(chartFile)) - linter.RunLinterRule(support.ErrorSev, validateChartNameDirMatch(linter.ChartDir, chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartNameDirMatch(linter.ChartDir, chartFile)) // Chart metadata - linter.RunLinterRule(support.ErrorSev, validateChartVersion(chartFile)) - linter.RunLinterRule(support.ErrorSev, validateChartEngine(chartFile)) - linter.RunLinterRule(support.ErrorSev, validateChartMaintainer(chartFile)) - linter.RunLinterRule(support.ErrorSev, validateChartSources(chartFile)) - linter.RunLinterRule(support.ErrorSev, validateChartHome(chartFile)) -} - -// Auxiliar validation methods -func validateChartYamlFileExistence(chartPath string) (lintError support.LintError) { - _, err := os.Stat(chartPath) - if err != nil { - lintError = fmt.Errorf("Chart.yaml file does not exists") - } - return + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartEngine(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile)) } -func validateChartYamlNotDirectory(chartPath string) (lintError support.LintError) { +func validateChartYamlNotDirectory(chartPath string) error { fi, err := os.Stat(chartPath) if err == nil && fi.IsDir() { - lintError = fmt.Errorf("Chart.yaml is a directory") + return errors.New("should be a file, not a directory") } - return + return nil } -func validateChartYamlFormat(chartFileError error) (lintError support.LintError) { +func validateChartYamlFormat(chartFileError error) error { if chartFileError != nil { - lintError = fmt.Errorf("Chart.yaml is malformed: %s", chartFileError.Error()) + return fmt.Errorf("unable to parse YAML\n\t%s", chartFileError.Error()) } - return + return nil } -func validateChartName(cf *chart.Metadata) (lintError support.LintError) { +func validateChartName(cf *chart.Metadata) error { if cf.Name == "" { - lintError = fmt.Errorf("Chart.yaml: 'name' is required") + return errors.New("name is required") } - return + return nil } -func validateChartNameDirMatch(chartDir string, cf *chart.Metadata) (lintError support.LintError) { +func validateChartNameDirMatch(chartDir string, cf *chart.Metadata) error { if cf.Name != filepath.Base(chartDir) { - lintError = fmt.Errorf("Chart.yaml: 'name' and directory do not match") + return fmt.Errorf("directory name (%s) and chart name (%s) must be the same", filepath.Base(chartDir), cf.Name) } - return + return nil } -func validateChartVersion(cf *chart.Metadata) (lintError support.LintError) { +func validateChartVersion(cf *chart.Metadata) error { if cf.Version == "" { - lintError = fmt.Errorf("Chart.yaml: 'version' value is required") - return + return errors.New("version is required") } version, err := semver.NewVersion(cf.Version) if err != nil { - lintError = fmt.Errorf("Chart.yaml: version '%s' is not a valid SemVer", cf.Version) - return + return fmt.Errorf("version '%s' is not a valid SemVer", cf.Version) } c, err := semver.NewConstraint("> 0") valid, msg := c.Validate(version) if !valid && len(msg) > 0 { - lintError = fmt.Errorf("Chart.yaml: 'version' %v", msg[0]) + return fmt.Errorf("version %v", msg[0]) } - return + return nil } -func validateChartEngine(cf *chart.Metadata) (lintError support.LintError) { +func validateChartEngine(cf *chart.Metadata) error { if cf.Engine == "" { - return + return nil } keys := make([]string, 0, len(chart.Metadata_Engine_value)) @@ -131,39 +121,38 @@ func validateChartEngine(cf *chart.Metadata) (lintError support.LintError) { } if str == cf.Engine { - return + return nil } keys = append(keys, str) } - lintError = fmt.Errorf("Chart.yaml: 'engine %v not valid. Valid options are %v", cf.Engine, keys) - return + return fmt.Errorf("engine '%v' not valid. Valid options are %v", cf.Engine, keys) } -func validateChartMaintainer(cf *chart.Metadata) (lintError support.LintError) { +func validateChartMaintainer(cf *chart.Metadata) error { for _, maintainer := range cf.Maintainers { if maintainer.Name == "" { - lintError = fmt.Errorf("Chart.yaml: maintainer requires a name") + return errors.New("each maintainer requires a name") } else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) { - lintError = fmt.Errorf("Chart.yaml: maintainer invalid email") + return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name) } } - return + return nil } -func validateChartSources(cf *chart.Metadata) (lintError support.LintError) { +func validateChartSources(cf *chart.Metadata) error { for _, source := range cf.Sources { if source == "" || !govalidator.IsRequestURL(source) { - lintError = fmt.Errorf("Chart.yaml: 'source' invalid URL %s", source) + return fmt.Errorf("invalid source URL '%s'", source) } } - return + return nil } -func validateChartHome(cf *chart.Metadata) (lintError support.LintError) { +func validateChartHome(cf *chart.Metadata) error { if cf.Home != "" && !govalidator.IsRequestURL(cf.Home) { - lintError = fmt.Errorf("Chart.yaml: 'home' invalid URL %s", cf.Home) + return fmt.Errorf("invalid home URL '%s'", cf.Home) } - return + return nil } diff --git a/pkg/lint/rules/chartfile_test.go b/pkg/lint/rules/chartfile_test.go index 6d49a55f4..e1295a7cf 100644 --- a/pkg/lint/rules/chartfile_test.go +++ b/pkg/lint/rules/chartfile_test.go @@ -28,29 +28,21 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" ) -const badChartDir = "testdata/badchartfile" -const goodChartDir = "testdata/goodone" +const ( + badChartDir = "testdata/badchartfile" + goodChartDir = "testdata/goodone" +) -var badChartFilePath string = filepath.Join(badChartDir, "Chart.yaml") -var goodChartFilePath string = filepath.Join(goodChartDir, "Chart.yaml") -var nonExistingChartFilePath string = filepath.Join(os.TempDir(), "Chart.yaml") +var ( + badChartFilePath = filepath.Join(badChartDir, "Chart.yaml") + goodChartFilePath = filepath.Join(goodChartDir, "Chart.yaml") + nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml") +) var badChart, chatLoadRrr = chartutil.LoadChartfile(badChartFilePath) var goodChart, _ = chartutil.LoadChartfile(goodChartFilePath) // Validation functions Test -func TestValidateChartYamlFileExistence(t *testing.T) { - err := validateChartYamlFileExistence(nonExistingChartFilePath) - if err == nil { - t.Errorf("validateChartYamlFileExistence to return a linter error, got no error") - } - - err = validateChartYamlFileExistence(badChartFilePath) - if err != nil { - t.Errorf("validateChartYamlFileExistence to return no error, got a linter error") - } -} - func TestValidateChartYamlNotDirectory(t *testing.T) { _ = os.Mkdir(nonExistingChartFilePath, os.ModePerm) defer os.Remove(nonExistingChartFilePath) @@ -103,10 +95,10 @@ func TestValidateChartVersion(t *testing.T) { Version string ErrorMsg string }{ - {"", "'version' value is required"}, + {"", "version is required"}, {"0", "0 is less than or equal to 0"}, - {"waps", "is not a valid SemVer"}, - {"-3", "is not a valid SemVer"}, + {"waps", "'waps' is not a valid SemVer"}, + {"-3", "'-3' is not a valid SemVer"}, } var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"} @@ -152,9 +144,9 @@ func TestValidateChartMaintainer(t *testing.T) { Email string ErrorMsg string }{ - {"", "", "maintainer requires a name"}, - {"", "test@test.com", "maintainer requires a name"}, - {"John Snow", "wrongFormatEmail.com", "maintainer invalid email"}, + {"", "", "each maintainer requires a name"}, + {"", "test@test.com", "each maintainer requires a name"}, + {"John Snow", "wrongFormatEmail.com", "invalid email"}, } var successTest = []struct { @@ -188,8 +180,8 @@ func TestValidateChartSources(t *testing.T) { for _, test := range failTest { badChart.Sources = []string{test} err := validateChartSources(badChart) - if err == nil || !strings.Contains(err.Error(), "invalid URL") { - t.Errorf("validateChartSources(%s) to return \"invalid URL\", got no error", test) + if err == nil || !strings.Contains(err.Error(), "invalid source URL") { + t.Errorf("validateChartSources(%s) to return \"invalid source URL\", got no error", test) } } @@ -209,8 +201,8 @@ func TestValidateChartHome(t *testing.T) { for _, test := range failTest { badChart.Home = test err := validateChartHome(badChart) - if err == nil || !strings.Contains(err.Error(), "invalid URL") { - t.Errorf("validateChartHome(%s) to return \"invalid URL\", got no error", test) + if err == nil || !strings.Contains(err.Error(), "invalid home URL") { + t.Errorf("validateChartHome(%s) to return \"invalid home URL\", got no error", test) } } @@ -232,15 +224,15 @@ func TestChartfile(t *testing.T) { t.Errorf("Expected 3 errors, got %d", len(msgs)) } - if !strings.Contains(msgs[0].Text, "'name' is required") { - t.Errorf("Unexpected message 0: %s", msgs[0].Text) + if !strings.Contains(msgs[0].Err.Error(), "name is required") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) } - if !strings.Contains(msgs[1].Text, "'name' and directory do not match") { - t.Errorf("Unexpected message 1: %s", msgs[1].Text) + if !strings.Contains(msgs[1].Err.Error(), "directory name (badchartfile) and chart name () must be the same") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) } - if !strings.Contains(msgs[2].Text, "'version' 0.0.0 is less than or equal to 0") { - t.Errorf("Unexpected message 2: %s", msgs[2].Text) + if !strings.Contains(msgs[2].Err.Error(), "version 0.0.0 is less than or equal to 0") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) } } diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index 47140eba8..ace4acf8c 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -18,24 +18,28 @@ package rules import ( "bytes" + "errors" "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + "github.com/Masterminds/sprig" "gopkg.in/yaml.v2" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/engine" "k8s.io/helm/pkg/lint/support" "k8s.io/helm/pkg/timeconv" - "os" - "path/filepath" - "regexp" - "strings" - "text/template" ) +// Templates lints the templates in the Linter. func Templates(linter *support.Linter) { - templatesPath := filepath.Join(linter.ChartDir, "templates") + path := "templates/" + templatesPath := filepath.Join(linter.ChartDir, path) - templatesDirExist := linter.RunLinterRule(support.WarningSev, validateTemplatesDir(templatesPath)) + templatesDirExist := linter.RunLinterRule(support.WarningSev, path, validateTemplatesDir(templatesPath)) // Templates directory is optional for now if !templatesDirExist { @@ -45,26 +49,23 @@ func Templates(linter *support.Linter) { // Load chart and parse templates, based on tiller/release_server chart, err := chartutil.Load(linter.ChartDir) - chartLoaded := linter.RunLinterRule(support.ErrorSev, validateNoError(err)) + chartLoaded := linter.RunLinterRule(support.ErrorSev, path, err) if !chartLoaded { return } - // Based on cmd/tiller/release_server.go - overrides := map[string]interface{}{ - "Release": map[string]interface{}{ - "Name": "testRelease", - "Service": "Tiller", - "Time": timeconv.Now(), - }, - "Chart": chart.Metadata, + options := chartutil.ReleaseOptions{Name: "testRelease", Time: timeconv.Now(), Namespace: "testNamespace"} + valuesToRender, err := chartutil.ToRenderValues(chart, chart.Values, options) + if err != nil { + // FIXME: This seems to generate a duplicate, but I can't find where the first + // error is coming from. + //linter.RunLinterRule(support.ErrorSev, err) + return } + renderedContentMap, err := engine.New().Render(chart, valuesToRender) - chartValues, _ := chartutil.CoalesceValues(chart, chart.Values, overrides) - renderedContentMap, err := engine.New().Render(chart, chartValues) - - renderOk := linter.RunLinterRule(support.ErrorSev, validateNoError(err)) + renderOk := linter.RunLinterRule(support.ErrorSev, path, err) if !renderOk { return @@ -79,8 +80,9 @@ func Templates(linter *support.Linter) { */ for _, template := range chart.Templates { fileName, preExecutedTemplate := template.Name, template.Data + path = fileName - linter.RunLinterRule(support.ErrorSev, validateAllowedExtension(fileName)) + linter.RunLinterRule(support.ErrorSev, path, validateAllowedExtension(fileName)) // We only apply the following lint rules to yaml files if filepath.Ext(fileName) != ".yaml" { @@ -88,9 +90,9 @@ func Templates(linter *support.Linter) { } // Check that all the templates have a matching value - linter.RunLinterRule(support.WarningSev, validateNonMissingValues(fileName, templatesPath, chartValues, preExecutedTemplate)) + linter.RunLinterRule(support.WarningSev, path, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) - linter.RunLinterRule(support.WarningSev, validateQuotes(fileName, string(preExecutedTemplate))) + linter.RunLinterRule(support.WarningSev, path, validateQuotes(string(preExecutedTemplate))) renderedContent := renderedContentMap[fileName] var yamlStruct K8sYamlStruct @@ -98,30 +100,30 @@ func Templates(linter *support.Linter) { // key will be raised as well err := yaml.Unmarshal([]byte(renderedContent), &yamlStruct) - validYaml := linter.RunLinterRule(support.ErrorSev, validateYamlContent(fileName, err)) + validYaml := linter.RunLinterRule(support.ErrorSev, path, validateYamlContent(err)) if !validYaml { continue } - linter.RunLinterRule(support.ErrorSev, validateNoNamespace(fileName, yamlStruct)) + linter.RunLinterRule(support.ErrorSev, path, validateNoNamespace(yamlStruct)) } } // Validation functions -func validateTemplatesDir(templatesPath string) (lintError support.LintError) { +func validateTemplatesDir(templatesPath string) error { if fi, err := os.Stat(templatesPath); err != nil { - lintError = fmt.Errorf("Templates directory not found") + return errors.New("directory not found") } else if err == nil && !fi.IsDir() { - lintError = fmt.Errorf("'templates' is not a directory") + return errors.New("not a directory") } - return + return nil } // Validates that go template tags include the quote pipelined function // i.e {{ .Foo.bar }} -> {{ .Foo.bar | quote }} // {{ .Foo.bar }}-{{ .Foo.baz }} -> "{{ .Foo.bar }}-{{ .Foo.baz }}" -func validateQuotes(templateName string, templateContent string) (lintError support.LintError) { +func validateQuotes(templateContent string) error { // {{ .Foo.bar }} r, _ := regexp.Compile(`(?m)(:|-)\s+{{[\w|\.|\s|\']+}}\s*$`) functions := r.FindAllString(templateContent, -1) @@ -129,8 +131,7 @@ func validateQuotes(templateName string, templateContent string) (lintError supp for _, str := range functions { if match, _ := regexp.MatchString("quote", str); !match { result := strings.Replace(str, "}}", " | quote }}", -1) - lintError = fmt.Errorf("templates: \"%s\". Wrap your substitution functions in quotes or use the sprig \"quote\" function: %s -> %s", templateName, str, result) - return + return fmt.Errorf("wrap substitution functions in quotes or use the sprig \"quote\" function: %s -> %s", str, result) } } @@ -140,29 +141,27 @@ func validateQuotes(templateName string, templateContent string) (lintError supp for _, str := range functions { result := strings.Replace(str, str, fmt.Sprintf("\"%s\"", str), -1) - lintError = fmt.Errorf("templates: \"%s\". Wrap your substitution functions in quotes: %s -> %s", templateName, str, result) - return + return fmt.Errorf("wrap substitution functions in quotes: %s -> %s", str, result) } - return + return nil } -func validateAllowedExtension(fileName string) (lintError support.LintError) { +func validateAllowedExtension(fileName string) error { ext := filepath.Ext(fileName) validExtensions := []string{".yaml", ".tpl"} for _, b := range validExtensions { if b == ext { - return + return nil } } - lintError = fmt.Errorf("templates: \"%s\" needs to use .yaml or .tpl extensions", fileName) - return + return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml or .tpl", ext) } -// validateNonMissingValues checks that all the {{}} functions returns a non empty value ( or "") +// validateNoMissingValues checks that all the {{}} functions returns a non empty value ( or "") // and return an error otherwise. -func validateNonMissingValues(fileName string, templatesPath string, chartValues chartutil.Values, templateContent []byte) (lintError support.LintError) { +func validateNoMissingValues(templatesPath string, chartValues chartutil.Values, templateContent []byte) error { // 1 - Load Main and associated templates // Main template that we will parse dynamically tmpl := template.New("tpl").Funcs(sprig.TxtFuncMap()) @@ -189,8 +188,7 @@ func validateNonMissingValues(fileName string, templatesPath string, chartValues for _, str := range functions { newtmpl, err := tmpl.Parse(str) if err != nil { - lintError = fmt.Errorf("templates: %s", err.Error()) - return + return err } err = newtmpl.ExecuteTemplate(&buf, "tpl", chartValues) @@ -208,32 +206,26 @@ func validateNonMissingValues(fileName string, templatesPath string, chartValues } if len(emptyValues) > 0 { - lintError = fmt.Errorf("templates: %s: The following functions are not returning any value %v", fileName, emptyValues) - } - return -} - -func validateNoError(readError error) (lintError support.LintError) { - if readError != nil { - lintError = fmt.Errorf("templates: %s", readError.Error()) + return fmt.Errorf("these substitution functions are returning no value: %v", emptyValues) } - return + return nil } -func validateYamlContent(filePath string, err error) (lintError support.LintError) { +func validateYamlContent(err error) error { if err != nil { - lintError = fmt.Errorf("templates: \"%s\". Wrong YAML content.", filePath) + return fmt.Errorf("unable to parse YAML\n\t%s", err) } - return + return nil } -func validateNoNamespace(filePath string, yamlStruct K8sYamlStruct) (lintError support.LintError) { +func validateNoNamespace(yamlStruct K8sYamlStruct) error { if yamlStruct.Metadata.Namespace != "" { - lintError = fmt.Errorf("templates: \"%s\". namespace option is currently NOT supported.", filePath) + return errors.New("namespace option is currently NOT supported") } - return + return nil } +// K8sYamlStruct stubs a Kubernetes YAML file. // Need to access for now to Namespace only type K8sYamlStruct struct { Metadata struct { diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index 8dc7e6dad..c60152794 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -17,9 +17,12 @@ limitations under the License. package rules import ( - "k8s.io/helm/pkg/lint/support" + "os" + "path/filepath" "strings" "testing" + + "k8s.io/helm/pkg/lint/support" ) const templateTestBasedir = "./testdata/albatross" @@ -28,8 +31,8 @@ func TestValidateAllowedExtension(t *testing.T) { var failTest = []string{"/foo", "/test.yml", "/test.toml", "test.yml"} for _, test := range failTest { err := validateAllowedExtension(test) - if err == nil || !strings.Contains(err.Error(), "needs to use .yaml or .tpl extension") { - t.Errorf("validateAllowedExtension('%s') to return \"needs to use .yaml or .tpl extension\", got no error", test) + if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml or .tpl") { + t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml or .tpl\", got no error", test) } } var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml"} @@ -46,7 +49,7 @@ func TestValidateQuotes(t *testing.T) { var failTest = []string{"foo: {{.Release.Service }}", "foo: {{.Release.Service }}", "- {{.Release.Service }}", "foo: {{default 'Never' .restart_policy}}", "- {{.Release.Service }} "} for _, test := range failTest { - err := validateQuotes("testTemplate.yaml", test) + err := validateQuotes(test) if err == nil || !strings.Contains(err.Error(), "use the sprig \"quote\" function") { t.Errorf("validateQuotes('%s') to return \"use the sprig \"quote\" function:\", got no error.", test) } @@ -55,7 +58,7 @@ func TestValidateQuotes(t *testing.T) { var successTest = []string{"foo: {{.Release.Service | quote }}", "foo: {{.Release.Service | quote }}", "- {{.Release.Service | quote }}", "foo: {{default 'Never' .restart_policy | quote }}", "foo: \"{{ .Release.Service }}\"", "foo: \"{{ .Release.Service }} {{ .Foo.Bar }}\"", "foo: \"{{ default 'Never' .Release.Service }} {{ .Foo.Bar }}\"", "foo: {{.Release.Service | squote }}"} for _, test := range successTest { - err := validateQuotes("testTemplate.yaml", test) + err := validateQuotes(test) if err != nil { t.Errorf("validateQuotes('%s') to return not error and got \"%s\"", test, err.Error()) } @@ -65,15 +68,15 @@ func TestValidateQuotes(t *testing.T) { failTest = []string{"foo: {{.Release.Service }}-{{ .Release.Bar }}", "foo: {{.Release.Service }} {{ .Release.Bar }}", "- {{.Release.Service }}-{{ .Release.Bar }}", "- {{.Release.Service }}-{{ .Release.Bar }} {{ .Release.Baz }}", "foo: {{.Release.Service | default }}-{{ .Release.Bar }}"} for _, test := range failTest { - err := validateQuotes("testTemplate.yaml", test) - if err == nil || !strings.Contains(err.Error(), "Wrap your substitution functions in quotes") { - t.Errorf("validateQuotes('%s') to return \"Wrap your substitution functions in quotes\", got no error", test) + err := validateQuotes(test) + if err == nil || !strings.Contains(err.Error(), "wrap substitution functions in quotes") { + t.Errorf("validateQuotes('%s') to return \"wrap substitution functions in quotes\", got no error", test) } } } -func TestTemplate(t *testing.T) { +func TestTemplateParsing(t *testing.T) { linter := support.Linter{ChartDir: templateTestBasedir} Templates(&linter) res := linter.Messages @@ -82,7 +85,26 @@ func TestTemplate(t *testing.T) { t.Fatalf("Expected one error, got %d, %v", len(res), res) } - if !strings.Contains(res[0].Text, "deliberateSyntaxError") { + if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") { t.Errorf("Unexpected error: %s", res[0]) } } + +var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml") +var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored") + +// Test a template with all the existing features: +// namespaces, partial templates +func TestTemplateIntegrationHappyPath(t *testing.T) { + // Rename file so it gets ignored by the linter + os.Rename(wrongTemplatePath, ignoredTemplatePath) + defer os.Rename(ignoredTemplatePath, wrongTemplatePath) + + linter := support.Linter{ChartDir: templateTestBasedir} + Templates(&linter) + res := linter.Messages + + if len(res) != 0 { + t.Fatalf("Expected no error, got %d, %v", len(res), res) + } +} diff --git a/pkg/lint/rules/testdata/albatross/templates/_helpers.tpl b/pkg/lint/rules/testdata/albatross/templates/_helpers.tpl new file mode 100644 index 000000000..200aee93a --- /dev/null +++ b/pkg/lint/rules/testdata/albatross/templates/_helpers.tpl @@ -0,0 +1,16 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{define "name"}}{{default "nginx" .Values.nameOverride | trunc 24 }}{{end}} + +{{/* +Create a default fully qualified app name. + +We truncate at 24 chars because some Kubernetes name fields are limited to this +(by the DNS naming spec). +*/}} +{{define "fullname"}} +{{- $name := default "nginx" .Values.nameOverride -}} +{{printf "%s-%s" .Release.Name $name | trunc 24 -}} +{{end}} diff --git a/pkg/lint/rules/testdata/albatross/templates/albatross.yaml b/pkg/lint/rules/testdata/albatross/templates/albatross.yaml deleted file mode 100644 index 6c2ceb8db..000000000 --- a/pkg/lint/rules/testdata/albatross/templates/albatross.yaml +++ /dev/null @@ -1,2 +0,0 @@ -metadata: - name: {{.name | default "foo" | title}} diff --git a/pkg/lint/rules/testdata/albatross/templates/svc.yaml b/pkg/lint/rules/testdata/albatross/templates/svc.yaml new file mode 100644 index 000000000..2c44ea2c6 --- /dev/null +++ b/pkg/lint/rules/testdata/albatross/templates/svc.yaml @@ -0,0 +1,18 @@ +# This is a service gateway to the replica set created by the deployment. +# Take a look at the deployment.yaml for general notes about this chart. +apiVersion: v1 +kind: Service +metadata: + name: "{{ .Values.name }}" + labels: + heritage: {{ .Release.Service | quote }} + release: {{ .Release.Name | quote }} + chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + ports: + - port: {{default 80 .Values.httpPort | quote}} + targetPort: 80 + protocol: TCP + name: http + selector: + app: {{template "fullname" .}} diff --git a/pkg/lint/rules/values.go b/pkg/lint/rules/values.go index 6d1d7af7f..9b97598f0 100644 --- a/pkg/lint/rules/values.go +++ b/pkg/lint/rules/values.go @@ -18,36 +18,38 @@ package rules import ( "fmt" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/lint/support" "os" "path/filepath" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/lint/support" ) // Values lints a chart's values.yaml file. func Values(linter *support.Linter) { - vf := filepath.Join(linter.ChartDir, "values.yaml") - fileExists := linter.RunLinterRule(support.InfoSev, validateValuesFileExistence(linter, vf)) + file := "values.yaml" + vf := filepath.Join(linter.ChartDir, file) + fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(linter, vf)) if !fileExists { return } - linter.RunLinterRule(support.ErrorSev, validateValuesFile(linter, vf)) + linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(linter, vf)) } -func validateValuesFileExistence(linter *support.Linter, valuesPath string) (lintError support.LintError) { +func validateValuesFileExistence(linter *support.Linter, valuesPath string) error { _, err := os.Stat(valuesPath) if err != nil { - lintError = fmt.Errorf("values.yaml file does not exists") + return fmt.Errorf("file does not exist") } - return + return nil } -func validateValuesFile(linter *support.Linter, valuesPath string) (lintError support.LintError) { +func validateValuesFile(linter *support.Linter, valuesPath string) error { _, err := chartutil.ReadValuesFile(valuesPath) if err != nil { - lintError = fmt.Errorf("values.yaml is malformed: %s", err.Error()) + return fmt.Errorf("unable to parse YAML\n\t%s", err) } - return + return nil } diff --git a/pkg/lint/support/doc.go b/pkg/lint/support/doc.go index a71f0f75d..4cf7272e4 100644 --- a/pkg/lint/support/doc.go +++ b/pkg/lint/support/doc.go @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package lint contains tools for linting charts. +/*Package support contains tools for linting charts. Linting is the process of testing charts for errors or warnings regarding formatting, compilation, or standards compliance. */ -package support +package support // import "k8s.io/helm/pkg/lint/support" diff --git a/pkg/lint/support/message.go b/pkg/lint/support/message.go index 1df57458e..6a878031a 100644 --- a/pkg/lint/support/message.go +++ b/pkg/lint/support/message.go @@ -33,39 +33,44 @@ const ( // sev matches the *Sev states. var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"} -// Message is a linting output message +// Linter encapsulates a linting run of a particular chart. +type Linter struct { + Messages []Message + // The highest severity of all the failing lint rules + HighestSeverity int + ChartDir string +} + +// Message describes an error encountered while linting. type Message struct { // Severity is one of the *Sev constants Severity int - // Text contains the message text - Text string + Path string + Err error } -type Linter struct { - Messages []Message - ChartDir string -} - -type LintError interface { - error +func (m Message) Error() string { + return fmt.Sprintf("[%s] %s: %s", sev[m.Severity], m.Path, m.Err.Error()) } -// String prints a string representation of this Message. -// -// Implements fmt.Stringer. -func (m Message) String() string { - return fmt.Sprintf("[%s] %s", sev[m.Severity], m.Text) +// NewMessage creates a new Message struct +func NewMessage(severity int, path string, err error) Message { + return Message{Severity: severity, Path: path, Err: err} } -// Returns true if the validation passed -func (l *Linter) RunLinterRule(severity int, lintError LintError) bool { +// RunLinterRule returns true if the validation passed +func (l *Linter) RunLinterRule(severity int, path string, err error) bool { // severity is out of bound if severity < 0 || severity >= len(sev) { return false } - if lintError != nil { - l.Messages = append(l.Messages, Message{Text: lintError.Error(), Severity: severity}) + if err != nil { + l.Messages = append(l.Messages, NewMessage(severity, path, err)) + + if severity > l.HighestSeverity { + l.HighestSeverity = severity + } } - return lintError == nil + return err == nil } diff --git a/pkg/lint/support/message_test.go b/pkg/lint/support/message_test.go index 9a18e01ed..4a9c33c34 100644 --- a/pkg/lint/support/message_test.go +++ b/pkg/lint/support/message_test.go @@ -17,56 +17,63 @@ limitations under the License. package support import ( - "fmt" + "errors" "testing" ) -var linter Linter = Linter{} -var lintError LintError = fmt.Errorf("Foobar") +var linter = Linter{} +var errLint = errors.New("lint failed") func TestRunLinterRule(t *testing.T) { var tests = []struct { - Severity int - LintError error - ExpectedMessages int - ExpectedReturn bool + Severity int + LintError error + ExpectedMessages int + ExpectedReturn bool + ExpectedHighestSeverity int }{ - {ErrorSev, lintError, 1, false}, - {WarningSev, lintError, 2, false}, - {InfoSev, lintError, 3, false}, + {InfoSev, errLint, 1, false, InfoSev}, + {WarningSev, errLint, 2, false, WarningSev}, + {ErrorSev, errLint, 3, false, ErrorSev}, // No error so it returns true - {ErrorSev, nil, 3, true}, + {ErrorSev, nil, 3, true, ErrorSev}, + // Retains highest severity + {InfoSev, errLint, 4, false, ErrorSev}, // Invalid severity values - {4, lintError, 3, false}, - {22, lintError, 3, false}, - {-1, lintError, 3, false}, + {4, errLint, 4, false, ErrorSev}, + {22, errLint, 4, false, ErrorSev}, + {-1, errLint, 4, false, ErrorSev}, } for _, test := range tests { - isValid := linter.RunLinterRule(test.Severity, test.LintError) + isValid := linter.RunLinterRule(test.Severity, "chart", test.LintError) if len(linter.Messages) != test.ExpectedMessages { - t.Errorf("RunLinterRule(%d, %v), linter.Messages should have now %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages)) + t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.Messages should now have %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages)) + } + + if linter.HighestSeverity != test.ExpectedHighestSeverity { + t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity) } if isValid != test.ExpectedReturn { - t.Errorf("RunLinterRule(%d, %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid) + t.Errorf("RunLinterRule(%d, \"chart\", %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid) } } } func TestMessage(t *testing.T) { - m := Message{ErrorSev, "Foo"} - if m.String() != "[ERROR] Foo" { - t.Errorf("Unexpected output: %s", m.String()) + m := Message{ErrorSev, "Chart.yaml", errors.New("Foo")} + if m.Error() != "[ERROR] Chart.yaml: Foo" { + t.Errorf("Unexpected output: %s", m.Error()) } - m = Message{WarningSev, "Bar"} - if m.String() != "[WARNING] Bar" { - t.Errorf("Unexpected output: %s", m.String()) + m = Message{WarningSev, "templates/", errors.New("Bar")} + if m.Error() != "[WARNING] templates/: Bar" { + t.Errorf("Unexpected output: %s", m.Error()) } - m = Message{InfoSev, "FooBar"} - if m.String() != "[INFO] FooBar" { - t.Errorf("Unexpected output: %s", m.String()) + m = Message{InfoSev, "templates/rc.yaml", errors.New("FooBar")} + if m.Error() != "[INFO] templates/rc.yaml: FooBar" { + t.Errorf("Unexpected output: %s", m.Error()) } } diff --git a/pkg/proto/hapi/release/hook.pb.go b/pkg/proto/hapi/release/hook.pb.go new file mode 100644 index 000000000..8694579a7 --- /dev/null +++ b/pkg/proto/hapi/release/hook.pb.go @@ -0,0 +1,125 @@ +// Code generated by protoc-gen-go. +// source: hapi/release/hook.proto +// DO NOT EDIT! + +/* +Package release is a generated protocol buffer package. + +It is generated from these files: + hapi/release/hook.proto + hapi/release/info.proto + hapi/release/release.proto + hapi/release/status.proto + +It has these top-level messages: + Hook + Info + Release + Status +*/ +package release + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" +import google_protobuf "github.com/golang/protobuf/ptypes/timestamp" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +const _ = proto.ProtoPackageIsVersion1 + +type Hook_Event int32 + +const ( + Hook_UNKNOWN Hook_Event = 0 + Hook_PRE_INSTALL Hook_Event = 1 + Hook_POST_INSTALL Hook_Event = 2 + Hook_PRE_DELETE Hook_Event = 3 + Hook_POST_DELETE Hook_Event = 4 + Hook_PRE_UPGRADE Hook_Event = 5 + Hook_POST_UPGRADE Hook_Event = 6 +) + +var Hook_Event_name = map[int32]string{ + 0: "UNKNOWN", + 1: "PRE_INSTALL", + 2: "POST_INSTALL", + 3: "PRE_DELETE", + 4: "POST_DELETE", + 5: "PRE_UPGRADE", + 6: "POST_UPGRADE", +} +var Hook_Event_value = map[string]int32{ + "UNKNOWN": 0, + "PRE_INSTALL": 1, + "POST_INSTALL": 2, + "PRE_DELETE": 3, + "POST_DELETE": 4, + "PRE_UPGRADE": 5, + "POST_UPGRADE": 6, +} + +func (x Hook_Event) String() string { + return proto.EnumName(Hook_Event_name, int32(x)) +} +func (Hook_Event) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0, 0} } + +// Hook defines a hook object. +type Hook struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // Kind is the Kubernetes kind. + Kind string `protobuf:"bytes,2,opt,name=kind" json:"kind,omitempty"` + // Path is the chart-relative path to the template. + Path string `protobuf:"bytes,3,opt,name=path" json:"path,omitempty"` + // Manifest is the manifest contents. + Manifest string `protobuf:"bytes,4,opt,name=manifest" json:"manifest,omitempty"` + // Events are the events that this hook fires on. + Events []Hook_Event `protobuf:"varint,5,rep,name=events,enum=hapi.release.Hook_Event" json:"events,omitempty"` + // LastRun indicates the date/time this was last run. + LastRun *google_protobuf.Timestamp `protobuf:"bytes,6,opt,name=last_run,json=lastRun" json:"last_run,omitempty"` +} + +func (m *Hook) Reset() { *m = Hook{} } +func (m *Hook) String() string { return proto.CompactTextString(m) } +func (*Hook) ProtoMessage() {} +func (*Hook) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *Hook) GetLastRun() *google_protobuf.Timestamp { + if m != nil { + return m.LastRun + } + return nil +} + +func init() { + proto.RegisterType((*Hook)(nil), "hapi.release.Hook") + proto.RegisterEnum("hapi.release.Hook_Event", Hook_Event_name, Hook_Event_value) +} + +var fileDescriptor0 = []byte{ + // 300 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x5f, 0x4f, 0xc2, 0x30, + 0x14, 0xc5, 0x05, 0xc6, 0x80, 0x0b, 0xc1, 0xa5, 0x2f, 0x36, 0xbc, 0x48, 0x78, 0xe2, 0xa9, 0x33, + 0x18, 0x3f, 0x00, 0x86, 0x46, 0x8d, 0x64, 0x90, 0x32, 0x62, 0xe2, 0x0b, 0x29, 0xb1, 0xc0, 0x02, + 0x6b, 0x17, 0xd6, 0xf9, 0xe2, 0xd7, 0xf3, 0x83, 0xb9, 0x76, 0x7f, 0xe2, 0xdb, 0xed, 0xef, 0x9c, + 0x7b, 0x7a, 0x0f, 0xdc, 0x9d, 0x78, 0x12, 0xf9, 0x57, 0x71, 0x11, 0x3c, 0x15, 0xfe, 0x49, 0xa9, + 0x33, 0x49, 0xae, 0x4a, 0x2b, 0x34, 0x30, 0x02, 0x29, 0x85, 0xd1, 0xfd, 0x51, 0xa9, 0xe3, 0x45, + 0xf8, 0x56, 0xdb, 0x67, 0x07, 0x5f, 0x47, 0xb1, 0x48, 0x35, 0x8f, 0x93, 0xc2, 0x3e, 0xf9, 0x6d, + 0x82, 0xf3, 0x9a, 0x6f, 0x23, 0x04, 0x8e, 0xe4, 0xb1, 0xc0, 0x8d, 0x71, 0x63, 0xda, 0x63, 0x76, + 0x36, 0xec, 0x1c, 0xc9, 0x2f, 0xdc, 0x2c, 0x98, 0x99, 0x0d, 0x4b, 0xb8, 0x3e, 0xe1, 0x56, 0xc1, + 0xcc, 0x8c, 0x46, 0xd0, 0x8d, 0xb9, 0x8c, 0x0e, 0x79, 0x32, 0x76, 0x2c, 0xaf, 0xdf, 0xe8, 0x01, + 0x5c, 0xf1, 0x2d, 0xa4, 0x4e, 0x71, 0x7b, 0xdc, 0x9a, 0x0e, 0x67, 0x98, 0xfc, 0x3f, 0x90, 0x98, + 0xbf, 0x09, 0x35, 0x06, 0x56, 0xfa, 0xd0, 0x13, 0x74, 0x2f, 0x3c, 0xd5, 0xbb, 0x6b, 0x26, 0xb1, + 0x9b, 0xa7, 0xf5, 0x67, 0x23, 0x52, 0xd4, 0x20, 0x55, 0x0d, 0x12, 0x56, 0x35, 0x58, 0xc7, 0x78, + 0x59, 0x26, 0x27, 0x3f, 0xd0, 0xb6, 0x39, 0xa8, 0x0f, 0x9d, 0x6d, 0xf0, 0x1e, 0xac, 0x3e, 0x02, + 0xef, 0x06, 0xdd, 0x42, 0x7f, 0xcd, 0xe8, 0xee, 0x2d, 0xd8, 0x84, 0xf3, 0xe5, 0xd2, 0x6b, 0x20, + 0x0f, 0x06, 0xeb, 0xd5, 0x26, 0xac, 0x49, 0x13, 0x0d, 0x01, 0x8c, 0x65, 0x41, 0x97, 0x34, 0xa4, + 0x5e, 0xcb, 0xae, 0x18, 0x47, 0x09, 0x9c, 0x2a, 0x63, 0xbb, 0x7e, 0x61, 0xf3, 0x05, 0xf5, 0xda, + 0x75, 0x46, 0x45, 0xdc, 0xe7, 0xde, 0x67, 0xa7, 0x6c, 0xb4, 0x77, 0xed, 0x91, 0x8f, 0x7f, 0x01, + 0x00, 0x00, 0xff, 0xff, 0x16, 0x64, 0x61, 0x76, 0xa2, 0x01, 0x00, 0x00, +} diff --git a/pkg/proto/hapi/release/info.pb.go b/pkg/proto/hapi/release/info.pb.go index 9812997f4..c54578569 100644 --- a/pkg/proto/hapi/release/info.pb.go +++ b/pkg/proto/hapi/release/info.pb.go @@ -2,19 +2,6 @@ // source: hapi/release/info.proto // DO NOT EDIT! -/* -Package release is a generated protocol buffer package. - -It is generated from these files: - hapi/release/info.proto - hapi/release/release.proto - hapi/release/status.proto - -It has these top-level messages: - Info - Release - Status -*/ package release import proto "github.com/golang/protobuf/proto" @@ -27,10 +14,6 @@ var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -const _ = proto.ProtoPackageIsVersion1 - // Info describes release information. type Info struct { Status *Status `protobuf:"bytes,1,opt,name=status" json:"status,omitempty"` @@ -43,7 +26,7 @@ type Info struct { func (m *Info) Reset() { *m = Info{} } func (m *Info) String() string { return proto.CompactTextString(m) } func (*Info) ProtoMessage() {} -func (*Info) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } +func (*Info) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} } func (m *Info) GetStatus() *Status { if m != nil { @@ -77,7 +60,7 @@ func init() { proto.RegisterType((*Info)(nil), "hapi.release.Info") } -var fileDescriptor0 = []byte{ +var fileDescriptor1 = []byte{ // 208 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0xcf, 0xcc, 0x4b, 0xcb, 0xd7, 0x2b, 0x28, diff --git a/pkg/proto/hapi/release/release.pb.go b/pkg/proto/hapi/release/release.pb.go index 587a446e5..5193d1d93 100644 --- a/pkg/proto/hapi/release/release.pb.go +++ b/pkg/proto/hapi/release/release.pb.go @@ -29,12 +29,16 @@ type Release struct { Config *hapi_chart.Config `protobuf:"bytes,4,opt,name=config" json:"config,omitempty"` // Manifest is the string representation of the rendered template. Manifest string `protobuf:"bytes,5,opt,name=manifest" json:"manifest,omitempty"` + // Hooks are all of the hooks declared for this release. + Hooks []*Hook `protobuf:"bytes,6,rep,name=hooks" json:"hooks,omitempty"` + // Version is an int32 which represents the version of the release. + Version int32 `protobuf:"varint,7,opt,name=version" json:"version,omitempty"` } func (m *Release) Reset() { *m = Release{} } func (m *Release) String() string { return proto.CompactTextString(m) } func (*Release) ProtoMessage() {} -func (*Release) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} } +func (*Release) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} } func (m *Release) GetInfo() *Info { if m != nil { @@ -57,23 +61,32 @@ func (m *Release) GetConfig() *hapi_chart.Config { return nil } +func (m *Release) GetHooks() []*Hook { + if m != nil { + return m.Hooks + } + return nil +} + func init() { proto.RegisterType((*Release)(nil), "hapi.release.Release") } -var fileDescriptor1 = []byte{ - // 200 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x54, 0x8f, 0xbd, 0x0e, 0x82, 0x30, - 0x14, 0x85, 0x83, 0xf2, 0x23, 0xd5, 0xc5, 0x3b, 0x68, 0xc3, 0x64, 0x1c, 0xd4, 0x38, 0x94, 0x44, - 0xdf, 0x40, 0x27, 0xd7, 0x8e, 0x6e, 0x95, 0x14, 0x69, 0x22, 0x2d, 0x01, 0x9e, 0xcd, 0xe7, 0x93, - 0xf6, 0x56, 0x83, 0xcb, 0x85, 0xde, 0xef, 0xcb, 0xe9, 0x29, 0xc9, 0x2a, 0xd1, 0xa8, 0xbc, 0x95, - 0x2f, 0x29, 0x3a, 0xf9, 0xfd, 0xb2, 0xa6, 0x35, 0xbd, 0x81, 0x85, 0x65, 0xcc, 0xef, 0xb2, 0xf5, - 0x9f, 0xa9, 0x74, 0x69, 0x50, 0xf3, 0xa0, 0xa8, 0x44, 0xdb, 0xe7, 0x85, 0xd1, 0xa5, 0x7a, 0x7a, - 0xb0, 0x1a, 0x03, 0x3b, 0x71, 0xbf, 0x7d, 0x07, 0x24, 0xe1, 0x98, 0x03, 0x40, 0x42, 0x2d, 0x6a, - 0x49, 0x83, 0x4d, 0x70, 0x48, 0xb9, 0xfb, 0x87, 0x1d, 0x09, 0x6d, 0x3c, 0x9d, 0x0c, 0xbb, 0xf9, - 0x09, 0xd8, 0xb8, 0x06, 0xbb, 0x0d, 0x84, 0x3b, 0x0e, 0x7b, 0x12, 0xb9, 0x58, 0x3a, 0x75, 0xe2, - 0x12, 0x45, 0xbc, 0xe9, 0x6a, 0x27, 0x47, 0x0e, 0x47, 0x12, 0x63, 0x31, 0x1a, 0x8e, 0x23, 0xbd, - 0xe9, 0x08, 0xf7, 0x06, 0x64, 0x64, 0x56, 0x0b, 0xad, 0x4a, 0xd9, 0xf5, 0x34, 0x72, 0xa5, 0x7e, - 0xe7, 0x4b, 0x7a, 0x4f, 0x7c, 0x8d, 0x47, 0xec, 0x9e, 0x72, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, - 0xd4, 0xf3, 0x60, 0x0b, 0x40, 0x01, 0x00, 0x00, +var fileDescriptor2 = []byte{ + // 239 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0x3f, 0x4f, 0x03, 0x31, + 0x0c, 0xc5, 0x75, 0xf4, 0xfe, 0x50, 0xc3, 0x82, 0x07, 0xb0, 0x6e, 0xaa, 0x18, 0xa0, 0x62, 0x48, + 0x25, 0xf8, 0x06, 0xb0, 0xc0, 0x9a, 0x91, 0x2d, 0x54, 0x39, 0x2e, 0x82, 0x26, 0x55, 0x72, 0xe2, + 0xc3, 0x33, 0x91, 0xc4, 0x29, 0xba, 0xc2, 0xe2, 0xc4, 0xfe, 0xbd, 0xbc, 0x3c, 0x19, 0xfa, 0x51, + 0xed, 0xcd, 0xc6, 0xeb, 0x4f, 0xad, 0x82, 0x3e, 0x9c, 0x62, 0xef, 0xdd, 0xe4, 0xf0, 0x3c, 0x31, + 0x51, 0x66, 0xfd, 0xd5, 0x91, 0x72, 0x74, 0xee, 0x83, 0x65, 0x7f, 0x80, 0xb1, 0x83, 0x3b, 0x02, + 0xdb, 0x51, 0xf9, 0x69, 0xb3, 0x75, 0x76, 0x30, 0xef, 0x05, 0x5c, 0xce, 0x41, 0xaa, 0x3c, 0xbf, + 0xfe, 0xae, 0xa0, 0x93, 0xec, 0x83, 0x08, 0xb5, 0x55, 0x3b, 0x4d, 0xd5, 0xaa, 0x5a, 0x2f, 0x65, + 0xbe, 0xe3, 0x0d, 0xd4, 0xc9, 0x9e, 0x4e, 0xe2, 0xec, 0xec, 0x1e, 0xc5, 0x3c, 0x9f, 0x78, 0x89, + 0x44, 0x66, 0x8e, 0xb7, 0xd0, 0x64, 0x5b, 0x5a, 0x64, 0xe1, 0x05, 0x0b, 0xf9, 0xa7, 0xa7, 0x54, + 0x25, 0x73, 0xbc, 0x83, 0x96, 0x83, 0x51, 0x3d, 0xb7, 0x2c, 0xca, 0x4c, 0x64, 0x51, 0x60, 0x0f, + 0xa7, 0x3b, 0x65, 0xcd, 0xa0, 0xc3, 0x44, 0x4d, 0x0e, 0xf5, 0xdb, 0xe3, 0x1a, 0x9a, 0xb4, 0x90, + 0x40, 0xed, 0x6a, 0xf1, 0x3f, 0xd9, 0x73, 0x44, 0x92, 0x05, 0x48, 0xd0, 0x7d, 0x69, 0x1f, 0x8c, + 0xb3, 0xd4, 0x45, 0x93, 0x46, 0x1e, 0xda, 0xc7, 0xe5, 0x6b, 0x57, 0x1e, 0xbc, 0xb5, 0x79, 0x1d, + 0x0f, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x49, 0x8d, 0x14, 0x1a, 0x9d, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/status.pb.go b/pkg/proto/hapi/release/status.pb.go index d64e4a0ae..1c7f161ef 100644 --- a/pkg/proto/hapi/release/status.pb.go +++ b/pkg/proto/hapi/release/status.pb.go @@ -47,7 +47,7 @@ var Status_Code_value = map[string]int32{ func (x Status_Code) String() string { return proto.EnumName(Status_Code_name, int32(x)) } -func (Status_Code) EnumDescriptor() ([]byte, []int) { return fileDescriptor2, []int{0, 0} } +func (Status_Code) EnumDescriptor() ([]byte, []int) { return fileDescriptor3, []int{0, 0} } // Status defines the status of a release. type Status struct { @@ -58,7 +58,7 @@ type Status struct { func (m *Status) Reset() { *m = Status{} } func (m *Status) String() string { return proto.CompactTextString(m) } func (*Status) ProtoMessage() {} -func (*Status) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} } +func (*Status) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} } func (m *Status) GetDetails() *google_protobuf1.Any { if m != nil { @@ -72,7 +72,7 @@ func init() { proto.RegisterEnum("hapi.release.Status_Code", Status_Code_name, Status_Code_value) } -var fileDescriptor2 = []byte{ +var fileDescriptor3 = []byte{ // 226 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0x2f, 0x2e, 0x49, 0x2c, 0x29, 0x2d, 0xd6, diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 40a18d7f4..906e0910b 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -30,8 +30,8 @@ import fmt "fmt" import math "math" import hapi_chart3 "k8s.io/helm/pkg/proto/hapi/chart" import hapi_chart "k8s.io/helm/pkg/proto/hapi/chart" +import hapi_release3 "k8s.io/helm/pkg/proto/hapi/release" import hapi_release2 "k8s.io/helm/pkg/proto/hapi/release" -import hapi_release1 "k8s.io/helm/pkg/proto/hapi/release" import ( context "golang.org/x/net/context" @@ -141,7 +141,7 @@ type ListReleasesResponse struct { // Total is the total number of queryable releases. Total int64 `protobuf:"varint,3,opt,name=total" json:"total,omitempty"` // Releases is the list of found release objects. - Releases []*hapi_release2.Release `protobuf:"bytes,4,rep,name=releases" json:"releases,omitempty"` + Releases []*hapi_release3.Release `protobuf:"bytes,4,rep,name=releases" json:"releases,omitempty"` } func (m *ListReleasesResponse) Reset() { *m = ListReleasesResponse{} } @@ -149,7 +149,7 @@ func (m *ListReleasesResponse) String() string { return proto.Compact func (*ListReleasesResponse) ProtoMessage() {} func (*ListReleasesResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } -func (m *ListReleasesResponse) GetReleases() []*hapi_release2.Release { +func (m *ListReleasesResponse) GetReleases() []*hapi_release3.Release { if m != nil { return m.Releases } @@ -172,7 +172,7 @@ type GetReleaseStatusResponse struct { // Name is the name of the release. Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` // Info contains information about the release. - Info *hapi_release1.Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` + Info *hapi_release2.Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` } func (m *GetReleaseStatusResponse) Reset() { *m = GetReleaseStatusResponse{} } @@ -180,7 +180,7 @@ func (m *GetReleaseStatusResponse) String() string { return proto.Com func (*GetReleaseStatusResponse) ProtoMessage() {} func (*GetReleaseStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } -func (m *GetReleaseStatusResponse) GetInfo() *hapi_release1.Info { +func (m *GetReleaseStatusResponse) GetInfo() *hapi_release2.Info { if m != nil { return m.Info } @@ -201,7 +201,7 @@ func (*GetReleaseContentRequest) Descriptor() ([]byte, []int) { return fileDescr // GetReleaseContentResponse is a response containing the contents of a release. type GetReleaseContentResponse struct { // The release content - Release *hapi_release2.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` + Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` } func (m *GetReleaseContentResponse) Reset() { *m = GetReleaseContentResponse{} } @@ -209,7 +209,7 @@ func (m *GetReleaseContentResponse) String() string { return proto.Co func (*GetReleaseContentResponse) ProtoMessage() {} func (*GetReleaseContentResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } -func (m *GetReleaseContentResponse) GetRelease() *hapi_release2.Release { +func (m *GetReleaseContentResponse) GetRelease() *hapi_release3.Release { if m != nil { return m.Release } @@ -271,7 +271,7 @@ func (m *InstallReleaseRequest) GetValues() *hapi_chart.Config { // InstallReleaseResponse is the response from a release installation. type InstallReleaseResponse struct { - Release *hapi_release2.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` + Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` } func (m *InstallReleaseResponse) Reset() { *m = InstallReleaseResponse{} } @@ -279,7 +279,7 @@ func (m *InstallReleaseResponse) String() string { return proto.Compa func (*InstallReleaseResponse) ProtoMessage() {} func (*InstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} } -func (m *InstallReleaseResponse) GetRelease() *hapi_release2.Release { +func (m *InstallReleaseResponse) GetRelease() *hapi_release3.Release { if m != nil { return m.Release } @@ -300,7 +300,7 @@ func (*UninstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescri // UninstallReleaseResponse represents a successful response to an uninstall request. type UninstallReleaseResponse struct { // Release is the release that was marked deleted. - Release *hapi_release2.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` + Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` } func (m *UninstallReleaseResponse) Reset() { *m = UninstallReleaseResponse{} } @@ -308,7 +308,7 @@ func (m *UninstallReleaseResponse) String() string { return proto.Com func (*UninstallReleaseResponse) ProtoMessage() {} func (*UninstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} } -func (m *UninstallReleaseResponse) GetRelease() *hapi_release2.Release { +func (m *UninstallReleaseResponse) GetRelease() *hapi_release3.Release { if m != nil { return m.Release } diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index b9f8fda44..ebc79f2c4 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package repo +package repo // import "k8s.io/helm/pkg/repo" import ( "crypto/sha1" @@ -118,6 +118,7 @@ func (r *ChartRepository) saveIndexFile() error { return ioutil.WriteFile(filepath.Join(r.RootPath, indexPath), index, 0644) } +// Index generates an index for the chart repository and writes an index.yaml file func (r *ChartRepository) Index() error { if r.IndexFile == nil { r.IndexFile = &IndexFile{Entries: make(map[string]*ChartRef)} diff --git a/pkg/storage/doc.go b/pkg/storage/doc.go index 9c191381b..9a4027923 100644 --- a/pkg/storage/doc.go +++ b/pkg/storage/doc.go @@ -20,4 +20,4 @@ Tiller stores releases (see 'cmd/tiller/environment'.Environment). The backend storage mechanism may be implemented with different backends. This package and its subpackages provide storage layers for Tiller objects. */ -package storage +package storage // import "k8s.io/helm/pkg/storage" diff --git a/pkg/timeconv/doc.go b/pkg/timeconv/doc.go index 8e022bd00..235167391 100644 --- a/pkg/timeconv/doc.go +++ b/pkg/timeconv/doc.go @@ -20,4 +20,4 @@ The gRPC/Protobuf libraries contain time implementations that require conversion to and from Go times. This library provides utilities and convenience functions for performing conversions. */ -package timeconv +package timeconv // import "k8s.io/helm/pkg/timeconv" diff --git a/pkg/version/version.go b/pkg/version/version.go index ceffa63d1..128b9a468 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -15,7 +15,7 @@ limitations under the License. */ // Package version represents the current version of the project. -package version +package version // import "k8s.io/helm/pkg/version" // Version is the current version of the Helm. // Update this whenever making a new release. diff --git a/scripts/ci.sh b/scripts/ci.sh new file mode 100755 index 000000000..04aee0b51 --- /dev/null +++ b/scripts/ci.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# 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. + +# Bash 'Strict Mode' +# http://redsymbol.net/articles/unofficial-bash-strict-mode +set -euo pipefail +IFS=$'\n\t' + +HELM_ROOT="${BASH_SOURCE[0]%/*}/.." +cd "$HELM_ROOT" + +run_unit_test() { + if [[ "${CIRCLE_BRANCH-}" == "master" ]]; then + echo "Running unit tests with coverage'" + ./scripts/coverage.sh --coveralls + else + echo "Running unit tests'" + make test-unit + fi +} + +run_style_check() { + echo "Running 'make test-style'" + make test-style +} + +case "${CIRCLE_NODE_INDEX-0}" in + 0) run_unit_test ;; + 1) run_style_check ;; +esac diff --git a/scripts/cluster/kube-system.yaml b/scripts/cluster/kube-system.yaml deleted file mode 100644 index 986f4b482..000000000 --- a/scripts/cluster/kube-system.yaml +++ /dev/null @@ -1,4 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: kube-system diff --git a/scripts/coverage.sh b/scripts/coverage.sh index ccb2d52fc..4ee5ab077 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -16,22 +16,36 @@ set -euo pipefail -COVERDIR=${COVERDIR:-.coverage} -COVERMODE=${COVERMODE:-atomic} -PACKAGES=($(go list $(glide novendor))) +covermode=${COVERMODE:-atomic} +coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX) +profile="${coverdir}/cover.out" -if [[ ! -d "$COVERDIR" ]]; then - mkdir -p "$COVERDIR" -fi +hash goveralls 2>/dev/null || go get github.com/mattn/goveralls +hash godir 2>/dev/null || go get github.com/Masterminds/godir -echo "mode: ${COVERMODE}" > "${COVERDIR}/coverage.out" +generate_cover_data() { + for d in $(godir) ; do + local output="${coverdir}/${d//\//-}.cover" + go test -coverprofile="${output}" -covermode="$covermode" "$d" + done -for d in "${PACKAGES[@]}"; do - go test -coverprofile=profile.out -covermode="$COVERMODE" "$d" - if [ -f profile.out ]; then - sed "/mode: $COVERMODE/d" profile.out >> "${COVERDIR}/coverage.out" - rm profile.out - fi -done + echo "mode: $covermode" >"$profile" + grep -h -v "^mode:" "$coverdir"/*.cover >>"$profile" +} + +push_to_coveralls() { + goveralls -coverprofile="${profile}" -service=circle-ci +} + +generate_cover_data +go tool cover -func "${profile}" + +case "$1" in + --html) + go tool cover -html "${profile}" + ;; + --coveralls) + push_to_coveralls + ;; +esac -go tool cover -html "${COVERDIR}/coverage.out" -o "${COVERDIR}/coverage.html" diff --git a/scripts/local-cluster.sh b/scripts/local-cluster.sh index 1cbcd78b9..bb0ce736d 100755 --- a/scripts/local-cluster.sh +++ b/scripts/local-cluster.sh @@ -169,7 +169,7 @@ start_kubernetes() { --volume=/:/rootfs:ro \ --volume=/sys:/sys:ro \ --volume=/var/lib/docker/:/var/lib/docker:rw \ - --volume=/var/lib/kubelet/:/var/lib/kubelet:rw \ + --volume=/var/lib/kubelet/:/var/lib/kubelet:rw,rslave \ --volume=/var/run:/var/run:rw \ --net=host \ --pid=host \ @@ -189,8 +189,10 @@ start_kubernetes() { sleep 1 done - # Create kube-system namespace in kubernetes - $KUBECTL create namespace kube-system >/dev/null + if [[ $KUBE_VERSION == "1.2"* ]]; then + create_kube_system_namespace + create_kube_dns + fi # We expect to have at least 3 running pods - etcd, master and kube-proxy. local attempt=1 @@ -218,6 +220,11 @@ setup_firewall() { fi } +# Create kube-system namespace in kubernetes +create_kube_system_namespace() { + $KUBECTL create namespace kube-system >/dev/null +} + # Activate skydns in kubernetes and wait for pods to be ready. create_kube_dns() { [[ "${ENABLE_CLUSTER_DNS}" = true ]] || return @@ -321,7 +328,6 @@ kube_up() { generate_kubeconfig start_kubernetes - create_kube_dns $KUBECTL cluster-info }