From 96ac6ebc6cc7eb59453bc60ce45fc8d65b9e39d7 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Fri, 24 Jun 2016 16:25:43 -0600 Subject: [PATCH 01/59] fix(tiller): stop printing when var is missing Instead of printing "no value", this prints an empty string by default, but adds a Strict flag on the engine, which (if true) will cause a template render to error out if a value is not supplied. Strict is set to false so that developers can instead use `default` to set default values. --- pkg/engine/engine.go | 16 +++++++++++++++- pkg/engine/engine_test.go | 5 +++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index f5cd9df21..b8be5a316 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -19,6 +19,7 @@ package engine import ( "bytes" "fmt" + "strings" "text/template" "github.com/Masterminds/sprig" @@ -31,6 +32,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. @@ -93,6 +97,13 @@ 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 { t = t.New(fname).Funcs(e.FuncMap) @@ -108,7 +119,10 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) { if err := t.ExecuteTemplate(&buf, file, tpls[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() } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index e8c8e54cc..28de0a779 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", @@ -82,6 +83,10 @@ func TestRender(t *testing.T) { if out["test2"] != expect { t.Errorf("Expected %q, got %q", expect, out["test2"]) } + expect = "" + if out["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) From bcffe8a3f25ba1b817f4b8c1f4057e0aaa69838f Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Mon, 27 Jun 2016 12:46:03 -0600 Subject: [PATCH 02/59] fix(chartutil): fix Expand function This ensures that all necessary directories get created when expanding a chart in the chartutil.Expand function. fixes #892 --- pkg/chartutil/expand.go | 10 ++++++++++ 1 file changed, 10 insertions(+) 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() { From 7bb4893cada2be2116d1fe36c0443008a27c17a2 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Mon, 27 Jun 2016 17:47:18 -0700 Subject: [PATCH 03/59] Support Linter for Values --- cmd/tiller/release_server.go | 21 +++----------- pkg/chartutil/values.go | 21 ++++++++++++++ pkg/lint/rules/template.go | 28 +++++++------------ pkg/lint/rules/template_test.go | 26 +++++++++++++++-- .../testdata/albatross/templates/_helpers.tpl | 16 +++++++++++ .../albatross/templates/albatross.yaml | 2 -- .../testdata/albatross/templates/svc.yaml | 18 ++++++++++++ 7 files changed, 93 insertions(+), 39 deletions(-) create mode 100644 pkg/lint/rules/testdata/albatross/templates/_helpers.tpl delete mode 100644 pkg/lint/rules/testdata/albatross/templates/albatross.yaml create mode 100644 pkg/lint/rules/testdata/albatross/templates/svc.yaml diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index c422cb0a7..770231ade 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -220,33 +220,20 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea 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 := map[string]interface{}{"namespace": s.env.Namespace, "releaseName": name, "releaseTime": ts} + 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 } diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 3987c5a16..c209f5f13 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -288,6 +288,27 @@ func coalesceTables(dst, src map[string]interface{}) map[string]interface{} { return dst } +// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files +func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options map[string]interface{}) (Values, error) { + overrides := map[string]interface{}{ + "Release": map[string]interface{}{ + "Name": options["releaseName"], + "Time": options["releaseTime"], + "Namespace": options["namespace"], + "Service": "Tiller", + }, + "Chart": chrt.Metadata, + } + + vals, err := CoalesceValues(chrt, chrtVals, nil) + if err != nil { + return nil, 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/lint/rules/template.go b/pkg/lint/rules/template.go index 47140eba8..9ea80e6a0 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -19,17 +19,18 @@ package rules import ( "bytes" "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" ) func Templates(linter *support.Linter) { @@ -51,18 +52,9 @@ func Templates(linter *support.Linter) { 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, - } - - chartValues, _ := chartutil.CoalesceValues(chart, chart.Values, overrides) - renderedContentMap, err := engine.New().Render(chart, chartValues) + options := map[string]interface{}{"namespace": "testNamespace", "releaseName": "testRelease", "releaseTime": timeconv.Now()} + valuesToRender, err := chartutil.ToRenderValues(chart, chart.Values, options) + renderedContentMap, err := engine.New().Render(chart, valuesToRender) renderOk := linter.RunLinterRule(support.ErrorSev, validateNoError(err)) @@ -88,7 +80,7 @@ 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, validateNonMissingValues(fileName, templatesPath, valuesToRender, preExecutedTemplate)) linter.RunLinterRule(support.WarningSev, validateQuotes(fileName, string(preExecutedTemplate))) diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index 8dc7e6dad..ecce95238 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" @@ -73,7 +76,7 @@ func TestValidateQuotes(t *testing.T) { } -func TestTemplate(t *testing.T) { +func TestTemplateParsing(t *testing.T) { linter := support.Linter{ChartDir: templateTestBasedir} Templates(&linter) res := linter.Messages @@ -86,3 +89,22 @@ func TestTemplate(t *testing.T) { t.Errorf("Unexpected error: %s", res[0]) } } + +var wrongTemplatePath string = filepath.Join(templateTestBasedir, "templates", "fail.yaml") +var ignoredTemplatePath string = 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" .}} From 26de8ecb760f2473bac7a536721b4abafaf7aafd Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 09:51:11 -0700 Subject: [PATCH 04/59] feat(cmd): silence usage on error Closes https://github.com/kubernetes/helm/issues/864 --- cmd/helm/helm.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index e54ceadb5..3a42044e0 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -69,6 +69,7 @@ var RootCommand = &cobra.Command{ Short: "The Helm package manager for Kubernetes.", Long: globalUsage, PersistentPostRun: teardown, + SilenceUsage: true, } func init() { From 64d90aa567dd4605c252b4f87d677cd4f5c6d91a Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 28 Jun 2016 12:47:24 -0600 Subject: [PATCH 05/59] fix(helm): show correct filename on error Closes #895 --- cmd/helm/install.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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) } From 2c07a32a6b7a5ff45d8af0d495a00b949ad6466f Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 28 Jun 2016 13:13:15 -0600 Subject: [PATCH 06/59] fix(*): fix various style issues from make test-style Closes #847. --- cmd/helm/package.go | 2 +- pkg/helm/client.go | 17 +++++++++-------- pkg/helm/compat.go | 9 +++++++++ pkg/lint/rules/chartfile_test.go | 14 +++++++++----- pkg/lint/rules/template.go | 6 ++++-- pkg/lint/support/doc.go | 2 +- pkg/lint/support/message.go | 4 +++- pkg/lint/support/message_test.go | 2 +- 8 files changed, 37 insertions(+), 19 deletions(-) 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/pkg/helm/client.go b/pkg/helm/client.go index bcfd41e08..91fd1923c 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -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,7 +55,7 @@ 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))) @@ -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..5e3088107 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{ @@ -39,21 +42,26 @@ func ListReleases(limit int, offset string, sort rls.ListSort_SortBy, order rls. return NewClient(HelmHost(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) } +// 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) } +// 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) } +// 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)) @@ -63,6 +71,7 @@ func InstallRelease(vals []byte, rlsName, chStr string, dryRun bool) (*rls.Insta 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)) diff --git a/pkg/lint/rules/chartfile_test.go b/pkg/lint/rules/chartfile_test.go index 6d49a55f4..02b5f2c12 100644 --- a/pkg/lint/rules/chartfile_test.go +++ b/pkg/lint/rules/chartfile_test.go @@ -28,12 +28,16 @@ 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) diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index 47140eba8..d231a4a28 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -32,6 +32,7 @@ import ( "text/template" ) +// Templates lints the templates in the Linter. func Templates(linter *support.Linter) { templatesPath := filepath.Join(linter.ChartDir, "templates") @@ -222,18 +223,19 @@ func validateNoError(readError error) (lintError support.LintError) { func validateYamlContent(filePath string, err error) (lintError support.LintError) { if err != nil { - lintError = fmt.Errorf("templates: \"%s\". Wrong YAML content.", filePath) + lintError = fmt.Errorf("templates: \"%s\". Wrong YAML content", filePath) } return } func validateNoNamespace(filePath string, yamlStruct K8sYamlStruct) (lintError support.LintError) { if yamlStruct.Metadata.Namespace != "" { - lintError = fmt.Errorf("templates: \"%s\". namespace option is currently NOT supported.", filePath) + lintError = fmt.Errorf("templates: \"%s\". namespace option is currently NOT supported", filePath) } return } +// K8sYamlStruct stubs a Kubernetes YAML file. // Need to access for now to Namespace only type K8sYamlStruct struct { Metadata struct { diff --git a/pkg/lint/support/doc.go b/pkg/lint/support/doc.go index a71f0f75d..c843f6468 100644 --- a/pkg/lint/support/doc.go +++ b/pkg/lint/support/doc.go @@ -14,7 +14,7 @@ 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. diff --git a/pkg/lint/support/message.go b/pkg/lint/support/message.go index 1df57458e..1c3fdcd58 100644 --- a/pkg/lint/support/message.go +++ b/pkg/lint/support/message.go @@ -41,11 +41,13 @@ type Message struct { Text string } +// Linter encapsulates a linting run of a particular chart. type Linter struct { Messages []Message ChartDir string } +// LintError describes an error encountered while linting. type LintError interface { error } @@ -57,7 +59,7 @@ func (m Message) String() string { return fmt.Sprintf("[%s] %s", sev[m.Severity], m.Text) } -// Returns true if the validation passed +// RunLinterRule returns true if the validation passed func (l *Linter) RunLinterRule(severity int, lintError LintError) bool { // severity is out of bound if severity < 0 || severity >= len(sev) { diff --git a/pkg/lint/support/message_test.go b/pkg/lint/support/message_test.go index 9a18e01ed..e97b173a7 100644 --- a/pkg/lint/support/message_test.go +++ b/pkg/lint/support/message_test.go @@ -21,7 +21,7 @@ import ( "testing" ) -var linter Linter = Linter{} +var linter = Linter{} var lintError LintError = fmt.Errorf("Foobar") func TestRunLinterRule(t *testing.T) { From 532f03ec788888e8fc39a6109362125acaa91ad1 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 28 Jun 2016 11:57:47 -0600 Subject: [PATCH 07/59] feat(tiller): add .Template object in templates This allows templates to access information about the template file. Right now, the template can only access the .Template.Name, which is the chart-relative path to the current template. Closes #894 --- pkg/chartutil/values.go | 11 ++++---- pkg/engine/engine.go | 37 ++++++++++++++------------- pkg/engine/engine_test.go | 53 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 25 deletions(-) diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 3987c5a16..690162880 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -17,7 +17,7 @@ limitations under the License. package chartutil import ( - "errors" + "fmt" "io" "io/ioutil" "log" @@ -28,7 +28,7 @@ import ( ) // 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 +92,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,7 +105,8 @@ 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. @@ -138,7 +139,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 { diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 9d46b02c9..737e92abd 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -115,9 +115,13 @@ 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) } + // 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. @@ -142,30 +146,27 @@ 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 + // 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, } } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 28de0a779..b11603e4f 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -154,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")}, }}, }, @@ -301,3 +304,49 @@ global: 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{ + "Lavinia": "LaviniaLatiumAeneid", + "Aeneas": "AeneasTroyAeneid", + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } + +} From 12aa72f1212f19c4f7c3d7de242902efba6a5585 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 28 Jun 2016 19:27:20 -0700 Subject: [PATCH 08/59] Replacing options interface argument --- cmd/tiller/release_server.go | 2 +- pkg/chartutil/values.go | 17 +++++++++++++---- pkg/lint/rules/template.go | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 770231ade..e4f84facf 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -226,7 +226,7 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea } ts := timeconv.Now() - options := map[string]interface{}{"namespace": s.env.Namespace, "releaseName": name, "releaseTime": ts} + 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 diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index c209f5f13..b14bc23ba 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -24,6 +24,7 @@ import ( "strings" "github.com/ghodss/yaml" + "github.com/golang/protobuf/ptypes/timestamp" "k8s.io/helm/pkg/proto/hapi/chart" ) @@ -288,13 +289,21 @@ 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 map[string]interface{}) (Values, error) { +func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions) (Values, error) { overrides := map[string]interface{}{ "Release": map[string]interface{}{ - "Name": options["releaseName"], - "Time": options["releaseTime"], - "Namespace": options["namespace"], + "Name": options.Name, + "Time": options.Time, + "Namespace": options.Namespace, "Service": "Tiller", }, "Chart": chrt.Metadata, diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index 9ea80e6a0..0291d401d 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -52,7 +52,7 @@ func Templates(linter *support.Linter) { return } - options := map[string]interface{}{"namespace": "testNamespace", "releaseName": "testRelease", "releaseTime": timeconv.Now()} + options := chartutil.ReleaseOptions{Name: "testRelease", Time: timeconv.Now(), Namespace: "testNamespace"} valuesToRender, err := chartutil.ToRenderValues(chart, chart.Values, options) renderedContentMap, err := engine.New().Render(chart, valuesToRender) From a64303d0bfdd874c771552f8206373e10515910f Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Tue, 28 Jun 2016 10:21:55 -0600 Subject: [PATCH 09/59] feat(lint): lint an archived chart * part of #836 --- cmd/helm/lint.go | 33 ++++++++++++++++ cmd/helm/lint_test.go | 37 ++++++++++++++++++ cmd/helm/search_test.go | 4 +- .../{ => testcache}/foobar-index.yaml | 0 .../testdata/{ => testcache}/local-index.yaml | 0 .../testcharts/compressedchart-0.1.0.tgz | Bin 0 -> 542 bytes .../testcharts/decompressedchart/.helmignore | 5 +++ .../testcharts/decompressedchart/Chart.yaml | 3 ++ .../testcharts/decompressedchart/values.yaml | 4 ++ pkg/lint/lint.go | 8 ++-- 10 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 cmd/helm/lint_test.go rename cmd/helm/testdata/{ => testcache}/foobar-index.yaml (100%) rename cmd/helm/testdata/{ => testcache}/local-index.yaml (100%) create mode 100644 cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz create mode 100644 cmd/helm/testdata/testcharts/decompressedchart/.helmignore create mode 100755 cmd/helm/testdata/testcharts/decompressedchart/Chart.yaml create mode 100644 cmd/helm/testdata/testcharts/decompressedchart/values.yaml diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index 7a52ea597..1c652e182 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -19,11 +19,14 @@ 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" ) @@ -55,6 +58,35 @@ func lintCmd(cmd *cobra.Command, args []string) error { path = args[0] } + if err := lintChart(path); err != nil { + return err + } + + return nil +} + +func lintChart(path string) error { + if strings.HasSuffix(path, ".tgz") { + tempDir, err := ioutil.TempDir("", "helm-lint") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + if err = chartutil.Expand(tempDir, file); err != nil { + return err + } + + base := strings.Split(filepath.Base(path), "-")[0] + path = filepath.Join(tempDir, base) + } + // Guard: Error out of this is not a chart. if _, err := os.Stat(filepath.Join(path, "Chart.yaml")); err != nil { return errLintNoChart @@ -69,5 +101,6 @@ func lintCmd(cmd *cobra.Command, args []string) error { for _, i := range issues { fmt.Printf("%s\n", i) } + return nil } diff --git a/cmd/helm/lint_test.go b/cmd/helm/lint_test.go new file mode 100644 index 000000000..b7e3db6c6 --- /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/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/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 0000000000000000000000000000000000000000..575b271286ca8a01b723298c49dedba66c6c18c6 GIT binary patch literal 542 zcmV+(0^$81iwG0|32ul0|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PL4di`y^|hWG4WG2L8RXjD7vWMOYhb13u^wwIoYVoz*QSu&EG zG;H_3cVsVVQK6y4WNFYth!rF2Bbs;KaiN>mptV>QH8<|nYyZu5ypb29krLPQd4DCs zYno+?eY?M(<+D7yfbmxF7dq>>q3MquC*0hBLW#C8qIE*68@PoxC>!V_0oK~U+irzM zp+lP}-rx-c;gW37*#6O!Wh_medN+}OCDi|h%MR_h3E+_aXIIyu{^#jc)c+}%z!KNI zlMaTH?`0nZ1xqIIxfT}a!{N*A`*&07)o|yqgtd_9J1nt~+#vWoF+>rxTo?;Z!^*e) za3B=@-09AM!={y-G7C#!$Suuo`fK9pkYAqq?>T!y{qK)u-(s#kG8G)sf11w%{Vx`I z9`%0;$nd3x`+N3*GzdSSe9Q)yTWw@{`S1`Wc-DmaXEjxTqEg!6XmcF&|HU8me?E`; zKM51~D|p3hMXl1%r=D?m(;lNxvj$(SQ_+>I=5K g`+DbSZ3(MHEDIu$NaT2a1^@v6|5o&hN&pl90NmINwg3PC literal 0 HcmV?d00001 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/pkg/lint/lint.go b/pkg/lint/lint.go index dbe980606..6c47413e6 100644 --- a/pkg/lint/lint.go +++ b/pkg/lint/lint.go @@ -17,17 +17,17 @@ limitations under the License. package 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 { + // 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) From ebaf5a224320bde1e8eb15e5fcbfab629a827db3 Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Tue, 28 Jun 2016 22:07:41 -0600 Subject: [PATCH 10/59] chore(docs): update "Name" field in values.yaml * the template refers to .Values.Name but the values.yaml file has a name field with a lowercase "n". Figured changing this is the right move since yaml is case sensitive. --- docs/examples/alpine/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5c53f8680364dd90010de8c6bdbe455ea1f593d1 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 22:58:31 -0700 Subject: [PATCH 11/59] fix(cmd): remove --file flag for helm get Users can redirect stdout to a file --- cmd/helm/get.go | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 84217f947..243f758c9 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -19,7 +19,6 @@ package main import ( "errors" "fmt" - "os" "time" "github.com/spf13/cobra" @@ -56,11 +55,6 @@ 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") @@ -88,9 +82,6 @@ var getManifestCommand = &cobra.Command{ } 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") @@ -151,10 +142,12 @@ func getValues(cmd *cobra.Command, args []string) error { if err != nil { return err } - return getToFile(cfgStr) + fmt.Println(cfgStr) + return nil } - return getToFile(res.Release.Config.Raw) + fmt.Println(res.Release.Config.Raw) + return nil } // getManifest implements 'helm get manifest' @@ -167,19 +160,6 @@ func getManifest(cmd *cobra.Command, args []string) error { if err != nil { return prettyError(err) } - return getToFile(res.Release.Manifest) -} - -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 - } - fmt.Fprintln(out, v) + fmt.Println(res.Release.Manifest) return nil } From 63f23d02348e716efe27766803fcd6ba1c8226f8 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 23:03:21 -0700 Subject: [PATCH 12/59] docs(cmd): remove --file flag from help text --- cmd/helm/get.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 243f758c9..2ec9217c7 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -43,8 +43,6 @@ 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 = ` From 77820c748282e9f2134b083a5c1afe480fe7c1da Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Tue, 28 Jun 2016 16:06:03 -0700 Subject: [PATCH 13/59] fix(lint): Return non-zero exit status when lint errors present --- cmd/helm/lint.go | 13 ++++++++++--- pkg/lint/lint.go | 5 ++--- pkg/lint/lint_test.go | 11 ++++++----- pkg/lint/support/message.go | 8 +++++++- pkg/lint/support/message_test.go | 29 +++++++++++++++++------------ 5 files changed, 42 insertions(+), 24 deletions(-) diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index 1c652e182..a14480bbc 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -28,6 +28,7 @@ import ( "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/lint" + "k8s.io/helm/pkg/lint/support" ) var longLintHelp = ` @@ -51,6 +52,7 @@ func init() { } var errLintNoChart = errors.New("no chart found for linting (missing Chart.yaml)") +var errLintFailed = errors.New("lint failed") func lintCmd(cmd *cobra.Command, args []string) error { path := "." @@ -92,15 +94,20 @@ func lintChart(path string) error { return errLintNoChart } - issues := lint.All(path) + linter := lint.All(path) - if len(issues) == 0 { + if len(linter.Messages) == 0 { fmt.Println("Lint OK") + return nil } - for _, i := range issues { + for _, i := range linter.Messages { fmt.Printf("%s\n", i) } + if linter.HighestSeverity == support.ErrorSev { + return errLintFailed + } + return nil } diff --git a/pkg/lint/lint.go b/pkg/lint/lint.go index 6c47413e6..ea723b2f0 100644 --- a/pkg/lint/lint.go +++ b/pkg/lint/lint.go @@ -24,8 +24,7 @@ import ( ) // 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 chartDir, _ := filepath.Abs(basedir) @@ -33,5 +32,5 @@ func All(basedir string) []support.Message { 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..396c23457 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) @@ -60,7 +61,7 @@ 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) } @@ -70,7 +71,7 @@ func TestInvalidYaml(t *testing.T) { } 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) } @@ -80,7 +81,7 @@ func TestBadValues(t *testing.T) { } 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/support/message.go b/pkg/lint/support/message.go index 1c3fdcd58..8615bc15e 100644 --- a/pkg/lint/support/message.go +++ b/pkg/lint/support/message.go @@ -44,7 +44,9 @@ type Message struct { // Linter encapsulates a linting run of a particular chart. type Linter struct { Messages []Message - ChartDir string + // The highest severity of all the failing lint rules + HighestSeverity int + ChartDir string } // LintError describes an error encountered while linting. @@ -68,6 +70,10 @@ func (l *Linter) RunLinterRule(severity int, lintError LintError) bool { if lintError != nil { l.Messages = append(l.Messages, Message{Text: lintError.Error(), Severity: severity}) + + if severity > l.HighestSeverity { + l.HighestSeverity = severity + } } return lintError == nil } diff --git a/pkg/lint/support/message_test.go b/pkg/lint/support/message_test.go index e97b173a7..438216f2f 100644 --- a/pkg/lint/support/message_test.go +++ b/pkg/lint/support/message_test.go @@ -26,26 +26,31 @@ var lintError LintError = fmt.Errorf("Foobar") 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, lintError, 1, false, InfoSev}, + {WarningSev, lintError, 2, false, WarningSev}, + {ErrorSev, lintError, 3, false, ErrorSev}, // No error so it returns true - {ErrorSev, nil, 3, true}, + {ErrorSev, nil, 3, true, ErrorSev}, // Invalid severity values - {4, lintError, 3, false}, - {22, lintError, 3, false}, - {-1, lintError, 3, false}, + {4, lintError, 3, false, ErrorSev}, + {22, lintError, 3, false, ErrorSev}, + {-1, lintError, 3, false, ErrorSev}, } for _, test := range tests { isValid := linter.RunLinterRule(test.Severity, 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, %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, %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity) } if isValid != test.ExpectedReturn { From 09f56459c7fe5dc928f80b7ca0bf92077f313ede Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Wed, 29 Jun 2016 13:53:13 -0700 Subject: [PATCH 14/59] Improve tests to ensure highest severity is retained --- pkg/lint/support/message_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/lint/support/message_test.go b/pkg/lint/support/message_test.go index 438216f2f..fcaa63c4f 100644 --- a/pkg/lint/support/message_test.go +++ b/pkg/lint/support/message_test.go @@ -37,10 +37,12 @@ func TestRunLinterRule(t *testing.T) { {ErrorSev, lintError, 3, false, ErrorSev}, // No error so it returns true {ErrorSev, nil, 3, true, ErrorSev}, + // Retains highest severity + {InfoSev, lintError, 4, false, ErrorSev}, // Invalid severity values - {4, lintError, 3, false, ErrorSev}, - {22, lintError, 3, false, ErrorSev}, - {-1, lintError, 3, false, ErrorSev}, + {4, lintError, 4, false, ErrorSev}, + {22, lintError, 4, false, ErrorSev}, + {-1, lintError, 4, false, ErrorSev}, } for _, test := range tests { From 4ef61a713675139f271b360ab7f46a164384a908 Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Wed, 29 Jun 2016 15:38:53 -0700 Subject: [PATCH 15/59] fix(chartutil): Ensure ReadValues doesn't return a nil map --- pkg/chartutil/values.go | 6 +++--- pkg/chartutil/values_test.go | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 3987c5a16..e072e7e3a 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -110,9 +110,9 @@ func tableLookup(v Values, simple string) (Values, error) { // 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 } 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) { From 310ef9bbcb5fd5f6320cff9230c0e81ccbc9e1e7 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Thu, 30 Jun 2016 12:45:44 -0600 Subject: [PATCH 16/59] fix(helm): fix linter test panic Handle a previously unhandled error in the linter. This simply bails out if a chart's values files do not parse. Also, changed the implementation of CoalesceValues to return a map even on error. --- pkg/chartutil/values.go | 2 +- pkg/lint/rules/template.go | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index b6816f605..d135f41df 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -312,7 +312,7 @@ func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOp vals, err := CoalesceValues(chrt, chrtVals, nil) if err != nil { - return nil, err + return overrides, err } overrides["Values"] = vals diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index a28157658..f83cb63b9 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -55,6 +55,12 @@ func Templates(linter *support.Linter) { 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) renderOk := linter.RunLinterRule(support.ErrorSev, validateNoError(err)) From d32c20fd5ce8a835999cc736a9460e88cb8891ca Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 12:49:10 -0700 Subject: [PATCH 17/59] ref(cmd): move flags out of init() --- cmd/helm/helm.go | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 3a42044e0..779403d8a 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -63,30 +63,35 @@ 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, - SilenceUsage: true, -} - -func init() { +func newRootCmd() *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") + return cmd } +// RootCommand is the top-level command for Helm. +var RootCommand = newRootCmd() + func main() { - if err := RootCommand.Execute(); err != nil { + cmd := RootCommand + if err := cmd.Execute(); err != nil { os.Exit(1) } } @@ -112,7 +117,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() } From 8cb39ce5cc2cb2054c52475e403aafb590d36c2b Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 12:50:45 -0700 Subject: [PATCH 18/59] ref(cmd): refactor out globals and init() --- cmd/helm/list.go | 78 +++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 01e22f4e7..fc62677cf 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -18,6 +18,8 @@ package main import ( "fmt" + "io" + "os" "strings" "github.com/gosuri/uitable" @@ -52,51 +54,59 @@ 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 lister struct { + long bool + max int + offset string + byDate bool + sortDesc bool + out io.Writer } -var ( - listLong bool - listMax int - listOffset string - listByDate bool - listSortDesc bool -) +func newListCmd(out io.Writer) *cobra.Command { + list := &lister{ + out: out, + } + 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 { + return list.run(args) + }, + } + 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.max, "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 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) + RootCommand.AddCommand(newListCmd(os.Stdout)) } -func listCmd(cmd *cobra.Command, args []string) error { +func (l *lister) run(args []string) error { var filter string if len(args) > 0 { filter = strings.Join(args, " ") } 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 := helm.ListReleases(l.max, l.offset, sortBy, sortOrder, filter) if err != nil { return prettyError(err) } @@ -106,21 +116,23 @@ 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") @@ -130,7 +142,5 @@ func formatList(rels []*release.Release) error { s := r.Info.Status.Code.String() table.AddRow(r.Name, t, s, c) } - fmt.Println(table) - - return nil + return table.String() } From b9904281115652b234a81da9def2fd98f1543d36 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 12:52:35 -0700 Subject: [PATCH 19/59] test(cmd): add pattern for testing cmd package --- cmd/helm/list_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 cmd/helm/list_test.go diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go new file mode 100644 index 000000000..4e82a9eeb --- /dev/null +++ b/cmd/helm/list_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "bytes" + "testing" + + "k8s.io/helm/pkg/helm" +) + +// Stubbed out tests at two diffent layers +// TestList() is testing the command action +// TestListCmd() is testing command line interface + +// TODO mock tiller responses + +func TestList(t *testing.T) { + helm.Config.ServAddr = ":44134" + + tests := []struct { + name string + lister *lister + expected string + err bool + }{ + { + name: "with a release", + lister: &lister{}, + expected: "understood-coral", + }, + { + name: "list --long", + lister: &lister{long: true}, + expected: "NAME \tUPDATED \tSTATUS \tCHART \nunderstood-coral\tTue Jun 28 12:29:54 2016\tDEPLOYED\tnginx-0.1.0", + }, + } + + var buf bytes.Buffer + for _, tt := range tests { + tt.lister.out = &buf + err := tt.lister.run([]string{}) + if (err != nil) != tt.err { + t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) + } + actual := string(bytes.TrimSpace(buf.Bytes())) + if actual != tt.expected { + t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, actual) + } + buf.Reset() + } +} + +func TestListCmd(t *testing.T) { + helm.Config.ServAddr = ":44134" + + tests := []struct { + name string + args []string + flags map[string]string + expected string + err bool + }{ + { + name: "with a release", + expected: "understood-coral", + }, + { + name: "list --long", + flags: map[string]string{"long": "1"}, + expected: "NAME \tUPDATED \tSTATUS \tCHART \nunderstood-coral\tTue Jun 28 12:29:54 2016\tDEPLOYED\tnginx-0.1.0", + }, + } + + var buf bytes.Buffer + for _, tt := range tests { + cmd := newListCmd(&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) + } + actual := string(bytes.TrimSpace(buf.Bytes())) + if actual != tt.expected { + t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, actual) + } + buf.Reset() + } +} From 73f1bef3c995aabe76a41b2b72b377d13fd6e9b4 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 16:58:54 -0700 Subject: [PATCH 20/59] test(cmd): add helm client mocking --- cmd/helm/helm.go | 4 ++ cmd/helm/list.go | 37 +++++++++-------- cmd/helm/list_test.go | 97 +++++++++++++++++++++++++++++++++---------- pkg/helm/interface.go | 30 +++++++++++++ 4 files changed, 130 insertions(+), 38 deletions(-) create mode 100644 pkg/helm/interface.go diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 779403d8a..4552df3fc 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -90,7 +90,11 @@ func newRootCmd() *cobra.Command { var RootCommand = newRootCmd() func main() { + out := os.Stdout + client := helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) + cmd := RootCommand + cmd.AddCommand(newListCmd(client, out)) if err := cmd.Execute(); err != nil { os.Exit(1) } diff --git a/cmd/helm/list.go b/cmd/helm/list.go index fc62677cf..a767cbec9 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -19,7 +19,6 @@ package main import ( "fmt" "io" - "os" "strings" "github.com/gosuri/uitable" @@ -55,17 +54,20 @@ flag with the '--offset' flag allows you to page through results. ` type lister struct { + filter string long bool - max int + limit int offset string byDate bool sortDesc bool out io.Writer + client helm.Interface } -func newListCmd(out io.Writer) *cobra.Command { +func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { list := &lister{ - out: out, + client: client, + out: out, } cmd := &cobra.Command{ Use: "list [flags] [FILTER]", @@ -74,28 +76,22 @@ func newListCmd(out io.Writer) *cobra.Command { Aliases: []string{"ls"}, PersistentPreRunE: setupConnection, RunE: func(cmd *cobra.Command, args []string) error { - return list.run(args) + if len(args) > 0 { + list.filter = strings.Join(args, " ") + } + 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.max, "max", "m", 256, "maximum number of releases to fetch") + 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 init() { - RootCommand.AddCommand(newListCmd(os.Stdout)) -} - -func (l *lister) run(args []string) error { - var filter string - if len(args) > 0 { - filter = strings.Join(args, " ") - } - +func (l *lister) run() error { sortBy := services.ListSort_NAME if l.byDate { sortBy = services.ListSort_LAST_RELEASED @@ -106,7 +102,14 @@ func (l *lister) run(args []string) error { sortOrder = services.ListSort_DESC } - res, err := helm.ListReleases(l.max, l.offset, 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) } diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 4e82a9eeb..56eee07f3 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -4,18 +4,51 @@ import ( "bytes" "testing" + "github.com/golang/protobuf/ptypes/timestamp" + "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" ) -// Stubbed out tests at two diffent layers -// TestList() is testing the command action -// TestListCmd() is testing command line interface +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("Hello")}, + }, + }, + Config: &chart.Config{Raw: `name = "value"`}, + } +} -// TODO mock tiller responses +type fakeReleaseLister struct { + helm.Interface + rels []*release.Release + err error +} -func TestList(t *testing.T) { - helm.Config.ServAddr = ":44134" +func (fl *fakeReleaseLister) ListReleases(opts ...helm.ReleaseListOption) (*rls.ListReleasesResponse, error) { + resp := &rls.ListReleasesResponse{ + Count: int64(len(fl.rels)), + Releases: fl.rels, + } + return resp, fl.err +} +func TestListRun(t *testing.T) { tests := []struct { name string lister *lister @@ -23,21 +56,34 @@ func TestList(t *testing.T) { err bool }{ { - name: "with a release", - lister: &lister{}, - expected: "understood-coral", + name: "with a release", + lister: &lister{ + client: &fakeReleaseLister{ + rels: []*release.Release{ + releaseMock("thomas-guide"), + }, + }, + }, + expected: "thomas-guide", }, { - name: "list --long", - lister: &lister{long: true}, - expected: "NAME \tUPDATED \tSTATUS \tCHART \nunderstood-coral\tTue Jun 28 12:29:54 2016\tDEPLOYED\tnginx-0.1.0", + name: "list --long", + lister: &lister{ + client: &fakeReleaseLister{ + rels: []*release.Release{ + releaseMock("atlas"), + }, + }, + long: true, + }, + expected: "NAME \tUPDATED \tSTATUS \tCHART \natlas\tFri Sep 2 15:04:05 1977\tDEPLOYED\tfoo-0.1.0-beta.1", }, } var buf bytes.Buffer for _, tt := range tests { tt.lister.out = &buf - err := tt.lister.run([]string{}) + err := tt.lister.run() if (err != nil) != tt.err { t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) } @@ -50,29 +96,38 @@ func TestList(t *testing.T) { } func TestListCmd(t *testing.T) { - helm.Config.ServAddr = ":44134" - tests := []struct { name string args []string flags map[string]string + client helm.Interface expected string err bool }{ { - name: "with a release", - expected: "understood-coral", + name: "with a release", + client: &fakeReleaseLister{ + rels: []*release.Release{ + releaseMock("thomas-guide"), + }, + }, + expected: "thomas-guide", }, { - name: "list --long", - flags: map[string]string{"long": "1"}, - expected: "NAME \tUPDATED \tSTATUS \tCHART \nunderstood-coral\tTue Jun 28 12:29:54 2016\tDEPLOYED\tnginx-0.1.0", + name: "list --long", + flags: map[string]string{"long": "1"}, + client: &fakeReleaseLister{ + rels: []*release.Release{ + releaseMock("atlas"), + }, + }, + expected: "NAME \tUPDATED \tSTATUS \tCHART \natlas\tFri Sep 2 15:04:05 1977\tDEPLOYED\tfoo-0.1.0-beta.1", }, } var buf bytes.Buffer for _, tt := range tests { - cmd := newListCmd(&buf) + cmd := newListCmd(tt.client, &buf) for flag, value := range tt.flags { cmd.Flags().Set(flag, value) } diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go new file mode 100644 index 000000000..7919adb8d --- /dev/null +++ b/pkg/helm/interface.go @@ -0,0 +1,30 @@ +/* +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) +} From 5013da3d2534ded6bee37288c257caeb613b64c7 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 20:38:58 -0700 Subject: [PATCH 21/59] fix(helm): add ReleaseContent to interface --- pkg/helm/interface.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index 7919adb8d..4cba4db43 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -27,4 +27,5 @@ type Interface interface { 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) } From 4d92bd086f40c44b63cb46db6810d21fe7681974 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 22:36:47 -0700 Subject: [PATCH 22/59] fix(cmd): lazy load client --- cmd/helm/helm.go | 12 ++++++------ cmd/helm/list.go | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 4552df3fc..886be4ef0 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -19,6 +19,7 @@ package main import ( "errors" "fmt" + "io" "os" "strings" @@ -63,7 +64,7 @@ Environment: $HELM_HOST Set an alternative Tiller host. The format is host:port (default ":44134"). ` -func newRootCmd() *cobra.Command { +func newRootCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "helm", Short: "The Helm package manager for Kubernetes.", @@ -83,18 +84,17 @@ func newRootCmd() *cobra.Command { 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(newListCmd(nil, out)) + return cmd } // RootCommand is the top-level command for Helm. -var RootCommand = newRootCmd() +var RootCommand = newRootCmd(os.Stdout) func main() { - out := os.Stdout - client := helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) - cmd := RootCommand - cmd.AddCommand(newListCmd(client, out)) if err := cmd.Execute(); err != nil { os.Exit(1) } diff --git a/cmd/helm/list.go b/cmd/helm/list.go index a767cbec9..42537a71f 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -65,10 +65,7 @@ type lister struct { } func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { - list := &lister{ - client: client, - out: out, - } + list := &lister{out: out} cmd := &cobra.Command{ Use: "list [flags] [FILTER]", Short: "list releases", @@ -79,6 +76,9 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { if len(args) > 0 { list.filter = strings.Join(args, " ") } + if list.client == nil { + list.client = helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) + } return list.run() }, } From 6453c992415d154061ee09ae736682143a59ed7c Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 23:19:51 -0700 Subject: [PATCH 23/59] fix(cmd): rename list cmd context --- cmd/helm/list.go | 6 +++--- cmd/helm/list_test.go | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 42537a71f..0bc454266 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -53,7 +53,7 @@ server's default, which may be much higher than 256. Pairing the '--max' flag with the '--offset' flag allows you to page through results. ` -type lister struct { +type listCmd struct { filter string long bool limit int @@ -65,7 +65,7 @@ type lister struct { } func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { - list := &lister{out: out} + list := &listCmd{out: out} cmd := &cobra.Command{ Use: "list [flags] [FILTER]", Short: "list releases", @@ -91,7 +91,7 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { return cmd } -func (l *lister) run() error { +func (l *listCmd) run() error { sortBy := services.ListSort_NAME if l.byDate { sortBy = services.ListSort_LAST_RELEASED diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 56eee07f3..dace068f7 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -51,13 +51,13 @@ func (fl *fakeReleaseLister) ListReleases(opts ...helm.ReleaseListOption) (*rls. func TestListRun(t *testing.T) { tests := []struct { name string - lister *lister + listCmd *listCmd expected string err bool }{ { name: "with a release", - lister: &lister{ + listCmd: &listCmd{ client: &fakeReleaseLister{ rels: []*release.Release{ releaseMock("thomas-guide"), @@ -68,7 +68,7 @@ func TestListRun(t *testing.T) { }, { name: "list --long", - lister: &lister{ + listCmd: &listCmd{ client: &fakeReleaseLister{ rels: []*release.Release{ releaseMock("atlas"), @@ -82,8 +82,8 @@ func TestListRun(t *testing.T) { var buf bytes.Buffer for _, tt := range tests { - tt.lister.out = &buf - err := tt.lister.run() + tt.listCmd.out = &buf + err := tt.listCmd.run() if (err != nil) != tt.err { t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) } From e339cc7e0c17f177c32e0f8b6f52023b6c0eb5ec Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 28 Jun 2016 23:23:10 -0700 Subject: [PATCH 24/59] fix(cmd): fix lazy load client --- cmd/helm/list.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 0bc454266..ede7cb01d 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -65,7 +65,10 @@ type listCmd struct { } func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { - list := &listCmd{out: out} + list := &listCmd{ + out: out, + client: client, + } cmd := &cobra.Command{ Use: "list [flags] [FILTER]", Short: "list releases", From 5e3044a65ffd2236b93eb4d56201d16c4e9cf23c Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 29 Jun 2016 21:40:38 -0700 Subject: [PATCH 25/59] ref(cmd): refactor get command --- cmd/helm/get.go | 139 +++++++++++++++++++++++++++++------------------ cmd/helm/helm.go | 1 + 2 files changed, 87 insertions(+), 53 deletions(-) diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 2ec9217c7..ab3ef238e 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -19,6 +19,7 @@ package main import ( "errors" "fmt" + "io" "time" "github.com/spf13/cobra" @@ -53,48 +54,88 @@ 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. ` -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, +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, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errReleaseRequired + } + get.release = args[0] + return get.run() + }, + PersistentPreRunE: setupConnection, + } + cmd.AddCommand(newGetValuesCmd(client, out)) + cmd.AddCommand(newGetManifestCmd(client, out)) + return cmd } -var getManifestCommand = &cobra.Command{ - Use: "manifest [flags] RELEASE_NAME", - Short: "download the manifest for a named release", - Long: getManifestHelp, - RunE: getManifest, +type getValuesCmd struct { + allValues bool + getCmd } -func init() { - // 'get values' flags. - getValuesCommand.PersistentFlags().BoolVarP(&allValues, "all", "a", false, "dump all (computed) values") +func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { + get := &getValuesCmd{} + get.out = out + get.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] + return get.run() + }, + } + cmd.Flags().BoolVarP(&get.allValues, "all", "a", false, "dump all (computed) values") + return cmd +} - getCommand.AddCommand(getValuesCommand) - getCommand.AddCommand(getManifestCommand) - RootCommand.AddCommand(getCommand) +type getManifestCmd struct { + getCmd } -// getCmd is the command that implements 'helm get' -func getCmd(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired +func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { + get := &getManifestCmd{} + get.out = out + get.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] + return get.run() + }, } + return cmd +} - res, err := helm.GetReleaseContent(args[0]) +// getCmd is the command that implements 'helm get' +func (g *getCmd) run() error { + res, err := helm.GetReleaseContent(g.release) if err != nil { return prettyError(err) } @@ -108,30 +149,26 @@ 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) + fmt.Fprintf(g.out, "CHART: %s-%s\n", res.Release.Chart.Metadata.Name, res.Release.Chart.Metadata.Version) + fmt.Fprintf(g.out, "RELEASED: %s\n", timeconv.Format(res.Release.Info.LastDeployed, time.ANSIC)) + fmt.Fprintln(g.out, "USER-SUPPLIED VALUES:") + fmt.Fprintln(g.out, res.Release.Config.Raw) + fmt.Fprintln(g.out, "COMPUTED VALUES:") + fmt.Fprintln(g.out, cfgStr) + fmt.Fprintln(g.out, "MANIFEST:") + fmt.Fprintln(g.out, 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]) +func (g *getValuesCmd) run() error { + res, err := helm.GetReleaseContent(g.release) if err != nil { return prettyError(err) } // If the user wants all values, compute the values and return. - if allValues { + if g.allValues { cfg, err := chartutil.CoalesceValues(res.Release.Chart, res.Release.Config, nil) if err != nil { return err @@ -140,24 +177,20 @@ func getValues(cmd *cobra.Command, args []string) error { if err != nil { return err } - fmt.Println(cfgStr) + fmt.Fprintln(g.out, cfgStr) return nil } - fmt.Println(res.Release.Config.Raw) + fmt.Fprintln(g.out, res.Release.Config.Raw) return nil } // 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 (g *getManifestCmd) run() error { + res, err := helm.GetReleaseContent(g.release) if err != nil { return prettyError(err) } - fmt.Println(res.Release.Manifest) + fmt.Fprintln(g.out, res.Release.Manifest) return nil } diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 886be4ef0..e2a39c48b 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -86,6 +86,7 @@ func newRootCmd(out io.Writer) *cobra.Command { p.BoolVarP(&flagDebug, "debug", "", false, "enable verbose output") cmd.AddCommand(newListCmd(nil, out)) + cmd.AddCommand(newGetCmd(nil, out)) return cmd } From 3b2ea04348365e82ed0ce0c9b135ff0f62abc3be Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Wed, 29 Jun 2016 19:35:10 -0700 Subject: [PATCH 26/59] fix(linter) Typos --- pkg/lint/lint_test.go | 2 +- pkg/lint/rules/chartfile.go | 4 ++-- pkg/lint/rules/template.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go index 396c23457..8f683cc44 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -39,7 +39,7 @@ 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.Text, "Directory 'templates/' not found") { w = true } } diff --git a/pkg/lint/rules/chartfile.go b/pkg/lint/rules/chartfile.go index b0333fc27..1cfc88512 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/lint/rules/chartfile.go @@ -59,7 +59,7 @@ func Chartfile(linter *support.Linter) { func validateChartYamlFileExistence(chartPath string) (lintError support.LintError) { _, err := os.Stat(chartPath) if err != nil { - lintError = fmt.Errorf("Chart.yaml file does not exists") + lintError = fmt.Errorf("Chart.yaml file does not exist") } return } @@ -137,7 +137,7 @@ func validateChartEngine(cf *chart.Metadata) (lintError support.LintError) { keys = append(keys, str) } - lintError = fmt.Errorf("Chart.yaml: 'engine %v not valid. Valid options are %v", cf.Engine, keys) + lintError = fmt.Errorf("Chart.yaml: engine '%v' not valid. Valid options are %v", cf.Engine, keys) return } diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index f83cb63b9..448a7567e 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -110,7 +110,7 @@ func Templates(linter *support.Linter) { // Validation functions func validateTemplatesDir(templatesPath string) (lintError support.LintError) { if fi, err := os.Stat(templatesPath); err != nil { - lintError = fmt.Errorf("Templates directory not found") + lintError = fmt.Errorf("Directory 'templates/' not found") } else if err == nil && !fi.IsDir() { lintError = fmt.Errorf("'templates' is not a directory") } From 7c01a28c5cda4f5587099ad3424787fad1fbc4b4 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 30 Jun 2016 15:02:12 -0700 Subject: [PATCH 27/59] test(cmd): add unit tests for get command --- cmd/helm/get.go | 108 +++--------------------------------- cmd/helm/get_manifest.go | 70 +++++++++++++++++++++++ cmd/helm/get_test.go | 49 ++++++++++++++++ cmd/helm/get_values.go | 84 ++++++++++++++++++++++++++++ cmd/helm/get_values_test.go | 49 ++++++++++++++++ cmd/helm/helm_test.go | 70 +++++++++++++++++++++++ cmd/helm/list_test.go | 48 ++-------------- 7 files changed, 334 insertions(+), 144 deletions(-) create mode 100644 cmd/helm/get_manifest.go create mode 100644 cmd/helm/get_test.go create mode 100644 cmd/helm/get_values.go create mode 100644 cmd/helm/get_values_test.go create mode 100644 cmd/helm/helm_test.go diff --git a/cmd/helm/get.go b/cmd/helm/get.go index ab3ef238e..6238773e1 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -42,18 +42,6 @@ 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. -` - -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. -` - var errReleaseRequired = errors.New("release name is required") type getCmd struct { @@ -68,74 +56,29 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { client: client, } cmd := &cobra.Command{ - Use: "get [flags] RELEASE_NAME", - Short: "download a named release", - Long: getHelp, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired - } - get.release = args[0] - return get.run() - }, + Use: "get [flags] RELEASE_NAME", + Short: "download a named release", + Long: getHelp, PersistentPreRunE: setupConnection, - } - cmd.AddCommand(newGetValuesCmd(client, out)) - cmd.AddCommand(newGetManifestCmd(client, out)) - return cmd -} - -type getValuesCmd struct { - allValues bool - getCmd -} - -func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { - get := &getValuesCmd{} - get.out = out - get.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] - return get.run() - }, - } - cmd.Flags().BoolVarP(&get.allValues, "all", "a", false, "dump all (computed) values") - return cmd -} - -type getManifestCmd struct { - getCmd -} - -func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { - get := &getManifestCmd{} - get.out = out - get.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 + if get.client == nil { + get.client = helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) } - get.release = args[0] return get.run() }, } + cmd.AddCommand(newGetValuesCmd(client, out)) + cmd.AddCommand(newGetManifestCmd(client, out)) return cmd } // getCmd is the command that implements 'helm get' func (g *getCmd) run() error { - res, err := helm.GetReleaseContent(g.release) + res, err := g.client.ReleaseContent(g.release) if err != nil { return prettyError(err) } @@ -159,38 +102,3 @@ func (g *getCmd) run() error { fmt.Fprintln(g.out, res.Release.Manifest) return nil } - -// getValues implements 'helm get values' -func (g *getValuesCmd) run() error { - res, err := helm.GetReleaseContent(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 -} - -// getManifest implements 'helm get manifest' -func (g *getManifestCmd) run() error { - res, err := helm.GetReleaseContent(g.release) - if err != nil { - return prettyError(err) - } - fmt.Fprintln(g.out, res.Release.Manifest) - return nil -} diff --git a/cmd/helm/get_manifest.go b/cmd/helm/get_manifest.go new file mode 100644 index 000000000..21a76155c --- /dev/null +++ b/cmd/helm/get_manifest.go @@ -0,0 +1,70 @@ +/* +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] + return get.run() + }, + } + return cmd +} + +// getManifest implements 'helm get manifest' +func (g *getManifestCmd) run() error { + res, err := helm.GetReleaseContent(g.release) + if err != nil { + return prettyError(err) + } + fmt.Fprintln(g.out, res.Release.Manifest) + return nil +} diff --git a/cmd/helm/get_test.go b/cmd/helm/get_test.go new file mode 100644 index 000000000..3bfb56017 --- /dev/null +++ b/cmd/helm/get_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "bytes" + "testing" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestGetCmd(t *testing.T) { + tests := []struct { + name string + args []string + client helm.Interface + expected string + err bool + }{ + { + name: "with a release", + client: &fakeReleaseClient{ + rels: []*release.Release{ + releaseMock("thomas-guide"), + }, + }, + args: []string{"thomas-guide"}, + expected: "CHART: foo-0.1.0-beta.1\nRELEASED: Fri Sep 2 15:04:05 1977\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nMANIFEST:", + }, + { + name: "requires release name arg", + client: &fakeReleaseClient{}, + err: true, + }, + } + + var buf bytes.Buffer + for _, tt := range tests { + cmd := newGetCmd(tt.client, &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) + } + actual := string(bytes.TrimSpace(buf.Bytes())) + if actual != tt.expected { + t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, actual) + } + buf.Reset() + } +} diff --git a/cmd/helm/get_values.go b/cmd/helm/get_values.go new file mode 100644 index 000000000..3765e762c --- /dev/null +++ b/cmd/helm/get_values.go @@ -0,0 +1,84 @@ +/* +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] + 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..5ccf9a5f6 --- /dev/null +++ b/cmd/helm/get_values_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "bytes" + "testing" + + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" +) + +func TestGetValuesCmd(t *testing.T) { + tests := []struct { + name string + args []string + client helm.Interface + expected string + err bool + }{ + { + name: "with a release", + client: &fakeReleaseClient{ + rels: []*release.Release{ + releaseMock("thomas-guide"), + }, + }, + args: []string{"thomas-guide"}, + expected: "name: \"value\"", + }, + { + name: "requires release name arg", + client: &fakeReleaseClient{}, + err: true, + }, + } + + var buf bytes.Buffer + for _, tt := range tests { + cmd := newGetValuesCmd(tt.client, &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) + } + actual := string(bytes.TrimSpace(buf.Bytes())) + if actual != tt.expected { + t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, actual) + } + buf.Reset() + } +} diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go new file mode 100644 index 000000000..8cee5f0aa --- /dev/null +++ b/cmd/helm/helm_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "github.com/golang/protobuf/ptypes/timestamp" + + "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" +) + +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("Hello")}, + }, + }, + Config: &chart.Config{Raw: `name: "value"`}, + } +} + +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 +} diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index dace068f7..bc87039c5 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -4,50 +4,10 @@ import ( "bytes" "testing" - "github.com/golang/protobuf/ptypes/timestamp" - "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" ) -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("Hello")}, - }, - }, - Config: &chart.Config{Raw: `name = "value"`}, - } -} - -type fakeReleaseLister struct { - helm.Interface - rels []*release.Release - err error -} - -func (fl *fakeReleaseLister) ListReleases(opts ...helm.ReleaseListOption) (*rls.ListReleasesResponse, error) { - resp := &rls.ListReleasesResponse{ - Count: int64(len(fl.rels)), - Releases: fl.rels, - } - return resp, fl.err -} - func TestListRun(t *testing.T) { tests := []struct { name string @@ -58,7 +18,7 @@ func TestListRun(t *testing.T) { { name: "with a release", listCmd: &listCmd{ - client: &fakeReleaseLister{ + client: &fakeReleaseClient{ rels: []*release.Release{ releaseMock("thomas-guide"), }, @@ -69,7 +29,7 @@ func TestListRun(t *testing.T) { { name: "list --long", listCmd: &listCmd{ - client: &fakeReleaseLister{ + client: &fakeReleaseClient{ rels: []*release.Release{ releaseMock("atlas"), }, @@ -106,7 +66,7 @@ func TestListCmd(t *testing.T) { }{ { name: "with a release", - client: &fakeReleaseLister{ + client: &fakeReleaseClient{ rels: []*release.Release{ releaseMock("thomas-guide"), }, @@ -116,7 +76,7 @@ func TestListCmd(t *testing.T) { { name: "list --long", flags: map[string]string{"long": "1"}, - client: &fakeReleaseLister{ + client: &fakeReleaseClient{ rels: []*release.Release{ releaseMock("atlas"), }, From 1ba822c7a4fba6ea1ca3c48f42ea44d7d5a5319e Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 30 Jun 2016 15:06:31 -0700 Subject: [PATCH 28/59] docs(cmd): add missing license headers --- cmd/helm/get_test.go | 16 ++++++++++++++++ cmd/helm/get_values_test.go | 16 ++++++++++++++++ cmd/helm/helm_test.go | 16 ++++++++++++++++ cmd/helm/list_test.go | 16 ++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/cmd/helm/get_test.go b/cmd/helm/get_test.go index 3bfb56017..1b9b9a0a5 100644 --- a/cmd/helm/get_test.go +++ b/cmd/helm/get_test.go @@ -1,3 +1,19 @@ +/* +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 ( diff --git a/cmd/helm/get_values_test.go b/cmd/helm/get_values_test.go index 5ccf9a5f6..e93d417f3 100644 --- a/cmd/helm/get_values_test.go +++ b/cmd/helm/get_values_test.go @@ -1,3 +1,19 @@ +/* +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 ( diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 8cee5f0aa..1a0a8a338 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -1,3 +1,19 @@ +/* +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 ( diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index bc87039c5..5aadc4ae3 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -1,3 +1,19 @@ +/* +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 ( From 2fb8b60765a454c0308c1e46560fbcba5e416d0a Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 30 Jun 2016 15:27:14 -0700 Subject: [PATCH 29/59] fix(cmd): load client inside subcommand --- cmd/helm/get.go | 4 ++-- cmd/helm/get_manifest.go | 5 ++++- cmd/helm/get_values.go | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 6238773e1..784bd6084 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -71,8 +71,8 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { return get.run() }, } - cmd.AddCommand(newGetValuesCmd(client, out)) - cmd.AddCommand(newGetManifestCmd(client, out)) + cmd.AddCommand(newGetValuesCmd(nil, out)) + cmd.AddCommand(newGetManifestCmd(nil, out)) return cmd } diff --git a/cmd/helm/get_manifest.go b/cmd/helm/get_manifest.go index 21a76155c..20f61443e 100644 --- a/cmd/helm/get_manifest.go +++ b/cmd/helm/get_manifest.go @@ -53,6 +53,9 @@ func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { return errReleaseRequired } get.release = args[0] + if get.client == nil { + get.client = helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) + } return get.run() }, } @@ -61,7 +64,7 @@ func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { // getManifest implements 'helm get manifest' func (g *getManifestCmd) run() error { - res, err := helm.GetReleaseContent(g.release) + res, err := g.client.ReleaseContent(g.release) if err != nil { return prettyError(err) } diff --git a/cmd/helm/get_values.go b/cmd/helm/get_values.go index 3765e762c..b62833db1 100644 --- a/cmd/helm/get_values.go +++ b/cmd/helm/get_values.go @@ -51,6 +51,9 @@ func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { return errReleaseRequired } get.release = args[0] + if get.client == nil { + get.client = helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) + } return get.run() }, } From 03ca4e892fd6bdee876a832177c76584de205f85 Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Thu, 30 Jun 2016 19:07:56 -0700 Subject: [PATCH 30/59] feat(lint): support linting multiple charts --- cmd/helm/lint.go | 33 ++++++++--- pkg/lint/lint_test.go | 14 ++--- pkg/lint/rules/chartfile.go | 97 ++++++++++++++------------------ pkg/lint/rules/chartfile_test.go | 44 ++++++--------- pkg/lint/rules/template.go | 76 +++++++++++-------------- pkg/lint/rules/template_test.go | 16 +++--- pkg/lint/rules/values.go | 24 ++++---- pkg/lint/support/message.go | 36 ++++++------ pkg/lint/support/message_test.go | 30 +++++----- 9 files changed, 177 insertions(+), 193 deletions(-) diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index a14480bbc..660973183 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -51,19 +51,33 @@ func init() { RootCommand.AddCommand(lintCommand) } -var errLintNoChart = errors.New("no chart found for linting (missing Chart.yaml)") -var errLintFailed = errors.New("lint failed") +var errLintNoChart = errors.New("No chart found for linting (missing Chart.yaml)") +var errLintFailed = errors.New("Lint failed") func lintCmd(cmd *cobra.Command, args []string) error { - path := "." + paths := []string{"."} if len(args) > 0 { - path = args[0] + paths = args } - if err := lintChart(path); err != nil { - return err + var failures int + for _, path := range paths { + if err := lintChart(path); err != nil { + fmt.Println(err) + if err != errLintNoChart { + failures = failures + 1 + } + } + fmt.Println("") + } + + msg := fmt.Sprintf("%d chart(s) linted", len(paths)) + if failures > 0 { + return fmt.Errorf("%s. %d chart(s) failed.", msg, failures) } + fmt.Printf("%s. No failures.\n", msg) + return nil } @@ -91,9 +105,12 @@ func lintChart(path string) error { // Guard: Error out of this is not a chart. if _, err := os.Stat(filepath.Join(path, "Chart.yaml")); err != nil { + fmt.Println("==> Skipping", path) return errLintNoChart } + fmt.Println("==> Linting", path) + linter := lint.All(path) if len(linter.Messages) == 0 { @@ -101,8 +118,8 @@ func lintChart(path string) error { return nil } - for _, i := range linter.Messages { - fmt.Printf("%s\n", i) + for _, msg := range linter.Messages { + fmt.Println(msg) } if linter.HighestSeverity == support.ErrorSev { diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go index 8f683cc44..66541c179 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -39,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, "Directory 'templates/' 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 } } @@ -65,7 +65,7 @@ func TestInvalidYaml(t *testing.T) { 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") } } @@ -75,8 +75,8 @@ func TestBadValues(t *testing.T) { 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) } } diff --git a/pkg/lint/rules/chartfile.go b/pkg/lint/rules/chartfile.go index 1cfc88512..b8b0e2d10 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/lint/rules/chartfile.go @@ -17,12 +17,14 @@ limitations under the License. package 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 exist") - } - 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 02b5f2c12..e1295a7cf 100644 --- a/pkg/lint/rules/chartfile_test.go +++ b/pkg/lint/rules/chartfile_test.go @@ -43,18 +43,6 @@ 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) @@ -107,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"} @@ -156,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 { @@ -192,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) } } @@ -213,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) } } @@ -236,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 448a7567e..ace4acf8c 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -18,6 +18,7 @@ package rules import ( "bytes" + "errors" "fmt" "os" "path/filepath" @@ -35,9 +36,10 @@ import ( // 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 { @@ -47,7 +49,7 @@ 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 @@ -63,7 +65,7 @@ func Templates(linter *support.Linter) { } renderedContentMap, err := engine.New().Render(chart, valuesToRender) - renderOk := linter.RunLinterRule(support.ErrorSev, validateNoError(err)) + renderOk := linter.RunLinterRule(support.ErrorSev, path, err) if !renderOk { return @@ -78,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" { @@ -87,9 +90,9 @@ func Templates(linter *support.Linter) { } // Check that all the templates have a matching value - linter.RunLinterRule(support.WarningSev, validateNonMissingValues(fileName, templatesPath, valuesToRender, 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 @@ -97,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("Directory 'templates/' 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) @@ -128,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) } } @@ -139,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()) @@ -188,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) @@ -207,30 +206,23 @@ 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. diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index ecce95238..dbd792774 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -31,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"} @@ -49,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) } @@ -58,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()) } @@ -68,9 +68,9 @@ 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) } } @@ -85,7 +85,7 @@ func TestTemplateParsing(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]) } } 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/message.go b/pkg/lint/support/message.go index 8615bc15e..05de920e6 100644 --- a/pkg/lint/support/message.go +++ b/pkg/lint/support/message.go @@ -33,14 +33,6 @@ const ( // sev matches the *Sev states. var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"} -// Message is a linting output message -type Message struct { - // Severity is one of the *Sev constants - Severity int - // Text contains the message text - Text string -} - // Linter encapsulates a linting run of a particular chart. type Linter struct { Messages []Message @@ -49,31 +41,35 @@ type Linter struct { ChartDir string } -// LintError describes an error encountered while linting. -type LintError interface { - error +// Message describes an error encountered while linting. +type Message struct { + // Severity is one of the *Sev constants + Severity int + Path string + Err 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) +func NewMessage(severity int, path string, err error) Message { + return Message{Severity: severity, Path: path, Err: err} } // RunLinterRule returns true if the validation passed -func (l *Linter) RunLinterRule(severity int, lintError LintError) bool { +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 fcaa63c4f..b2a4d4d98 100644 --- a/pkg/lint/support/message_test.go +++ b/pkg/lint/support/message_test.go @@ -17,12 +17,12 @@ limitations under the License. package support import ( - "fmt" + "errors" "testing" ) var linter = Linter{} -var lintError LintError = fmt.Errorf("Foobar") +var lintError = errors.New("lint failed") func TestRunLinterRule(t *testing.T) { var tests = []struct { @@ -46,34 +46,34 @@ func TestRunLinterRule(t *testing.T) { } 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 now have %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, %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity) + 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()) } } From 4db6cd93bb5a88de1d79fd5a692553fc8e725352 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 1 Jul 2016 11:17:20 -0700 Subject: [PATCH 31/59] fix(test): match output using regexp --- cmd/helm/get_test.go | 9 +++++---- cmd/helm/list_test.go | 17 +++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/cmd/helm/get_test.go b/cmd/helm/get_test.go index 1b9b9a0a5..7dce68eb1 100644 --- a/cmd/helm/get_test.go +++ b/cmd/helm/get_test.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "regexp" "testing" "k8s.io/helm/pkg/helm" @@ -40,7 +41,7 @@ func TestGetCmd(t *testing.T) { }, }, args: []string{"thomas-guide"}, - expected: "CHART: foo-0.1.0-beta.1\nRELEASED: Fri Sep 2 15:04:05 1977\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nMANIFEST:", + expected: "CHART: foo-0.1.0-beta.1\nRELEASED: (.*)\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nMANIFEST:", }, { name: "requires release name arg", @@ -56,9 +57,9 @@ func TestGetCmd(t *testing.T) { if (err != nil) != tt.err { t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) } - actual := string(bytes.TrimSpace(buf.Bytes())) - if actual != tt.expected { - t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, actual) + 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/list_test.go b/cmd/helm/list_test.go index 5aadc4ae3..845641bc2 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "regexp" "testing" "k8s.io/helm/pkg/helm" @@ -52,7 +53,7 @@ func TestListRun(t *testing.T) { }, long: true, }, - expected: "NAME \tUPDATED \tSTATUS \tCHART \natlas\tFri Sep 2 15:04:05 1977\tDEPLOYED\tfoo-0.1.0-beta.1", + expected: "NAME \tUPDATED \tSTATUS \tCHART \natlas\t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1", }, } @@ -63,9 +64,9 @@ func TestListRun(t *testing.T) { if (err != nil) != tt.err { t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) } - actual := string(bytes.TrimSpace(buf.Bytes())) - if actual != tt.expected { - t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, actual) + 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() } @@ -97,7 +98,7 @@ func TestListCmd(t *testing.T) { releaseMock("atlas"), }, }, - expected: "NAME \tUPDATED \tSTATUS \tCHART \natlas\tFri Sep 2 15:04:05 1977\tDEPLOYED\tfoo-0.1.0-beta.1", + expected: "NAME \tUPDATED \tSTATUS \tCHART \natlas\t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1", }, } @@ -111,9 +112,9 @@ func TestListCmd(t *testing.T) { if (err != nil) != tt.err { t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) } - actual := string(bytes.TrimSpace(buf.Bytes())) - if actual != tt.expected { - t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, actual) + 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() } From 37cf2b9e7d470402fdb8c93245239b35059167c0 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 1 Jul 2016 11:42:12 -0700 Subject: [PATCH 32/59] ref(tests): simplify creating fake client --- cmd/helm/get_test.go | 21 +++++++++------------ cmd/helm/get_values_test.go | 21 +++++++++------------ cmd/helm/list_test.go | 20 +++++++++----------- 3 files changed, 27 insertions(+), 35 deletions(-) diff --git a/cmd/helm/get_test.go b/cmd/helm/get_test.go index 7dce68eb1..0ee6ff356 100644 --- a/cmd/helm/get_test.go +++ b/cmd/helm/get_test.go @@ -21,7 +21,6 @@ import ( "regexp" "testing" - "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/proto/hapi/release" ) @@ -29,30 +28,28 @@ func TestGetCmd(t *testing.T) { tests := []struct { name string args []string - client helm.Interface + resp *release.Release expected string err bool }{ { - name: "with a release", - client: &fakeReleaseClient{ - rels: []*release.Release{ - releaseMock("thomas-guide"), - }, - }, + name: "with a release", + resp: releaseMock("thomas-guide"), args: []string{"thomas-guide"}, expected: "CHART: foo-0.1.0-beta.1\nRELEASED: (.*)\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nMANIFEST:", }, { - name: "requires release name arg", - client: &fakeReleaseClient{}, - err: true, + name: "requires release name arg", + err: true, }, } var buf bytes.Buffer for _, tt := range tests { - cmd := newGetCmd(tt.client, &buf) + c := &fakeReleaseClient{ + rels: []*release.Release{tt.resp}, + } + cmd := newGetCmd(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) diff --git a/cmd/helm/get_values_test.go b/cmd/helm/get_values_test.go index e93d417f3..4a28ccb24 100644 --- a/cmd/helm/get_values_test.go +++ b/cmd/helm/get_values_test.go @@ -20,7 +20,6 @@ import ( "bytes" "testing" - "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/proto/hapi/release" ) @@ -28,30 +27,28 @@ func TestGetValuesCmd(t *testing.T) { tests := []struct { name string args []string - client helm.Interface + resp *release.Release expected string err bool }{ { - name: "with a release", - client: &fakeReleaseClient{ - rels: []*release.Release{ - releaseMock("thomas-guide"), - }, - }, + name: "with a release", + resp: releaseMock("thomas-guide"), args: []string{"thomas-guide"}, expected: "name: \"value\"", }, { - name: "requires release name arg", - client: &fakeReleaseClient{}, - err: true, + name: "requires release name arg", + err: true, }, } var buf bytes.Buffer for _, tt := range tests { - cmd := newGetValuesCmd(tt.client, &buf) + c := &fakeReleaseClient{ + rels: []*release.Release{tt.resp}, + } + cmd := newGetValuesCmd(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) diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 845641bc2..c3bec76ec 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -21,7 +21,6 @@ import ( "regexp" "testing" - "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/proto/hapi/release" ) @@ -77,26 +76,22 @@ func TestListCmd(t *testing.T) { name string args []string flags map[string]string - client helm.Interface + resp []*release.Release expected string err bool }{ { name: "with a release", - client: &fakeReleaseClient{ - rels: []*release.Release{ - releaseMock("thomas-guide"), - }, + resp: []*release.Release{ + releaseMock("thomas-guide"), }, expected: "thomas-guide", }, { name: "list --long", flags: map[string]string{"long": "1"}, - client: &fakeReleaseClient{ - rels: []*release.Release{ - releaseMock("atlas"), - }, + resp: []*release.Release{ + releaseMock("atlas"), }, expected: "NAME \tUPDATED \tSTATUS \tCHART \natlas\t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1", }, @@ -104,7 +99,10 @@ func TestListCmd(t *testing.T) { var buf bytes.Buffer for _, tt := range tests { - cmd := newListCmd(tt.client, &buf) + c := &fakeReleaseClient{ + rels: tt.resp, + } + cmd := newListCmd(c, &buf) for flag, value := range tt.flags { cmd.Flags().Set(flag, value) } From f273510c978d36c5f8139be31dbc7388a93c7706 Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Fri, 1 Jul 2016 15:20:13 -0700 Subject: [PATCH 33/59] fix(lint): fix golint errors --- cmd/helm/lint.go | 4 ++-- pkg/lint/rules/template_test.go | 4 ++-- pkg/lint/support/message.go | 1 + pkg/lint/support/message_test.go | 16 ++++++++-------- pkg/repo/repo.go | 1 + 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index 660973183..3d173790c 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -73,10 +73,10 @@ func lintCmd(cmd *cobra.Command, args []string) error { msg := fmt.Sprintf("%d chart(s) linted", len(paths)) if failures > 0 { - return fmt.Errorf("%s. %d chart(s) failed.", msg, failures) + return fmt.Errorf("%s, %d chart(s) failed", msg, failures) } - fmt.Printf("%s. No failures.\n", msg) + fmt.Printf("%s, no failures\n", msg) return nil } diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index dbd792774..c60152794 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -90,8 +90,8 @@ func TestTemplateParsing(t *testing.T) { } } -var wrongTemplatePath string = filepath.Join(templateTestBasedir, "templates", "fail.yaml") -var ignoredTemplatePath string = filepath.Join(templateTestBasedir, "fail.yaml.ignored") +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 diff --git a/pkg/lint/support/message.go b/pkg/lint/support/message.go index 05de920e6..6a878031a 100644 --- a/pkg/lint/support/message.go +++ b/pkg/lint/support/message.go @@ -53,6 +53,7 @@ func (m Message) Error() string { return fmt.Sprintf("[%s] %s: %s", sev[m.Severity], m.Path, m.Err.Error()) } +// NewMessage creates a new Message struct func NewMessage(severity int, path string, err error) Message { return Message{Severity: severity, Path: path, Err: err} } diff --git a/pkg/lint/support/message_test.go b/pkg/lint/support/message_test.go index b2a4d4d98..4a9c33c34 100644 --- a/pkg/lint/support/message_test.go +++ b/pkg/lint/support/message_test.go @@ -22,7 +22,7 @@ import ( ) var linter = Linter{} -var lintError = errors.New("lint failed") +var errLint = errors.New("lint failed") func TestRunLinterRule(t *testing.T) { var tests = []struct { @@ -32,17 +32,17 @@ func TestRunLinterRule(t *testing.T) { ExpectedReturn bool ExpectedHighestSeverity int }{ - {InfoSev, lintError, 1, false, InfoSev}, - {WarningSev, lintError, 2, false, WarningSev}, - {ErrorSev, lintError, 3, false, ErrorSev}, + {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}, // Retains highest severity - {InfoSev, lintError, 4, false, ErrorSev}, + {InfoSev, errLint, 4, false, ErrorSev}, // Invalid severity values - {4, lintError, 4, false, ErrorSev}, - {22, lintError, 4, false, ErrorSev}, - {-1, lintError, 4, false, ErrorSev}, + {4, errLint, 4, false, ErrorSev}, + {22, errLint, 4, false, ErrorSev}, + {-1, errLint, 4, false, ErrorSev}, } for _, test := range tests { diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index b9f8fda44..b113629f2 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -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)} From 8d302bd0393c34e2d5a9bf88324d85e053f88c45 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 1 Jul 2016 08:58:09 -0700 Subject: [PATCH 34/59] ref(ci): remove glide setup in circle.yml --- circle.yml | 47 +++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/circle.yml b/circle.yml index 83b4dcadd..3bc78e7b2 100644 --- a/circle.yml +++ b/circle.yml @@ -1,43 +1,38 @@ machine: 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 + - wget "https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz" + - sudo tar -C /usr/local -xzf "go${GOVERSION}.linux-amd64.tar.gz" + + - mkdir -p "$(dirname ${WORKDIR})" + - cp -R "${HOME}/helm" "${WORKDIR}" + + - 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}" && make test 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 - - make docker-build - - sudo docker push gcr.io/kubernetes-helm/tiller:canary + - docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io + - cd "${WORKDIR}" && make docker-build + - docker push gcr.io/kubernetes-helm/tiller:canary From 6ddcca583bb15c108bc87ae28d2ea11271dda759 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 1 Jul 2016 21:51:40 -0700 Subject: [PATCH 35/59] ref(ci): split unit-test and lint steps --- circle.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 3bc78e7b2..307abb0f5 100644 --- a/circle.yml +++ b/circle.yml @@ -13,12 +13,15 @@ dependencies: - rm -rf "$GOPATH" override: + # 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: @@ -26,7 +29,8 @@ dependencies: test: override: - - cd "${WORKDIR}" && make test + - cd "${WORKDIR}" && make test-style + - cd "${WORKDIR}" && make test-unit deployment: gcr: From e35474811820066d91484c2dc2e25f47a16dedce Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 5 Jul 2016 09:04:37 -0700 Subject: [PATCH 36/59] chore(ci): bump docker version --- circle.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/circle.yml b/circle.yml index 307abb0f5..d20f703ba 100644 --- a/circle.yml +++ b/circle.yml @@ -1,4 +1,7 @@ machine: + pre: + - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 + environment: GOVERSION: "1.6.2" GOPATH: "${HOME}/.go_workspace" From 3133f4345d8df654f94528439a8367f3699642a9 Mon Sep 17 00:00:00 2001 From: Jeff Zellner Date: Tue, 5 Jul 2016 10:18:59 -0600 Subject: [PATCH 37/59] Update developers.md fix typo --- docs/developers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From f89b6ce7619c876181deba1450577b0a5e796661 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 5 Jul 2016 10:55:17 -0700 Subject: [PATCH 38/59] feat(ci): setup parallel testing --- circle.yml | 5 ++--- scripts/ci.sh | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100755 scripts/ci.sh diff --git a/circle.yml b/circle.yml index d20f703ba..54117b8e1 100644 --- a/circle.yml +++ b/circle.yml @@ -32,8 +32,8 @@ dependencies: test: override: - - cd "${WORKDIR}" && make test-style - - cd "${WORKDIR}" && make test-unit + - cd "${WORKDIR}" && ./scripts/ci.sh: + parallel: true deployment: gcr: @@ -41,5 +41,4 @@ deployment: commands: - echo $GCLOUD_SERVICE_KEY | base64 --decode > ${HOME}/gcloud-service-key.json - docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io - - cd "${WORKDIR}" && make docker-build - docker push gcr.io/kubernetes-helm/tiller:canary diff --git a/scripts/ci.sh b/scripts/ci.sh new file mode 100755 index 000000000..80423a3ef --- /dev/null +++ b/scripts/ci.sh @@ -0,0 +1,38 @@ +#!/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" + +case "${CIRCLE_NODE_INDEX-0}" in + 0) + echo "Running 'make test-unit'" + make test-unit + ;; + 1) + echo "Running 'make test-style'" + make test-style + ;; + 2) + echo "Running 'make docker-build'" + make test-style + ;; +esac From f3e754794e629eb150ebcafdac88fe617ef524e3 Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Tue, 5 Jul 2016 11:53:19 -0700 Subject: [PATCH 39/59] fix(lint): print relative path of packaged chart instead of temp dir --- cmd/helm/lint.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index 3d173790c..9c5676cf8 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -82,6 +82,7 @@ func lintCmd(cmd *cobra.Command, args []string) error { } func lintChart(path string) error { + var chartPath string if strings.HasSuffix(path, ".tgz") { tempDir, err := ioutil.TempDir("", "helm-lint") if err != nil { @@ -100,18 +101,20 @@ func lintChart(path string) error { } base := strings.Split(filepath.Base(path), "-")[0] - path = filepath.Join(tempDir, base) + chartPath = filepath.Join(tempDir, base) + } else { + chartPath = path } // Guard: Error out of this is not a chart. - if _, err := os.Stat(filepath.Join(path, "Chart.yaml")); err != nil { + if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { fmt.Println("==> Skipping", path) return errLintNoChart } fmt.Println("==> Linting", path) - linter := lint.All(path) + linter := lint.All(chartPath) if len(linter.Messages) == 0 { fmt.Println("Lint OK") From eba297714f4944f298d6c6bd8d636a9172fb104a Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 5 Jul 2016 13:02:10 -0700 Subject: [PATCH 40/59] fix(scripts): update local-cluster.sh to work with v1.3 --- scripts/cluster/kube-system.yaml | 4 ---- scripts/local-cluster.sh | 14 ++++++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) delete mode 100644 scripts/cluster/kube-system.yaml 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/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 } From 988bb02991b5af50bd9103546e634917d1764483 Mon Sep 17 00:00:00 2001 From: Keerthan Mala Date: Tue, 5 Jul 2016 15:58:47 -0400 Subject: [PATCH 41/59] docs(helm):correct the documentation for the global values usage --- docs/charts.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/charts.md b/docs/charts.md index f1d119d87..d62f82603 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -302,9 +302,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: From dda61f60744e9e547aae9b13be7e8540eafce32f Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Tue, 5 Jul 2016 16:57:22 -0700 Subject: [PATCH 42/59] fix(lint): only return count of actually linted charts --- cmd/helm/lint.go | 53 +++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index 9c5676cf8..d3e496cda 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -52,7 +52,6 @@ func init() { } var errLintNoChart = errors.New("No chart found for linting (missing Chart.yaml)") -var errLintFailed = errors.New("Lint failed") func lintCmd(cmd *cobra.Command, args []string) error { paths := []string{"."} @@ -60,18 +59,32 @@ func lintCmd(cmd *cobra.Command, args []string) error { paths = args } + var total int var failures int for _, path := range paths { - if err := lintChart(path); err != nil { + if linter, err := lintChart(path); err != nil { + fmt.Println("==> Skipping", path) fmt.Println(err) - if err != errLintNoChart { + } 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", len(paths)) + msg := fmt.Sprintf("%d chart(s) linted", total) if failures > 0 { return fmt.Errorf("%s, %d chart(s) failed", msg, failures) } @@ -81,23 +94,25 @@ func lintCmd(cmd *cobra.Command, args []string) error { return nil } -func lintChart(path string) error { +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 err + return linter, err } defer os.RemoveAll(tempDir) file, err := os.Open(path) if err != nil { - return err + return linter, err } defer file.Close() if err = chartutil.Expand(tempDir, file); err != nil { - return err + return linter, err } base := strings.Split(filepath.Base(path), "-")[0] @@ -108,26 +123,8 @@ func lintChart(path string) error { // Guard: Error out of this is not a chart. if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { - fmt.Println("==> Skipping", path) - return errLintNoChart + return linter, errLintNoChart } - fmt.Println("==> Linting", path) - - linter := lint.All(chartPath) - - if len(linter.Messages) == 0 { - fmt.Println("Lint OK") - return nil - } - - for _, msg := range linter.Messages { - fmt.Println(msg) - } - - if linter.HighestSeverity == support.ErrorSev { - return errLintFailed - } - - return nil + return lint.All(chartPath), nil } From 1d957e81ed1d9305a03846366ae88ae9075a1d1d Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Tue, 5 Jul 2016 17:34:49 -0700 Subject: [PATCH 43/59] chore(deps): pin kubernetes to an official release (v1.3.0) --- glide.lock | 21 ++++++++++----------- glide.yaml | 6 +++--- 2 files changed, 13 insertions(+), 14 deletions(-) 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 From a4458085ab610d52f192ce85085680f1a369cf05 Mon Sep 17 00:00:00 2001 From: Adnan Abdulhussein Date: Wed, 6 Jul 2016 10:04:21 -0700 Subject: [PATCH 44/59] fix(lint): fix tests --- cmd/helm/lint_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/helm/lint_test.go b/cmd/helm/lint_test.go index b7e3db6c6..9f71c0231 100644 --- a/cmd/helm/lint_test.go +++ b/cmd/helm/lint_test.go @@ -26,11 +26,11 @@ var ( ) func TestLintChart(t *testing.T) { - if err := lintChart(chartDirPath); err != nil { + if _, err := lintChart(chartDirPath); err != nil { t.Errorf("%s", err) } - if err := lintChart(archivedChartPath); err != nil { + if _, err := lintChart(archivedChartPath); err != nil { t.Errorf("%s", err) } From 7223e33deb52135629320eb33921d14d24d79f00 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 6 Jul 2016 10:35:08 -0700 Subject: [PATCH 45/59] fix(ci): add docker-build to the parallel builds --- scripts/ci.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci.sh b/scripts/ci.sh index 80423a3ef..b6c59943f 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -33,6 +33,6 @@ case "${CIRCLE_NODE_INDEX-0}" in ;; 2) echo "Running 'make docker-build'" - make test-style + make docker-build ;; esac From 5b14d2567fff69613bc2b4953a17639a0a97b8f3 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 6 Jul 2016 10:38:34 -0700 Subject: [PATCH 46/59] docs(ci): add cicleci badge to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) 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. From c5682803cf63577d926e18f917a0cf7ac84b4352 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 6 Jul 2016 13:36:17 -0700 Subject: [PATCH 47/59] fix(ci): move docker-build out of parallel step circle ci can't seem to find the image when it is build in a parallel container. --- circle.yml | 1 + scripts/ci.sh | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/circle.yml b/circle.yml index 54117b8e1..1c40610fb 100644 --- a/circle.yml +++ b/circle.yml @@ -41,4 +41,5 @@ deployment: commands: - echo $GCLOUD_SERVICE_KEY | base64 --decode > ${HOME}/gcloud-service-key.json - docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io + - make docker-build - docker push gcr.io/kubernetes-helm/tiller:canary diff --git a/scripts/ci.sh b/scripts/ci.sh index b6c59943f..7ed63df4a 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -31,8 +31,4 @@ case "${CIRCLE_NODE_INDEX-0}" in echo "Running 'make test-style'" make test-style ;; - 2) - echo "Running 'make docker-build'" - make docker-build - ;; esac From ff32062ae80950d44d07916154d4b22b72a93ac9 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 29 Jun 2016 17:21:32 -0600 Subject: [PATCH 48/59] feat(tiller): support hooks for install --- _proto/hapi/release/hook.proto | 45 +++++++ _proto/hapi/release/release.proto | 4 + cmd/tiller/environment/environment.go | 13 ++ cmd/tiller/environment/environment_test.go | 3 + cmd/tiller/hooks.go | 107 +++++++++++++++ cmd/tiller/hooks_test.go | 121 +++++++++++++++++ cmd/tiller/release_server.go | 101 ++++++++++++-- cmd/tiller/release_server_test.go | 62 +++++++++ .../nginx/templates/post-install-job.yaml | 31 +++++ .../nginx/templates/pre-install-secret.yaml | 14 ++ docs/examples/nginx/values.yaml | 3 + pkg/kube/client.go | 85 ++++++++++++ pkg/proto/hapi/release/hook.pb.go | 125 ++++++++++++++++++ pkg/proto/hapi/release/info.pb.go | 21 +-- pkg/proto/hapi/release/release.pb.go | 42 +++--- pkg/proto/hapi/release/status.pb.go | 6 +- pkg/proto/hapi/services/tiller.pb.go | 22 +-- 17 files changed, 742 insertions(+), 63 deletions(-) create mode 100644 _proto/hapi/release/hook.proto create mode 100644 cmd/tiller/hooks.go create mode 100644 cmd/tiller/hooks_test.go create mode 100644 docs/examples/nginx/templates/post-install-job.yaml create mode 100644 docs/examples/nginx/templates/pre-install-secret.yaml create mode 100644 pkg/proto/hapi/release/hook.pb.go 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..890a4ed72 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,7 @@ 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; } 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..4d074fa9a 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -216,6 +216,16 @@ 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 } @@ -250,9 +260,11 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea 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 +279,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 +289,40 @@ func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea Status: &release.Status{Code: release.Status_UNKNOWN}, }, Manifest: b.String(), + Hooks: hooks, } + 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 +332,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 +392,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..4b03caf6f 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) } @@ -115,6 +152,7 @@ func TestInstallReleaseDryRun(t *testing.T) { {Name: "empty", Data: []byte("")}, {Name: "with-partials", Data: []byte("hello: {{ template \"partials/_planet\" . }}")}, {Name: "partials/_planet", Data: []byte("Earth")}, + {Name: "hooks", Data: []byte(manifestWithHook)}, }, }, DryRun: true, @@ -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/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/pkg/kube/client.go b/pkg/kube/client.go index 0ed34932e..7618f89c4 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -19,12 +19,16 @@ package 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/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..fc59fecbb 100644 --- a/pkg/proto/hapi/release/release.pb.go +++ b/pkg/proto/hapi/release/release.pb.go @@ -29,12 +29,14 @@ 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"` } 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 +59,31 @@ 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{ + // 224 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0x3f, 0x4f, 0x85, 0x30, + 0x14, 0xc5, 0x83, 0xfc, 0x93, 0xab, 0x8b, 0x77, 0xd0, 0x86, 0x89, 0x38, 0x28, 0x71, 0x28, 0x89, + 0x7e, 0x03, 0x5d, 0x74, 0xed, 0xe8, 0x56, 0x49, 0x91, 0x46, 0x69, 0x09, 0xf0, 0x81, 0xfd, 0x28, + 0xd2, 0xde, 0xbe, 0x17, 0x78, 0x6f, 0xb9, 0x6d, 0xcf, 0xef, 0xe4, 0xf4, 0xb4, 0x50, 0xf6, 0x72, + 0xd4, 0xcd, 0xa4, 0x7e, 0x95, 0x9c, 0xd5, 0x61, 0xe5, 0xe3, 0x64, 0x17, 0x8b, 0xd7, 0x8e, 0xf1, + 0xa0, 0x95, 0x77, 0x3b, 0x67, 0x6f, 0xed, 0x0f, 0xd9, 0x4e, 0x80, 0x36, 0x9d, 0xdd, 0x81, 0xb6, + 0x97, 0xd3, 0xd2, 0xb4, 0xd6, 0x74, 0xfa, 0x3b, 0x80, 0xdb, 0x2d, 0x70, 0x93, 0xf4, 0xfb, 0xbf, + 0x08, 0x72, 0x41, 0x39, 0x88, 0x90, 0x18, 0x39, 0x28, 0x16, 0x55, 0x51, 0x5d, 0x08, 0xbf, 0xc7, + 0x07, 0x48, 0x5c, 0x3c, 0xbb, 0x58, 0xb5, 0xab, 0x67, 0xe4, 0xdb, 0x7e, 0xfc, 0x63, 0x25, 0xc2, + 0x73, 0x7c, 0x84, 0xd4, 0xc7, 0xb2, 0xd8, 0x1b, 0x6f, 0xc8, 0x48, 0x37, 0xbd, 0xb9, 0x29, 0x88, + 0xe3, 0x13, 0x64, 0x54, 0x8c, 0x25, 0xdb, 0xc8, 0xe0, 0xf4, 0x44, 0x04, 0x07, 0x96, 0x70, 0x39, + 0x48, 0xa3, 0x3b, 0x35, 0x2f, 0x2c, 0xf5, 0xa5, 0x8e, 0x67, 0xac, 0x21, 0x75, 0x1f, 0x32, 0xb3, + 0xac, 0x8a, 0xcf, 0x9b, 0xbd, 0xaf, 0x48, 0x90, 0xe1, 0xb5, 0xf8, 0xcc, 0x83, 0xfc, 0x95, 0xf9, + 0x47, 0xbf, 0xfc, 0x07, 0x00, 0x00, 0xff, 0xff, 0x96, 0x35, 0x9e, 0xef, 0x83, 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 } From 15b428d450cc7a63d5ecb338b211182de9db7fb2 Mon Sep 17 00:00:00 2001 From: Michelle Noorali Date: Wed, 6 Jul 2016 17:01:34 -0600 Subject: [PATCH 49/59] feat(*): add version to release laying ground work for #690 --- _proto/hapi/release/release.proto | 4 ++++ cmd/helm/get.go | 3 ++- cmd/helm/get_test.go | 2 +- cmd/helm/helm_test.go | 3 ++- cmd/helm/list.go | 5 +++-- cmd/helm/list_test.go | 4 ++-- cmd/tiller/release_server.go | 1 + pkg/proto/hapi/release/release.pb.go | 33 +++++++++++++++------------- 8 files changed, 33 insertions(+), 22 deletions(-) diff --git a/_proto/hapi/release/release.proto b/_proto/hapi/release/release.proto index 890a4ed72..a13c99274 100644 --- a/_proto/hapi/release/release.proto +++ b/_proto/hapi/release/release.proto @@ -44,4 +44,8 @@ message Release { // 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/cmd/helm/get.go b/cmd/helm/get.go index 784bd6084..79be29ca8 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -92,8 +92,9 @@ func (g *getCmd) run() error { return err } - fmt.Fprintf(g.out, "CHART: %s-%s\n", res.Release.Chart.Metadata.Name, res.Release.Chart.Metadata.Version) + fmt.Fprintf(g.out, "VERSION: %v\n", res.Release.Version) fmt.Fprintf(g.out, "RELEASED: %s\n", timeconv.Format(res.Release.Info.LastDeployed, time.ANSIC)) + fmt.Fprintf(g.out, "CHART: %s-%s\n", res.Release.Chart.Metadata.Name, res.Release.Chart.Metadata.Version) fmt.Fprintln(g.out, "USER-SUPPLIED VALUES:") fmt.Fprintln(g.out, res.Release.Config.Raw) fmt.Fprintln(g.out, "COMPUTED VALUES:") diff --git a/cmd/helm/get_test.go b/cmd/helm/get_test.go index 0ee6ff356..d487dc40a 100644 --- a/cmd/helm/get_test.go +++ b/cmd/helm/get_test.go @@ -36,7 +36,7 @@ func TestGetCmd(t *testing.T) { name: "with a release", resp: releaseMock("thomas-guide"), args: []string{"thomas-guide"}, - expected: "CHART: foo-0.1.0-beta.1\nRELEASED: (.*)\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nMANIFEST:", + expected: "VERSION: 1\nRELEASED: (.*)\nCHART: foo-0.1.0-beta.1\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nMANIFEST:", }, { name: "requires release name arg", diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 1a0a8a338..0939abaf2 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -43,7 +43,8 @@ func releaseMock(name string) *release.Release { {Name: "foo.tpl", Data: []byte("Hello")}, }, }, - Config: &chart.Config{Raw: `name: "value"`}, + Config: &chart.Config{Raw: `name: "value"`}, + Version: 1, } } diff --git a/cmd/helm/list.go b/cmd/helm/list.go index ede7cb01d..0897f6b56 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -141,12 +141,13 @@ func (l *listCmd) run() 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) } return table.String() } diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index c3bec76ec..2378835c5 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -52,7 +52,7 @@ func TestListRun(t *testing.T) { }, long: true, }, - expected: "NAME \tUPDATED \tSTATUS \tCHART \natlas\t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1", + expected: "NAME \tVERSION\tUPDATED \tSTATUS \tCHART \natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\n", }, } @@ -93,7 +93,7 @@ func TestListCmd(t *testing.T) { resp: []*release.Release{ releaseMock("atlas"), }, - expected: "NAME \tUPDATED \tSTATUS \tCHART \natlas\t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1", + expected: "NAME \tVERSION\tUPDATED \tSTATUS \tCHART \natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\n", }, } diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 7be145c32..c628ff8b7 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -277,6 +277,7 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re }, Manifest: b.String(), Hooks: hooks, + Version: 1, } return rel, nil } diff --git a/pkg/proto/hapi/release/release.pb.go b/pkg/proto/hapi/release/release.pb.go index fc59fecbb..5193d1d93 100644 --- a/pkg/proto/hapi/release/release.pb.go +++ b/pkg/proto/hapi/release/release.pb.go @@ -31,6 +31,8 @@ type Release struct { 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{} } @@ -71,19 +73,20 @@ func init() { } var fileDescriptor2 = []byte{ - // 224 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0x3f, 0x4f, 0x85, 0x30, - 0x14, 0xc5, 0x83, 0xfc, 0x93, 0xab, 0x8b, 0x77, 0xd0, 0x86, 0x89, 0x38, 0x28, 0x71, 0x28, 0x89, - 0x7e, 0x03, 0x5d, 0x74, 0xed, 0xe8, 0x56, 0x49, 0x91, 0x46, 0x69, 0x09, 0xf0, 0x81, 0xfd, 0x28, - 0xd2, 0xde, 0xbe, 0x17, 0x78, 0x6f, 0xb9, 0x6d, 0xcf, 0xef, 0xe4, 0xf4, 0xb4, 0x50, 0xf6, 0x72, - 0xd4, 0xcd, 0xa4, 0x7e, 0x95, 0x9c, 0xd5, 0x61, 0xe5, 0xe3, 0x64, 0x17, 0x8b, 0xd7, 0x8e, 0xf1, - 0xa0, 0x95, 0x77, 0x3b, 0x67, 0x6f, 0xed, 0x0f, 0xd9, 0x4e, 0x80, 0x36, 0x9d, 0xdd, 0x81, 0xb6, - 0x97, 0xd3, 0xd2, 0xb4, 0xd6, 0x74, 0xfa, 0x3b, 0x80, 0xdb, 0x2d, 0x70, 0x93, 0xf4, 0xfb, 0xbf, - 0x08, 0x72, 0x41, 0x39, 0x88, 0x90, 0x18, 0x39, 0x28, 0x16, 0x55, 0x51, 0x5d, 0x08, 0xbf, 0xc7, - 0x07, 0x48, 0x5c, 0x3c, 0xbb, 0x58, 0xb5, 0xab, 0x67, 0xe4, 0xdb, 0x7e, 0xfc, 0x63, 0x25, 0xc2, - 0x73, 0x7c, 0x84, 0xd4, 0xc7, 0xb2, 0xd8, 0x1b, 0x6f, 0xc8, 0x48, 0x37, 0xbd, 0xb9, 0x29, 0x88, - 0xe3, 0x13, 0x64, 0x54, 0x8c, 0x25, 0xdb, 0xc8, 0xe0, 0xf4, 0x44, 0x04, 0x07, 0x96, 0x70, 0x39, - 0x48, 0xa3, 0x3b, 0x35, 0x2f, 0x2c, 0xf5, 0xa5, 0x8e, 0x67, 0xac, 0x21, 0x75, 0x1f, 0x32, 0xb3, - 0xac, 0x8a, 0xcf, 0x9b, 0xbd, 0xaf, 0x48, 0x90, 0xe1, 0xb5, 0xf8, 0xcc, 0x83, 0xfc, 0x95, 0xf9, - 0x47, 0xbf, 0xfc, 0x07, 0x00, 0x00, 0xff, 0xff, 0x96, 0x35, 0x9e, 0xef, 0x83, 0x01, 0x00, 0x00, + // 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, } From ebffaadba7d0ef9b79c7068b7507919e098fc7b0 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Wed, 6 Jul 2016 20:03:10 -0700 Subject: [PATCH 50/59] feat(ci): setup test coverage reports with coveralls.io --- scripts/ci.sh | 25 +++++++++++++++++-------- scripts/coverage.sh | 43 ++++++++++++++++++++++++++++--------------- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/scripts/ci.sh b/scripts/ci.sh index 7ed63df4a..04aee0b51 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -22,13 +22,22 @@ IFS=$'\n\t' HELM_ROOT="${BASH_SOURCE[0]%/*}/.." cd "$HELM_ROOT" -case "${CIRCLE_NODE_INDEX-0}" in - 0) - echo "Running 'make test-unit'" +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 - ;; - 1) - echo "Running 'make test-style'" - make test-style - ;; + 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/coverage.sh b/scripts/coverage.sh index ccb2d52fc..30553c1b9 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -16,22 +16,35 @@ 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 -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" From e64223664f347b0f0d0cd7168ebc511314580fae Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 7 Jul 2016 09:39:41 -0700 Subject: [PATCH 51/59] fix(ci): ensure godir is installed for coverage --- scripts/coverage.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 30553c1b9..4ee5ab077 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -21,6 +21,7 @@ coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX) profile="${coverdir}/cover.out" hash goveralls 2>/dev/null || go get github.com/mattn/goveralls +hash godir 2>/dev/null || go get github.com/Masterminds/godir generate_cover_data() { for d in $(godir) ; do From 73a2890277136d3bbc6eb97edd7c2ff7d01c11e2 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 6 Jul 2016 15:31:12 -0600 Subject: [PATCH 52/59] fix(engine): change template naming Template paths were relative to the chart that contained them, which meant that all templates were named 'template/SOMETHING'. This made it trivially easy to hit namespace collisions as in #933. Template path names are essentially opaque strings so this patch simply changes them to be qualified by parent chart. --- cmd/tiller/release_server_test.go | 12 ++++++------ pkg/engine/engine.go | 19 +++++++++++++++---- pkg/engine/engine_test.go | 29 ++++++++++++++++------------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 4b03caf6f..47981e640 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -134,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) } } @@ -150,8 +150,8 @@ 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)}, }, }, @@ -165,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) } @@ -177,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) } diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 737e92abd..934917701 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -19,6 +19,8 @@ package engine import ( "bytes" "fmt" + "log" + "path" "strings" "text/template" @@ -105,6 +107,7 @@ func (e *Engine) render(tpls map[string]renderable) (map[string]string, error) { } 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) @@ -137,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 } @@ -145,7 +148,7 @@ 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) { +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{} @@ -170,11 +173,19 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals char } } + 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 b11603e4f..46ec43e54 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -75,16 +75,16 @@ 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["test3"] != expect { + if out["moby/test3"] != expect { t.Errorf("Expected %q, got %q", expect, out["test3"]) } @@ -188,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)}, }, @@ -211,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"]) } @@ -220,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"}, @@ -288,19 +291,19 @@ 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]) } } @@ -340,8 +343,8 @@ func TestRenderBuiltinValues(t *testing.T) { } expects := map[string]string{ - "Lavinia": "LaviniaLatiumAeneid", - "Aeneas": "AeneasTroyAeneid", + "Troy/charts/Latium/Lavinia": "Troy/charts/Latium/LaviniaLatiumAeneid", + "Troy/Aeneas": "Troy/AeneasTroyAeneid", } for file, expect := range expects { if out[file] != expect { From 1cd9f5d5414ede41310f36090e92623f659da38b Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Wed, 6 Jul 2016 17:17:52 -0600 Subject: [PATCH 53/59] feat(helm): add 'helm get hooks'. This adds 'helm get hooks' and updates 'helm get' to return hook information. --- cmd/helm/get.go | 51 +++++++++++++++++++------ cmd/helm/get_hooks.go | 72 +++++++++++++++++++++++++++++++++++ cmd/helm/get_hooks_test.go | 43 +++++++++++++++++++++ cmd/helm/get_manifest_test.go | 43 +++++++++++++++++++++ cmd/helm/get_test.go | 37 +++++------------- cmd/helm/get_values.go | 4 +- cmd/helm/get_values_test.go | 35 ++++------------- cmd/helm/helm_test.go | 65 ++++++++++++++++++++++++++++++- 8 files changed, 280 insertions(+), 70 deletions(-) create mode 100644 cmd/helm/get_hooks.go create mode 100644 cmd/helm/get_hooks_test.go create mode 100644 cmd/helm/get_manifest_test.go diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 79be29ca8..5edba1c15 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -18,8 +18,8 @@ package main import ( "errors" - "fmt" "io" + "text/template" "time" "github.com/spf13/cobra" @@ -73,9 +73,27 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { } cmd.AddCommand(newGetValuesCmd(nil, out)) cmd.AddCommand(newGetManifestCmd(nil, out)) + cmd.AddCommand(newGetHooksCmd(nil, out)) return cmd } +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 (g *getCmd) run() error { res, err := g.client.ReleaseContent(g.release) @@ -92,14 +110,25 @@ func (g *getCmd) run() error { return err } - fmt.Fprintf(g.out, "VERSION: %v\n", res.Release.Version) - fmt.Fprintf(g.out, "RELEASED: %s\n", timeconv.Format(res.Release.Info.LastDeployed, time.ANSIC)) - fmt.Fprintf(g.out, "CHART: %s-%s\n", res.Release.Chart.Metadata.Name, res.Release.Chart.Metadata.Version) - fmt.Fprintln(g.out, "USER-SUPPLIED VALUES:") - fmt.Fprintln(g.out, res.Release.Config.Raw) - fmt.Fprintln(g.out, "COMPUTED VALUES:") - fmt.Fprintln(g.out, cfgStr) - fmt.Fprintln(g.out, "MANIFEST:") - fmt.Fprintln(g.out, res.Release.Manifest) - return nil + data := map[string]interface{}{ + "Release": res.Release, + "ComputedValues": cfgStr, + "ReleaseDate": timeconv.Format(res.Release.Info.LastDeployed, time.ANSIC), + } + return tpl(getTemplate, data, g.out) +} + +func tpl(t string, vals map[string]interface{}, out io.Writer) error { + tt, err := template.New("_").Parse(t) + if err != nil { + return err + } + return tt.Execute(out, vals) +} + +func ensureHelmClient(h helm.Interface) helm.Interface { + if h != nil { + return h + } + return helm.NewClient(helm.HelmHost(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_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 index d487dc40a..962b23c04 100644 --- a/cmd/helm/get_test.go +++ b/cmd/helm/get_test.go @@ -17,47 +17,28 @@ limitations under the License. package main import ( - "bytes" - "regexp" + "io" "testing" - "k8s.io/helm/pkg/proto/hapi/release" + "github.com/spf13/cobra" ) func TestGetCmd(t *testing.T) { - tests := []struct { - name string - args []string - resp *release.Release - expected string - err bool - }{ + tests := []releaseCase{ { - name: "with a release", + 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\nMANIFEST:", + 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: "requires release name arg", + name: "get requires release name arg", err: true, }, } - var buf bytes.Buffer - for _, tt := range tests { - c := &fakeReleaseClient{ - rels: []*release.Release{tt.resp}, - } - cmd := newGetCmd(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 %q, got %q", tt.name, tt.expected, buf.String()) - } - buf.Reset() + 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 index b62833db1..bd06e7fb2 100644 --- a/cmd/helm/get_values.go +++ b/cmd/helm/get_values.go @@ -51,9 +51,7 @@ func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { return errReleaseRequired } get.release = args[0] - if get.client == nil { - get.client = helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) - } + get.client = ensureHelmClient(get.client) return get.run() }, } diff --git a/cmd/helm/get_values_test.go b/cmd/helm/get_values_test.go index 4a28ccb24..71aef1eb9 100644 --- a/cmd/helm/get_values_test.go +++ b/cmd/helm/get_values_test.go @@ -17,46 +17,27 @@ limitations under the License. package main import ( - "bytes" + "io" "testing" - "k8s.io/helm/pkg/proto/hapi/release" + "github.com/spf13/cobra" ) func TestGetValuesCmd(t *testing.T) { - tests := []struct { - name string - args []string - resp *release.Release - expected string - err bool - }{ + tests := []releaseCase{ { - name: "with a release", + name: "get values with a release", resp: releaseMock("thomas-guide"), args: []string{"thomas-guide"}, expected: "name: \"value\"", }, { - name: "requires release name arg", + name: "get values requires release name arg", err: true, }, } - - var buf bytes.Buffer - for _, tt := range tests { - c := &fakeReleaseClient{ - rels: []*release.Release{tt.resp}, - } - cmd := newGetValuesCmd(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) - } - actual := string(bytes.TrimSpace(buf.Bytes())) - if actual != tt.expected { - t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, actual) - } - buf.Reset() + cmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command { + return newGetValuesCmd(c, out) } + runReleaseCases(t, tests, cmd) } diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 0939abaf2..05fae24ad 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -17,7 +17,13 @@ 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" @@ -25,6 +31,19 @@ import ( 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{ @@ -40,11 +59,22 @@ func releaseMock(name string) *release.Release { Version: "0.1.0-beta.1", }, Templates: []*chart.Template{ - {Name: "foo.tpl", Data: []byte("Hello")}, + {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, } } @@ -85,3 +115,36 @@ func (c *fakeReleaseClient) ReleaseContent(rlsName string, opts ...helm.ContentO } 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 +} From 71fa7ecb9be9b879914183ce251b01ea39bab728 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Sun, 10 Jul 2016 13:57:07 -0700 Subject: [PATCH 54/59] ref(cmd): remove duplicate test cases --- cmd/helm/list_test.go | 47 ------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 2378835c5..6d44173cb 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -24,53 +24,6 @@ import ( "k8s.io/helm/pkg/proto/hapi/release" ) -func TestListRun(t *testing.T) { - tests := []struct { - name string - listCmd *listCmd - expected string - err bool - }{ - { - name: "with a release", - listCmd: &listCmd{ - client: &fakeReleaseClient{ - rels: []*release.Release{ - releaseMock("thomas-guide"), - }, - }, - }, - expected: "thomas-guide", - }, - { - name: "list --long", - listCmd: &listCmd{ - client: &fakeReleaseClient{ - rels: []*release.Release{ - releaseMock("atlas"), - }, - }, - long: true, - }, - 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 { - tt.listCmd.out = &buf - err := tt.listCmd.run() - 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() - } -} - func TestListCmd(t *testing.T) { tests := []struct { name string From 561e4206598cb2b3b31f9c6cda890fbc57cdfc7d Mon Sep 17 00:00:00 2001 From: Michael Stapelberg Date: Mon, 11 Jul 2016 21:50:07 +0100 Subject: [PATCH 55/59] Add reference to text/template Go package doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …since people might not be familiar with Go templates. --- docs/charts.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/charts.md b/docs/charts.md index d62f82603..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 From a14e76a65c4ebf0bc3781203935e888892152be6 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Mon, 11 Jul 2016 22:59:55 -0600 Subject: [PATCH 56/59] chore(*): add canonical import path annotation https://golang.org/doc/go1.4#canonicalimports --- cmd/helm/helm.go | 2 +- cmd/tiller/tiller.go | 2 +- pkg/chartutil/doc.go | 2 +- pkg/client/install.go | 2 +- pkg/engine/doc.go | 2 +- pkg/helm/client.go | 2 +- pkg/ignore/doc.go | 2 +- pkg/kube/client.go | 2 +- pkg/lint/lint.go | 2 +- pkg/lint/rules/chartfile.go | 2 +- pkg/lint/support/doc.go | 2 +- pkg/repo/repo.go | 2 +- pkg/storage/doc.go | 2 +- pkg/timeconv/doc.go | 2 +- pkg/version/version.go | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index e2a39c48b..00cd3937e 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.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/helm" import ( "errors" 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/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/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/helm/client.go b/pkg/helm/client.go index 91fd1923c..a815cf5f6 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" 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 7618f89c4..f589cc75a 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -14,7 +14,7 @@ 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" diff --git a/pkg/lint/lint.go b/pkg/lint/lint.go index ea723b2f0..7903d215c 100644 --- a/pkg/lint/lint.go +++ b/pkg/lint/lint.go @@ -14,7 +14,7 @@ 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" diff --git a/pkg/lint/rules/chartfile.go b/pkg/lint/rules/chartfile.go index b8b0e2d10..8763af0de 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/lint/rules/chartfile.go @@ -14,7 +14,7 @@ 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" diff --git a/pkg/lint/support/doc.go b/pkg/lint/support/doc.go index c843f6468..4cf7272e4 100644 --- a/pkg/lint/support/doc.go +++ b/pkg/lint/support/doc.go @@ -19,4 +19,4 @@ limitations under the License. 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/repo/repo.go b/pkg/repo/repo.go index b113629f2..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" 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. From 4dd95addd24fbcf02442c3d4590050b8aef09440 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 14 Jul 2016 11:30:11 -0700 Subject: [PATCH 57/59] ref(cmd): refactor create cmd --- cmd/helm/create.go | 42 +++++++++++++++++++++++++++--------------- cmd/helm/helm.go | 8 +++++--- 2 files changed, 32 insertions(+), 18 deletions(-) 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/helm.go b/cmd/helm/helm.go index 00cd3937e..bbc271456 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -85,9 +85,11 @@ func newRootCmd(out io.Writer) *cobra.Command { p.StringVarP(&tillerNamespace, "namespace", "", "", "kubernetes namespace") p.BoolVarP(&flagDebug, "debug", "", false, "enable verbose output") - cmd.AddCommand(newListCmd(nil, out)) - cmd.AddCommand(newGetCmd(nil, out)) - + cmd.AddCommand( + newCreateCmd(out), + newGetCmd(nil, out), + newListCmd(nil, out), + ) return cmd } From 736009982e6ced47eb54399db6efdf0ab4ba2f9e Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Thu, 14 Jul 2016 13:00:41 -0700 Subject: [PATCH 58/59] ref(helm): fix naming issues from golint --- cmd/helm/get.go | 4 ++-- cmd/helm/get_manifest.go | 2 +- cmd/helm/list.go | 2 +- pkg/helm/client.go | 4 ++-- pkg/helm/compat.go | 12 ++++++------ pkg/helm/option.go | 8 ++++---- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 5edba1c15..954b76407 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -66,7 +66,7 @@ func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { } get.release = args[0] if get.client == nil { - get.client = helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) + get.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) } return get.run() }, @@ -130,5 +130,5 @@ func ensureHelmClient(h helm.Interface) helm.Interface { if h != nil { return h } - return helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) + return helm.NewClient(helm.Host(helm.Config.ServAddr)) } diff --git a/cmd/helm/get_manifest.go b/cmd/helm/get_manifest.go index 20f61443e..f3b9679bd 100644 --- a/cmd/helm/get_manifest.go +++ b/cmd/helm/get_manifest.go @@ -54,7 +54,7 @@ func newGetManifestCmd(client helm.Interface, out io.Writer) *cobra.Command { } get.release = args[0] if get.client == nil { - get.client = helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) + get.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) } return get.run() }, diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 0897f6b56..7314a4a22 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -80,7 +80,7 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { list.filter = strings.Join(args, " ") } if list.client == nil { - list.client = helm.NewClient(helm.HelmHost(helm.Config.ServAddr)) + list.client = helm.NewClient(helm.Host(helm.Config.ServAddr)) } return list.run() }, diff --git a/pkg/helm/client.go b/pkg/helm/client.go index a815cf5f6..b1efcac2d 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -57,8 +57,8 @@ func (h *Client) Option(opts ...Option) *Client { // 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. diff --git a/pkg/helm/compat.go b/pkg/helm/compat.go index 5e3088107..d398f83ae 100644 --- a/pkg/helm/compat.go +++ b/pkg/helm/compat.go @@ -39,32 +39,32 @@ 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()) } @@ -74,7 +74,7 @@ func InstallRelease(vals []byte, rlsName, chStr string, dryRun bool) (*rls.Insta // 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/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 } From 0ceeb19deda23cb188a8bb1ddc916f33ff97c197 Mon Sep 17 00:00:00 2001 From: Adam Reese Date: Fri, 1 Jul 2016 12:25:59 -0700 Subject: [PATCH 59/59] ref(cmd): refactor status cmd --- cmd/helm/helm.go | 1 + cmd/helm/status.go | 50 +++++++++++++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index bbc271456..7a9ab74fc 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -89,6 +89,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newCreateCmd(out), newGetCmd(nil, out), newListCmd(nil, out), + newStatusCmd(nil, out), ) return cmd } 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 }