diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..273643b7c --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,25 @@ +version: 2 +jobs: + build: + working_directory: /go/src/k8s.io/helm + parallelism: 3 + docker: + - image: golang:1.8 + environment: + PROJECT_NAME: "kubernetes-helm" + steps: + - checkout + - setup_remote_docker + - run: + name: install dependencies + command: make bootstrap + - save_cache: + key: vendor-{{ checksum "glide.yaml" }}-{{ checksum "glide.lock" }} + paths: + - vendor + - run: + name: test + command: .circleci/test.sh + - deploy: + name: deploy + command: .circleci/deploy.sh diff --git a/scripts/ci/deploy.sh b/.circleci/deploy.sh similarity index 67% rename from scripts/ci/deploy.sh rename to .circleci/deploy.sh index 6f8199a18..84bcfa33b 100755 --- a/scripts/ci/deploy.sh +++ b/.circleci/deploy.sh @@ -15,6 +15,11 @@ # limitations under the License. set -euo pipefail +# Skip on pull request builds +if [[ -n "${CIRCLE_PR_NUMBER:-}" ]]; then + exit +fi + : ${GCLOUD_SERVICE_KEY:?"GCLOUD_SERVICE_KEY environment variable is not set"} : ${PROJECT_NAME:?"PROJECT_NAME environment variable is not set"} @@ -27,13 +32,21 @@ else exit 1 fi -echo "Updating gcloud components" -sudo /opt/google-cloud-sdk/bin/gcloud --quiet components update +echo "Install docker client" +VER="17.03.0-ce" +curl -L -o /tmp/docker-$VER.tgz https://get.docker.com/builds/Linux/x86_64/docker-$VER.tgz +tar -xz -C /tmp -f /tmp/docker-$VER.tgz +mv /tmp/docker/* /usr/bin + +echo "Install gcloud components" +export CLOUDSDK_CORE_DISABLE_PROMPTS=1 +curl https://sdk.cloud.google.com | bash +${HOME}/google-cloud-sdk/bin/gcloud --quiet components update echo "Configuring gcloud authentication" echo "${GCLOUD_SERVICE_KEY}" | base64 --decode > "${HOME}/gcloud-service-key.json" -sudo /opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file "${HOME}/gcloud-service-key.json" -sudo /opt/google-cloud-sdk/bin/gcloud config set project "${PROJECT_NAME}" +${HOME}/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file "${HOME}/gcloud-service-key.json" +${HOME}/google-cloud-sdk/bin/gcloud config set project "${PROJECT_NAME}" docker login -e 1234@5678.com -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io echo "Building the tiller image" @@ -47,4 +60,4 @@ make build-cross make dist checksum VERSION="${VERSION}" echo "Pushing binaries to gs bucket" -sudo /opt/google-cloud-sdk/bin/gsutil cp ./_dist/* "gs://${PROJECT_NAME}" +${HOME}/google-cloud-sdk/bin/gsutil cp ./_dist/* "gs://${PROJECT_NAME}" diff --git a/scripts/ci.sh b/.circleci/test.sh similarity index 100% rename from scripts/ci.sh rename to .circleci/test.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7005d4f4b..c98d49a05 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,7 @@ Whether you are a user or contributor, official support channels include: Before opening a new issue or submitting a new pull request, it's helpful to search the project - it's likely that another user has already reported the issue you're facing, or it's a known issue that we're already aware of. ## Milestones + We use milestones to track progress of releases. There are also 2 special milestones used for helping us keep work organized: `Upcoming - Minor` and `Upcoming - Major` @@ -52,10 +53,27 @@ An issue that we are not sure we will be doing will not be added to any mileston A milestone (and hence release) is considered done when all outstanding issues/PRs have been closed or moved to another milestone. +## Semver + +Helm maintains a strong commitment to backward compatibility. All of our changes to protocols and formats are backward compatible from Helm 2.0 until Helm 3.0. No features, flags, or commands are removed or substantially modified (other than bug fixes). + +We also try very hard to not change publicly accessible Go library definitions inside of the `pkg/` directory of our source code. + +For a quick summary of our backward compatibility guidelines for releases between 2.0 and 3.0: + +- Protobuf and gRPC changes MUST be backward compatible. +- Command line commands, flags, and arguments MUST be backward compatible +- File formats (such as Chart.yaml, repositories.yaml, and requirements.yaml) MUST be backward compatible +- Any chart that worked on a previous version of Helm MUST work on a new version of Helm (barring the cases where (a) Kubernetes itself changed, and (b) the chart worked because it exploited a bug) +- Chart repository functionality MUST be backward compatible +- Go libraries inside of `pkg/` SHOULD remain backward compatible (though code inside of `cmd/` may be changed from release to release without notice). + ## Issues + Issues are used as the primary method for tracking anything to do with the Helm project. ### Issue Types + There are 4 types of issues (each with their own corresponding [label](#labels)): - Question: These are support or functionality inquiries that we want to have a record of for future reference. Generally these are questions that are too complex or large to store in the @@ -72,6 +90,7 @@ from a "Proposal" or can be submitted individually depending on the size. - Bugs: These track bugs with the code or problems with the documentation (i.e. missing or incomplete) ### Issue Lifecycle + The issue lifecycle is mainly driven by the core maintainers, but is good information for those contributing to Helm. All issue types follow the same general lifecycle. Differences are noted below. 1. Issue creation @@ -107,9 +126,11 @@ https://github.com/kubernetes/helm/blob/master/docs/developers.md The next section contains more information on the workflow followed for PRs ## Pull Requests + Like any good open source project, we use Pull Requests to track code changes ### PR Lifecycle + 1. PR creation - We more than welcome PRs that are currently in progress. They are a great way to keep track of important work that is in-flight, but useful for others to see. If a PR is a work in progress, @@ -149,19 +170,23 @@ Like any good open source project, we use Pull Requests to track code changes merge the PR once it is approved. #### Documentation PRs + Documentation PRs will follow the same lifecycle as other PRs. They will also be labeled with the `docs` label. For documentation, special attention will be paid to spelling, grammar, and clarity (whereas those things don't matter *as* much for comments in code). ## The Triager + Each week, one of the core maintainers will serve as the designated "triager" starting after the public standup meetings on Thursday. This person will be in charge triaging new PRs and issues throughout the work week. ## Labels + The following tables define all label types used for Helm. It is split up by category. ### Common + | Label | Description | | ----- | ----------- | | `bug` | Marks an issue as a bug or a PR as a bugfix | @@ -173,6 +198,7 @@ The following tables define all label types used for Helm. It is split up by cat | `refactor` | Indicates that the issue is a code refactor and is not fixing a bug or adding additional functionality | ### Issue Specific + | Label | Description | | ----- | ----------- | | `help wanted` | This issue is one the core maintainers cannot get to right now and would appreciate help with | @@ -182,6 +208,7 @@ The following tables define all label types used for Helm. It is split up by cat | `wont fix` | The issue has been discussed and will not be implemented (or accepted in the case of a proposal) | ### PR Specific + | Label | Description | | ----- | ----------- | | `awaiting review` | The PR has been triaged and is ready for someone to review | @@ -194,6 +221,7 @@ The following tables define all label types used for Helm. It is split up by cat | `picked` | This PR has been picked into a feature branch | #### Size labels + Size labels are used to indicate how "dangerous" a PR is. The guidelines below are used to assign the labels, but ultimately this can be changed by the maintainers. For example, even if a PR only makes 30 lines of changes in 1 file, but it changes key functionality, it will likely be labeled as `size/large` diff --git a/Makefile b/Makefile index 2070fd4d0..737be298c 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ build: .PHONY: build-cross build-cross: LDFLAGS += -extldflags "-static" build-cross: - CGO_ENABLED=0 gox -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/$(APP) + CGO_ENABLED=0 gox -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/$(APP) .PHONY: dist dist: diff --git a/README.md b/README.md index 3d08a41f9..21a009e2c 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,10 @@ Think of it like apt/yum/homebrew for Kubernetes. Binary downloads of the Helm client can be found at the following links: -- [OSX](https://kubernetes-helm.storage.googleapis.com/helm-v2.6.0-darwin-amd64.tar.gz) -- [Linux](https://kubernetes-helm.storage.googleapis.com/helm-v2.6.0-linux-amd64.tar.gz) -- [Linux 32-bit](https://kubernetes-helm.storage.googleapis.com/helm-v2.6.0-linux-386.tar.gz) +- [OSX](https://kubernetes-helm.storage.googleapis.com/helm-v2.6.1-darwin-amd64.tar.gz) +- [Linux](https://kubernetes-helm.storage.googleapis.com/helm-v2.6.1-linux-amd64.tar.gz) +- [Linux 32-bit](https://kubernetes-helm.storage.googleapis.com/helm-v2.6.1-linux-386.tar.gz) +- [Windows](https://kubernetes-helm.storage.googleapis.com/helm-v2.6.1-windows-amd64.tar.gz) Unpack the `helm` binary and add it to your PATH and you are good to go! macOS/[homebrew](https://brew.sh/) users can also use `brew install kubernetes-helm`. diff --git a/_proto/hapi/release/hook.proto b/_proto/hapi/release/hook.proto index 2237883ca..22c1fedef 100644 --- a/_proto/hapi/release/hook.proto +++ b/_proto/hapi/release/hook.proto @@ -35,6 +35,10 @@ message Hook { RELEASE_TEST_SUCCESS = 9; RELEASE_TEST_FAILURE = 10; } + enum DeletePolicy { + SUCCEEDED = 0; + FAILED = 1; + } string name = 1; // Kind is the Kubernetes kind. string kind = 2; @@ -48,4 +52,6 @@ message Hook { google.protobuf.Timestamp last_run = 6; // Weight indicates the sort order for execution among similar Hook type int32 weight = 7; + // DeletePolicies are the policies that indicate when to delete the hook + repeated DeletePolicy delete_policies = 8; } diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 5b30c8be4..000000000 --- a/circle.yml +++ /dev/null @@ -1,52 +0,0 @@ -machine: - pre: - - curl -sSL https://s3.amazonaws.com/circle-downloads/install-circleci-docker.sh | bash -s -- 1.10.0 - - environment: - GOVERSION: "1.8.3" - GOPATH: "${HOME}/.go_workspace" - WORKDIR: "${GOPATH}/src/k8s.io/helm" - PROJECT_NAME: "kubernetes-helm" - - services: - - docker - -dependencies: - cache_directories: - - "~/.glide" - - pre: - # remove old go files - - sudo rm -rf /usr/local/go - - rm -rf "$GOPATH" - - override: - # install go - - wget "https://storage.googleapis.com/golang/go${GOVERSION}.linux-amd64.tar.gz" -O "${HOME}/go${GOVERSION}.tar.gz" - - sudo tar -C /usr/local -xzf "${HOME}/go${GOVERSION}.tar.gz" - - # move repository to the canonical import path - - mkdir -p "$(dirname ${WORKDIR})" - - cp -R "${HOME}/helm" "${WORKDIR}" - - # install dependencies - - cd "${WORKDIR}" && make bootstrap - - post: - - go env - -test: - override: - - cd "${WORKDIR}" && ./scripts/ci.sh: - parallel: true - -deployment: - release: - tag: /.*/ - commands: - - cd "${WORKDIR}" && ./scripts/ci/deploy.sh - - canary: - branch: master - commands: - - cd "${WORKDIR}" && ./scripts/ci/deploy.sh diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index bc8885b4b..6fe08b07e 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -69,7 +69,7 @@ Environment: $HELM_HOME set an alternative location for Helm files. By default, these are stored in ~/.helm $HELM_HOST set an alternative Tiller host. The format is host:port $HELM_NO_PLUGINS disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. - $TILLER_NAMESPACE set an alternative Tiller namespace (default "kube-namespace") + $TILLER_NAMESPACE set an alternative Tiller namespace (default "kube-system") $KUBECONFIG set an alternative Kubernetes configuration file (default "~/.kube/config") ` @@ -125,6 +125,7 @@ func newRootCmd(args []string) *cobra.Command { newHomeCmd(out), newInitCmd(out), newPluginCmd(out), + newTemplateCmd(out), // Hidden documentation generator command: 'helm docs' newDocsCmd(out), diff --git a/cmd/helm/init.go b/cmd/helm/init.go index 16065614d..28a21f811 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -299,7 +299,7 @@ func ensureDefaultRepos(home helmpath.Home, out io.Writer, skipRefresh bool) err if err != nil { return err } - lr, err := initLocalRepo(home.LocalRepository(localRepositoryIndexFile), home.CacheIndex("local")) + lr, err := initLocalRepo(home.LocalRepository(localRepositoryIndexFile), home.CacheIndex("local"), out) if err != nil { return err } @@ -314,7 +314,9 @@ func ensureDefaultRepos(home helmpath.Home, out io.Writer, skipRefresh bool) err return nil } -func initStableRepo(cacheFile string, skipRefresh bool, home helmpath.Home) (*repo.Entry, error) { + +func initStableRepo(cacheFile string, out io.Writer, skipRefresh bool, home helmPath.Home) (*repo.Entry, error) { + fmt.Fprintf(out, "Adding %s repo with URL: %s \n", stableRepository, stableRepositoryURL) c := repo.Entry{ Name: stableRepository, URL: stableRepositoryURL, @@ -338,8 +340,9 @@ func initStableRepo(cacheFile string, skipRefresh bool, home helmpath.Home) (*re return &c, nil } -func initLocalRepo(indexFile, cacheFile string) (*repo.Entry, error) { +func initLocalRepo(indexFile, cacheFile string, out io.Writer) (*repo.Entry, error) { if fi, err := os.Stat(indexFile); err != nil { + fmt.Fprintf(out, "Adding %s repo with URL: %s \n", localRepository, localRepositoryURL) i := repo.NewIndexFile() if err := i.WriteFile(indexFile, 0644); err != nil { return nil, err diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 7e59333b6..a9d308908 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -177,7 +177,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { f := cmd.Flags() f.VarP(&inst.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)") f.StringVarP(&inst.name, "name", "n", "", "release name. If unspecified, it will autogenerate one for you") - f.StringVar(&inst.namespace, "namespace", "", "namespace to install the release into") + f.StringVar(&inst.namespace, "namespace", "", "namespace to install the release into. Defaults to the current kube config namespace.") f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install") f.BoolVar(&inst.disableHooks, "no-hooks", false, "prevent hooks from running during install") f.BoolVar(&inst.replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production") diff --git a/cmd/helm/template.go b/cmd/helm/template.go new file mode 100644 index 000000000..88ca92fc9 --- /dev/null +++ b/cmd/helm/template.go @@ -0,0 +1,236 @@ +/* +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 ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/spf13/cobra" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/engine" + "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/helm/pkg/proto/hapi/release" + util "k8s.io/helm/pkg/releaseutil" + "k8s.io/helm/pkg/tiller" + "k8s.io/helm/pkg/timeconv" +) + +const templateDesc = ` +Render chart templates locally and display the output. + +This does not require Tiller. However, any values that would normally be +looked up or retrieved in-cluster will be faked locally. Additionally, none +of the server-side testing of chart validity (e.g. whether an API is supported) +is done. + +To render just one template in a chart, use '-x': + + $ helm template mychart -x templates/deployment.yaml +` + +type templateCmd struct { + namespace string + valueFiles valueFiles + chartPath string + out io.Writer + client helm.Interface + values []string + nameTemplate string + showNotes bool + releaseName string + renderFiles []string +} + +func newTemplateCmd(out io.Writer) *cobra.Command { + + t := &templateCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "template [flags] CHART", + Short: fmt.Sprintf("locally render templates"), + Long: templateDesc, + RunE: t.run, + } + + f := cmd.Flags() + f.BoolVar(&t.showNotes, "notes", false, "show the computed NOTES.txt file as well") + f.StringVarP(&t.releaseName, "name", "n", "RELEASE-NAME", "release name") + f.StringArrayVarP(&t.renderFiles, "execute", "x", []string{}, "only execute the given templates") + f.VarP(&t.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)") + f.StringVar(&t.namespace, "namespace", "", "namespace to install the release into") + f.StringArrayVar(&t.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringVar(&t.nameTemplate, "name-template", "", "specify template used to name the release") + + return cmd +} + +func (t *templateCmd) run(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("chart is required") + } + // verify chart path exists + if _, err := os.Stat(args[0]); err == nil { + if t.chartPath, err = filepath.Abs(args[0]); err != nil { + return err + } + } else { + return err + } + // verify specified templates exist relative to chart + rf := []string{} + var af string + var err error + if len(t.renderFiles) > 0 { + for _, f := range t.renderFiles { + if !filepath.IsAbs(f) { + af, err = filepath.Abs(t.chartPath + "/" + f) + if err != nil { + return fmt.Errorf("could not resolve template path: %s", err) + } + } else { + af = f + } + rf = append(rf, af) + + if _, err := os.Stat(af); err != nil { + return fmt.Errorf("could not resolve template path: %s", err) + } + } + } + + if t.namespace == "" { + t.namespace = defaultNamespace() + } + // get combined values and create config + rawVals, err := vals(t.valueFiles, t.values) + if err != nil { + return err + } + config := &chart.Config{Raw: string(rawVals), Values: map[string]*chart.Value{}} + + // If template is specified, try to run the template. + if t.nameTemplate != "" { + t.releaseName, err = generateName(t.nameTemplate) + if err != nil { + return err + } + } + + // Check chart requirements to make sure all dependencies are present in /charts + c, err := chartutil.Load(t.chartPath) + if err != nil { + return prettyError(err) + } + + if req, err := chartutil.LoadRequirements(c); err == nil { + if err := checkDependencies(c, req); err != nil { + return prettyError(err) + } + } else if err != chartutil.ErrRequirementsNotFound { + return fmt.Errorf("cannot load requirements: %v", err) + } + options := chartutil.ReleaseOptions{ + Name: t.releaseName, + Time: timeconv.Now(), + Namespace: t.namespace, + } + + err = chartutil.ProcessRequirementsEnabled(c, config) + if err != nil { + return err + } + err = chartutil.ProcessRequirementsImportValues(c) + if err != nil { + return err + } + + // Set up engine. + renderer := engine.New() + + vals, err := chartutil.ToRenderValues(c, config, options) + if err != nil { + return err + } + + out, err := renderer.Render(c, vals) + listManifests := []tiller.Manifest{} + if err != nil { + return err + } + // extract kind and name + re := regexp.MustCompile("kind:(.*)\n") + for k, v := range out { + match := re.FindStringSubmatch(v) + h := "Unknown" + if len(match) == 2 { + h = strings.TrimSpace(match[1]) + } + m := tiller.Manifest{Name: k, Content: v, Head: &util.SimpleHead{Kind: h}} + listManifests = append(listManifests, m) + } + in := func(needle string, haystack []string) bool { + // make needle path absolute + d := strings.Split(needle, "/") + dd := d[1:] + an := t.chartPath + "/" + strings.Join(dd, "/") + + for _, h := range haystack { + if h == an { + return true + } + } + return false + } + if settings.Debug { + rel := &release.Release{ + Name: t.releaseName, + Chart: c, + Config: config, + Version: 1, + Namespace: t.namespace, + Info: &release.Info{LastDeployed: timeconv.Timestamp(time.Now())}, + } + printRelease(os.Stdout, rel) + } + + for _, m := range tiller.SortByKind(listManifests) { + if len(t.renderFiles) > 0 && in(m.Name, rf) == false { + continue + } + data := m.Content + b := filepath.Base(m.Name) + if !t.showNotes && b == "NOTES.txt" { + continue + } + if strings.HasPrefix(b, "_") { + continue + } + fmt.Printf("---\n# Source: %s\n", m.Name) + fmt.Println(data) + } + return nil +} diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go new file mode 100644 index 000000000..b1e080493 --- /dev/null +++ b/cmd/helm/template_test.go @@ -0,0 +1,160 @@ +/* +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 ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" +) + +var chartPath = "./../../pkg/chartutil/testdata/subpop/charts/subchart1" + +func TestTemplateCmd(t *testing.T) { + absChartPath, err := filepath.Abs(chartPath) + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + desc string + args []string + expectKey string + expectValue string + }{ + { + name: "check_name", + desc: "check for a known name in chart", + args: []string{chartPath}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "protocol: TCP\n name: nginx", + }, + { + name: "check_set_name", + desc: "verify --set values exist", + args: []string{chartPath, "-x", "templates/service.yaml", "--set", "service.name=apache"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "protocol: TCP\n name: apache", + }, + { + name: "check_execute", + desc: "verify --execute single template", + args: []string{chartPath, "-x", "templates/service.yaml", "--set", "service.name=apache"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "protocol: TCP\n name: apache", + }, + { + name: "check_execute_absolute", + desc: "verify --execute single template", + args: []string{chartPath, "-x", absChartPath + "/" + "templates/service.yaml", "--set", "service.name=apache"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "protocol: TCP\n name: apache", + }, + { + name: "check_namespace", + desc: "verify --namespace", + args: []string{chartPath, "--namespace", "test"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "namespace: \"test\"", + }, + { + name: "check_release_name", + desc: "verify --release exists", + args: []string{chartPath, "--name", "test"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "release-name: \"test\"", + }, + { + name: "check_notes", + desc: "verify --notes shows notes", + args: []string{chartPath, "--notes", "true"}, + expectKey: "subchart1/templates/NOTES.txt", + expectValue: "Sample notes for subchart1", + }, + { + name: "check_values_files", + desc: "verify --values files values exist", + args: []string{chartPath, "--values", chartPath + "/charts/subchartA/values.yaml"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "name: apache", + }, + { + name: "check_name_template", + desc: "verify --name-template result exists", + args: []string{chartPath, "--name-template", "foobar-{{ b64enc \"abc\" }}-baz"}, + expectKey: "subchart1/templates/service.yaml", + expectValue: "release-name: \"foobar-YWJj-baz\"", + }, + } + + var buf bytes.Buffer + for _, tt := range tests { + t.Run(tt.name, func(T *testing.T) { + // capture stdout + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + // execute template command + out := bytes.NewBuffer(nil) + cmd := newTemplateCmd(out) + cmd.SetArgs(tt.args) + err := cmd.Execute() + if err != nil { + t.Errorf("expected: %v, got %v", tt.expectValue, err) + } + // restore stdout + w.Close() + os.Stdout = old + var b bytes.Buffer + io.Copy(&b, r) + r.Close() + // scan yaml into map[]yaml + scanner := bufio.NewScanner(&b) + next := false + lastKey := "" + m := map[string]string{} + for scanner.Scan() { + if scanner.Text() == "---" { + next = true + } else if next { + // remove '# Source: ' + head := "# Source: " + lastKey = scanner.Text()[len(head):] + next = false + } else { + m[lastKey] = m[lastKey] + scanner.Text() + "\n" + } + } + if err := scanner.Err(); err != nil { + fmt.Fprintln(os.Stderr, "reading standard input:", err) + } + if v, ok := m[tt.expectKey]; ok { + if strings.Contains(v, tt.expectValue) == false { + t.Errorf("failed to match expected value %s in %s", tt.expectValue, v) + } + } else { + t.Errorf("could not find key %s", tt.expectKey) + } + buf.Reset() + }) + } +} diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 989d73cdd..4b852198a 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -121,7 +121,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { f.BoolVar(&upgrade.verify, "verify", false, "verify the provenance of the chart before upgrading") f.StringVar(&upgrade.keyring, "keyring", defaultKeyring(), "path to the keyring that contains public signing keys") f.BoolVarP(&upgrade.install, "install", "i", false, "if a release by this name doesn't already exist, run an install") - f.StringVar(&upgrade.namespace, "namespace", "default", "namespace to install the release into (only used if --install is set)") + f.StringVar(&upgrade.namespace, "namespace", "", "namespace to install the release into (only used if --install is set). Defaults to the current kube config namespace") f.StringVar(&upgrade.version, "version", "", "specify the exact chart version to use. If this is not specified, the latest version is used") f.Int64Var(&upgrade.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") f.BoolVar(&upgrade.resetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart") diff --git a/docs/chart_template_guide/functions_and_pipelines.md b/docs/chart_template_guide/functions_and_pipelines.md index 7eb1ca26c..54eb8e24f 100644 --- a/docs/chart_template_guide/functions_and_pipelines.md +++ b/docs/chart_template_guide/functions_and_pipelines.md @@ -138,7 +138,13 @@ data: food: "PIZZA" ``` -It is considered good (almost mandatory) practice to set defaults with `default` for any object that originates from `.Values`. (In some places, an `if` conditional guard may be better suited. We'll see those in the next section.) +In an actual chart, all static default values should live in the values.yaml, and should not be repeated using the `default` command (otherwise they would be redundant). However, the `default` command is perfect for computed values, which can not be declared inside values.yaml. For example: + +```yaml +drink: {{ .Values.favorite.drink | default (printf "%s-tea" (include "fullname" .)) }} +``` + +In some places, an `if` conditional guard may be better suited than `default`. We'll see those in the next section. Template functions and pipelines are a powerful way to transform information and then insert it into your YAML. But sometimes it's necessary to add some template logic that is a little more sophisticated than just inserting a string. In the next section we will look at the control structures provided by the template language. diff --git a/docs/charts_hooks.md b/docs/charts_hooks.md index 6ac9bc2b0..92c50e20c 100644 --- a/docs/charts_hooks.md +++ b/docs/charts_hooks.md @@ -87,7 +87,7 @@ in the future.) It is considered good practice to add a hook weight, and set it to `0` if weight is not important. -### Hook resources are unmanaged +### Hook resources are not managed with correponding releases The resources that a hook creates are not tracked or managed as part of the release. Once Tiller verifies that the hook has reached its ready state, it @@ -95,8 +95,8 @@ will leave the hook resource alone. Practically speaking, this means that if you create resources in a hook, you cannot rely upon `helm delete` to remove the resources. To destroy such -resources, you need to write code to perform this operation in a `pre-delete` -or `post-delete` hook. +resources, you need to either write code to perform this operation in a `pre-delete` +or `post-delete` hook or add `"helm.sh/hook-delete-policy"` annotation to the hook template file. ## Writing a Hook @@ -122,6 +122,7 @@ metadata: # job is considered part of the release. "helm.sh/hook": post-install "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded spec: template: metadata: @@ -160,7 +161,7 @@ and a config map as a pre-install hook. When subcharts declare hooks, those are also evaluated. There is no way for a top-level chart to disable the hooks declared by subcharts. -It is also possible to define a weight for a hook which will help build a +It is possible to define a weight for a hook which will help build a deterministic executing order. Weights are defined using the following annotation: ``` @@ -172,3 +173,12 @@ Hook weights can be positive or negative numbers but must be represented as strings. When Tiller starts the execution cycle of hooks of a particular Kind it will sort those hooks in ascending order. +It is also possible to define policies that determine when to delete corresponding hook resources. Hook deletion policies are defined using the following annotation: + +``` + annotations: + "helm.sh/hook-delete-policy": hook-succeeded +``` + +When using `"helm.sh/hook-delete-policy"` annoation, you can choose its value from `"hook-succeeded"` and `"hook-failed"`. The value `"hook-succeeded"` specifies Tiller should delete the hook after the hook is successfully excuted, while the value `"hook-failed"`specifies Tiller should delete the hook if the hook is failed during execuation. + diff --git a/docs/helm/helm.md b/docs/helm/helm.md index 2b15a3b12..36e41835e 100644 --- a/docs/helm/helm.md +++ b/docs/helm/helm.md @@ -25,7 +25,7 @@ Environment: $HELM_HOME set an alternative location for Helm files. By default, these are stored in ~/.helm $HELM_HOST set an alternative Tiller host. The format is host:port $HELM_NO_PLUGINS disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. - $TILLER_NAMESPACE set an alternative Tiller namespace (default "kube-namespace") + $TILLER_NAMESPACE set an alternative Tiller namespace (default "kube-system") $KUBECONFIG set an alternative Kubernetes configuration file (default "~/.kube/config") @@ -61,9 +61,10 @@ Environment: * [helm search](helm_search.md) - search for a keyword in charts * [helm serve](helm_serve.md) - start a local http web server * [helm status](helm_status.md) - displays the status of the named release +* [helm template](helm_template.md) - locally render templates * [helm test](helm_test.md) - test a release * [helm upgrade](helm_upgrade.md) - upgrade a release * [helm verify](helm_verify.md) - verify that a chart at the given path has been signed and is valid * [helm version](helm_version.md) - print the client/server version information -###### Auto generated by spf13/cobra on 23-Jun-2017 +###### Auto generated by spf13/cobra on 8-Aug-2017 diff --git a/docs/helm/helm_install.md b/docs/helm/helm_install.md index 0a4cb48e8..8858f534d 100644 --- a/docs/helm/helm_install.md +++ b/docs/helm/helm_install.md @@ -76,7 +76,7 @@ helm install [CHART] --keyring string location of public keys used for verification (default "~/.gnupg/pubring.gpg") -n, --name string release name. If unspecified, it will autogenerate one for you --name-template string specify template used to name the release - --namespace string namespace to install the release into + --namespace string namespace to install the release into. Defaults to the current kube config namespace. --no-hooks prevent hooks from running during install --replace re-use the given name, even if that name is already used. This is unsafe in production --repo string chart repository url where to locate the requested chart @@ -106,4 +106,4 @@ helm install [CHART] ### SEE ALSO * [helm](helm.md) - The Helm package manager for Kubernetes. -###### Auto generated by spf13/cobra on 23-Jun-2017 +###### Auto generated by spf13/cobra on 15-Aug-2017 diff --git a/docs/helm/helm_template.md b/docs/helm/helm_template.md new file mode 100644 index 000000000..80ecdec39 --- /dev/null +++ b/docs/helm/helm_template.md @@ -0,0 +1,50 @@ +## helm template + +locally render templates + +### Synopsis + + + +Render chart templates locally and display the output. + +This does not require Tiller. However, any values that would normally be +looked up or retrieved in-cluster will be faked locally. Additionally, none +of the server-side testing of chart validity (e.g. whether an API is supported) +is done. + +To render just one template in a chart, use '-x': + + $ helm template mychart -x templates/deployment.yaml + + +``` +helm template [flags] CHART +``` + +### Options + +``` + -x, --execute stringArray only execute the given templates + -n, --name string release name (default "RELEASE-NAME") + --name-template string specify template used to name the release + --namespace string namespace to install the release into + --notes show the computed NOTES.txt file as well + --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + -f, --values valueFiles specify values in a YAML file (can specify multiple) (default []) +``` + +### Options inherited from parent commands + +``` + --debug enable verbose output + --home string location of your Helm config. Overrides $HELM_HOME (default "$HOME/.helm") + --host string address of Tiller. Overrides $HELM_HOST + --kube-context string name of the kubeconfig context to use + --tiller-namespace string namespace of Tiller (default "kube-system") +``` + +### SEE ALSO +* [helm](helm.md) - The Helm package manager for Kubernetes. + +###### Auto generated by spf13/cobra on 24-Aug-2017 diff --git a/docs/helm/helm_upgrade.md b/docs/helm/helm_upgrade.md index 934cadd13..fdf95854f 100644 --- a/docs/helm/helm_upgrade.md +++ b/docs/helm/helm_upgrade.md @@ -44,7 +44,7 @@ helm upgrade [RELEASE] [CHART] -i, --install if a release by this name doesn't already exist, run an install --key-file string identify HTTPS client using this SSL key file --keyring string path to the keyring that contains public signing keys (default "~/.gnupg/pubring.gpg") - --namespace string namespace to install the release into (only used if --install is set) (default "default") + --namespace string namespace to install the release into (only used if --install is set). Defaults to the current kube config namespace --no-hooks disable pre/post upgrade hooks --recreate-pods performs pods restart for the resource if applicable --repo string chart repository url where to locate the requested chart @@ -76,4 +76,4 @@ helm upgrade [RELEASE] [CHART] ### SEE ALSO * [helm](helm.md) - The Helm package manager for Kubernetes. -###### Auto generated by spf13/cobra on 23-Jun-2017 +###### Auto generated by spf13/cobra on 15-Aug-2017 diff --git a/docs/install.md b/docs/install.md index ed0bd4ead..af5d57a7b 100755 --- a/docs/install.md +++ b/docs/install.md @@ -48,7 +48,7 @@ $ chmod 700 get_helm.sh $ ./get_helm.sh ``` -Yes, you can `curl ...| bash` that if you want to live on the edge. +Yes, you can `curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get | bash` that if you want to live on the edge. ### From Canary Builds @@ -102,7 +102,7 @@ whatever cluster `kubectl` connects to by default (`kubectl config view`). Once it connects, it will install `tiller` into the `kube-system` namespace. -After `helm init`, you should be able to run `kubectl get po --namespace +After `helm init`, you should be able to run `kubectl get pods --namespace kube-system` and see Tiller running. You can explicitly tell `helm init` to... diff --git a/docs/related.md b/docs/related.md index 22ac36380..c450e2607 100644 --- a/docs/related.md +++ b/docs/related.md @@ -44,7 +44,7 @@ tag on their plugin repositories. Tools layered on top of Helm or Tiller. -- [Wheel](https://github.com/appscode/wheel) - Ajax friendly Helm Tiller Proxy using [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) +- [AppsCode Swift](https://github.com/appscode/swift) - Ajax friendly Helm Tiller Proxy using [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) - [Quay App Registry](https://coreos.com/blog/quay-application-registry-for-kubernetes.html) - Open Kubernetes application registry, including a Helm access client - [Chartify](https://github.com/appscode/chartify) - Generate Helm charts from existing Kubernetes resources. - [VIM-Kubernetes](https://github.com/andrewstuart/vim-kubernetes) - VIM plugin for Kubernetes and Helm diff --git a/pkg/chartutil/create.go b/pkg/chartutil/create.go index 317737b2e..fe06e14e0 100644 --- a/pkg/chartutil/create.go +++ b/pkg/chartutil/create.go @@ -221,7 +221,7 @@ const defaultNotes = `1. Get the application URL by running these commands: {{- else if contains "ClusterIP" .Values.service.type }} export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ template "name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") echo "Visit http://127.0.0.1:8080 to use your application" - kubectl port-forward $POD_NAME 8080:{{ .Values.service.externalPort }} + kubectl port-forward $POD_NAME 8080:{{ .Values.service.internalPort }} {{- end }} ` diff --git a/pkg/chartutil/save.go b/pkg/chartutil/save.go index e3b5c1afd..b89917d96 100644 --- a/pkg/chartutil/save.go +++ b/pkg/chartutil/save.go @@ -115,6 +115,14 @@ func Save(c *chart.Chart, outDir string) (string, error) { filename := fmt.Sprintf("%s-%s.tgz", cfile.Name, cfile.Version) filename = filepath.Join(outDir, filename) + if stat, err := os.Stat(filepath.Dir(filename)); os.IsNotExist(err) { + if err := os.MkdirAll(filepath.Dir(filename), 0755); !os.IsExist(err) { + return "", err + } + } else if !stat.IsDir() { + return "", fmt.Errorf("is not a directory: %s", filepath.Dir(filename)) + } + f, err := os.Create(filename) if err != nil { return "", err diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml index 712b3a2fa..f0381ae6a 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml @@ -3,7 +3,7 @@ # Declare variables to be passed into your templates. # subchartA service: - name: nginx + name: apache type: ClusterIP externalPort: 80 internalPort: 80 diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/NOTES.txt b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/NOTES.txt new file mode 100644 index 000000000..4bdf443f6 --- /dev/null +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/NOTES.txt @@ -0,0 +1 @@ +Sample notes for {{ .Chart.Name }} \ No newline at end of file diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/service.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/service.yaml index fdf75aa91..bf7672e12 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/service.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/service.yaml @@ -4,6 +4,8 @@ metadata: name: {{ .Chart.Name }} labels: chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + namespace: "{{ .Release.Namespace }}" + release-name: "{{ .Release.Name }}" spec: type: {{ .Values.service.type }} ports: diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 2d4b73d8a..7c2ad6229 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -264,6 +264,9 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { return err } } + if err := move(tmpPath, destPath); err != nil { + return err + } if err := os.RemoveAll(tmpPath); err != nil { return fmt.Errorf("Failed to remove %v: %v", tmpPath, err) } @@ -610,3 +613,17 @@ func tarFromLocalDir(chartpath string, name string, repo string, version string) return "", fmt.Errorf("can't get a valid version for dependency %s", name) } + +// move files from tmppath to destpath +func move(tmpPath, destPath string) error { + files, _ := ioutil.ReadDir(tmpPath) + for _, file := range files { + filename := file.Name() + tmpfile := filepath.Join(tmpPath, filename) + destfile := filepath.Join(destPath, filename) + if err := os.Rename(tmpfile, destfile); err != nil { + return fmt.Errorf("Unable to move local charts to charts dir: %v", err) + } + } + return nil +} diff --git a/pkg/hooks/hooks.go b/pkg/hooks/hooks.go index e1fccb416..ed2d946a4 100644 --- a/pkg/hooks/hooks.go +++ b/pkg/hooks/hooks.go @@ -26,6 +26,9 @@ const HookAnno = "helm.sh/hook" // HookWeightAnno is the label name for a hook weight const HookWeightAnno = "helm.sh/hook-weight" +// HookDeleteAnno is the label name for the delete policy for a hook +const HookDeleteAnno = "helm.sh/hook-delete-policy" + // Types of hooks const ( PreInstall = "pre-install" @@ -40,6 +43,12 @@ const ( ReleaseTestFailure = "test-failure" ) +// Type of policy for deleting the hook +const ( + HookSucceeded = "hook-succeeded" + HookFailed = "hook-failed" +) + // FilterTestHooks filters the list of hooks are returns only testing hooks. func FilterTestHooks(hooks []*release.Hook) []*release.Hook { testHooks := []*release.Hook{} diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index 73206b80c..3ccd9acdb 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -58,7 +58,7 @@ func Templates(linter *support.Linter) { APIVersions: chartutil.DefaultVersionSet, KubeVersion: &version.Info{ Major: "1", - Minor: "6", + Minor: "7", GoVersion: runtime.Version(), Compiler: runtime.Compiler, Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), diff --git a/pkg/proto/hapi/release/hook.pb.go b/pkg/proto/hapi/release/hook.pb.go index f7d5419cc..bd9391c50 100644 --- a/pkg/proto/hapi/release/hook.pb.go +++ b/pkg/proto/hapi/release/hook.pb.go @@ -6,19 +6,9 @@ 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 - hapi/release/test_run.proto - hapi/release/test_suite.proto It has these top-level messages: Hook - Info - Release - Status - TestRun - TestSuite */ package release @@ -86,6 +76,27 @@ func (x Hook_Event) String() string { } func (Hook_Event) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0, 0} } +type Hook_DeletePolicy int32 + +const ( + Hook_SUCCEEDED Hook_DeletePolicy = 0 + Hook_FAILED Hook_DeletePolicy = 1 +) + +var Hook_DeletePolicy_name = map[int32]string{ + 0: "SUCCEEDED", + 1: "FAILED", +} +var Hook_DeletePolicy_value = map[string]int32{ + "SUCCEEDED": 0, + "FAILED": 1, +} + +func (x Hook_DeletePolicy) String() string { + return proto.EnumName(Hook_DeletePolicy_name, int32(x)) +} +func (Hook_DeletePolicy) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0, 1} } + // Hook defines a hook object. type Hook struct { Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` @@ -101,6 +112,8 @@ type Hook struct { LastRun *google_protobuf.Timestamp `protobuf:"bytes,6,opt,name=last_run,json=lastRun" json:"last_run,omitempty"` // Weight indicates the sort order for execution among similar Hook type Weight int32 `protobuf:"varint,7,opt,name=weight" json:"weight,omitempty"` + // DeletePolicies are the policies that indicate when to delete the hook + DeletePolicies []Hook_DeletePolicy `protobuf:"varint,8,rep,packed,name=delete_policies,json=deletePolicies,enum=hapi.release.Hook_DeletePolicy" json:"delete_policies,omitempty"` } func (m *Hook) Reset() { *m = Hook{} } @@ -157,37 +170,48 @@ func (m *Hook) GetWeight() int32 { return 0 } +func (m *Hook) GetDeletePolicies() []Hook_DeletePolicy { + if m != nil { + return m.DeletePolicies + } + return nil +} + func init() { proto.RegisterType((*Hook)(nil), "hapi.release.Hook") proto.RegisterEnum("hapi.release.Hook_Event", Hook_Event_name, Hook_Event_value) + proto.RegisterEnum("hapi.release.Hook_DeletePolicy", Hook_DeletePolicy_name, Hook_DeletePolicy_value) } func init() { proto.RegisterFile("hapi/release/hook.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 371 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0xdf, 0x8e, 0x93, 0x40, - 0x14, 0x87, 0x65, 0x5b, 0xa0, 0x3d, 0x5d, 0xd7, 0x71, 0x62, 0x74, 0xd2, 0x1b, 0xc9, 0x5e, 0x71, - 0x35, 0x98, 0x35, 0x3e, 0x00, 0xdb, 0x1d, 0x75, 0xb3, 0x84, 0x36, 0x03, 0xc4, 0xc4, 0x1b, 0xc2, - 0xc6, 0x69, 0x21, 0x2d, 0x0c, 0x29, 0x53, 0x7d, 0x33, 0x9f, 0xc4, 0x07, 0x32, 0x33, 0xfc, 0x89, - 0x89, 0x77, 0x67, 0xbe, 0xdf, 0xc7, 0x39, 0x9c, 0x03, 0xef, 0xca, 0xa2, 0xad, 0x82, 0xb3, 0x38, - 0x89, 0xa2, 0x13, 0x41, 0x29, 0xe5, 0x91, 0xb6, 0x67, 0xa9, 0x24, 0xbe, 0xd6, 0x01, 0x1d, 0x82, - 0xf5, 0xfb, 0x83, 0x94, 0x87, 0x93, 0x08, 0x4c, 0xf6, 0x7c, 0xd9, 0x07, 0xaa, 0xaa, 0x45, 0xa7, - 0x8a, 0xba, 0xed, 0xf5, 0xdb, 0xdf, 0x33, 0x98, 0x7f, 0x95, 0xf2, 0x88, 0x31, 0xcc, 0x9b, 0xa2, - 0x16, 0xc4, 0xf2, 0x2c, 0x7f, 0xc9, 0x4d, 0xad, 0xd9, 0xb1, 0x6a, 0x7e, 0x90, 0xab, 0x9e, 0xe9, - 0x5a, 0xb3, 0xb6, 0x50, 0x25, 0x99, 0xf5, 0x4c, 0xd7, 0x78, 0x0d, 0x8b, 0xba, 0x68, 0xaa, 0xbd, - 0xe8, 0x14, 0x99, 0x1b, 0x3e, 0xbd, 0xf1, 0x07, 0x70, 0xc4, 0x4f, 0xd1, 0xa8, 0x8e, 0xd8, 0xde, - 0xcc, 0xbf, 0xb9, 0x23, 0xf4, 0xdf, 0x1f, 0xa4, 0x7a, 0x36, 0x65, 0x5a, 0xe0, 0x83, 0x87, 0x3f, - 0xc1, 0xe2, 0x54, 0x74, 0x2a, 0x3f, 0x5f, 0x1a, 0xe2, 0x78, 0x96, 0xbf, 0xba, 0x5b, 0xd3, 0x7e, - 0x0d, 0x3a, 0xae, 0x41, 0xd3, 0x71, 0x0d, 0xee, 0x6a, 0x97, 0x5f, 0x1a, 0xfc, 0x16, 0x9c, 0x5f, - 0xa2, 0x3a, 0x94, 0x8a, 0xb8, 0x9e, 0xe5, 0xdb, 0x7c, 0x78, 0xdd, 0xfe, 0xb1, 0xc0, 0x36, 0x03, - 0xf0, 0x0a, 0xdc, 0x2c, 0x7e, 0x8a, 0xb7, 0xdf, 0x62, 0xf4, 0x02, 0xbf, 0x82, 0xd5, 0x8e, 0xb3, - 0xfc, 0x31, 0x4e, 0xd2, 0x30, 0x8a, 0x90, 0x85, 0x11, 0x5c, 0xef, 0xb6, 0x49, 0x3a, 0x91, 0x2b, - 0x7c, 0x03, 0xa0, 0x95, 0x07, 0x16, 0xb1, 0x94, 0xa1, 0x99, 0xf9, 0x44, 0x1b, 0x03, 0x98, 0x8f, - 0x3d, 0xb2, 0xdd, 0x17, 0x1e, 0x3e, 0x30, 0x64, 0x4f, 0x3d, 0x46, 0xe2, 0x18, 0xc2, 0x59, 0xce, - 0xb7, 0x51, 0x74, 0x1f, 0x6e, 0x9e, 0x90, 0x8b, 0x5f, 0xc3, 0x4b, 0xe3, 0x4c, 0x68, 0x81, 0x09, - 0xbc, 0xe1, 0x2c, 0x62, 0x61, 0xc2, 0xf2, 0x94, 0x25, 0x69, 0x9e, 0x64, 0x9b, 0x0d, 0x4b, 0x12, - 0xb4, 0xfc, 0x2f, 0xf9, 0x1c, 0x3e, 0x46, 0x19, 0x67, 0x08, 0xee, 0x97, 0xdf, 0xdd, 0xe1, 0x86, - 0xcf, 0x8e, 0x39, 0xcb, 0xc7, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x82, 0x3c, 0x7a, 0x0e, 0x14, - 0x02, 0x00, 0x00, + // 428 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x91, 0xd1, 0x6e, 0xda, 0x30, + 0x14, 0x86, 0x9b, 0x12, 0x12, 0x38, 0x50, 0xea, 0x59, 0xd3, 0x66, 0x71, 0x53, 0xc4, 0x15, 0xbb, + 0x09, 0x53, 0xa7, 0x3d, 0x40, 0x4a, 0xce, 0xd6, 0xaa, 0x11, 0x20, 0x27, 0x68, 0xd2, 0x6e, 0xa2, + 0x74, 0xb8, 0x10, 0x11, 0xe2, 0x88, 0x98, 0x4d, 0x7b, 0xa6, 0xbd, 0xce, 0x1e, 0x68, 0xb2, 0x09, + 0x59, 0xa5, 0xed, 0xee, 0x9c, 0xef, 0x7c, 0x76, 0xce, 0x1f, 0xc3, 0xdb, 0x6d, 0x5a, 0x66, 0xd3, + 0x83, 0xc8, 0x45, 0x5a, 0x89, 0xe9, 0x56, 0xca, 0x9d, 0x57, 0x1e, 0xa4, 0x92, 0xb4, 0xaf, 0x07, + 0x5e, 0x3d, 0x18, 0xde, 0x6c, 0xa4, 0xdc, 0xe4, 0x62, 0x6a, 0x66, 0x4f, 0xc7, 0xe7, 0xa9, 0xca, + 0xf6, 0xa2, 0x52, 0xe9, 0xbe, 0x3c, 0xe9, 0xe3, 0x5f, 0x36, 0xd8, 0xf7, 0x52, 0xee, 0x28, 0x05, + 0xbb, 0x48, 0xf7, 0x82, 0x59, 0x23, 0x6b, 0xd2, 0xe5, 0xa6, 0xd6, 0x6c, 0x97, 0x15, 0x6b, 0x76, + 0x79, 0x62, 0xba, 0xd6, 0xac, 0x4c, 0xd5, 0x96, 0xb5, 0x4e, 0x4c, 0xd7, 0x74, 0x08, 0x9d, 0x7d, + 0x5a, 0x64, 0xcf, 0xa2, 0x52, 0xcc, 0x36, 0xbc, 0xe9, 0xe9, 0x7b, 0x70, 0xc4, 0x77, 0x51, 0xa8, + 0x8a, 0xb5, 0x47, 0xad, 0xc9, 0xe0, 0x96, 0x79, 0x2f, 0x17, 0xf4, 0xf4, 0xb7, 0x3d, 0xd4, 0x02, + 0xaf, 0x3d, 0xfa, 0x11, 0x3a, 0x79, 0x5a, 0xa9, 0xe4, 0x70, 0x2c, 0x98, 0x33, 0xb2, 0x26, 0xbd, + 0xdb, 0xa1, 0x77, 0x8a, 0xe1, 0x9d, 0x63, 0x78, 0xf1, 0x39, 0x06, 0x77, 0xb5, 0xcb, 0x8f, 0x05, + 0x7d, 0x03, 0xce, 0x0f, 0x91, 0x6d, 0xb6, 0x8a, 0xb9, 0x23, 0x6b, 0xd2, 0xe6, 0x75, 0x47, 0xef, + 0xe1, 0x7a, 0x2d, 0x72, 0xa1, 0x44, 0x52, 0xca, 0x3c, 0xfb, 0x96, 0x89, 0x8a, 0x75, 0xcc, 0x26, + 0x37, 0xff, 0xd9, 0x24, 0x30, 0xe6, 0x52, 0x8b, 0x3f, 0xf9, 0x60, 0xfd, 0xb7, 0xcb, 0x44, 0x35, + 0xfe, 0x6d, 0x41, 0xdb, 0xac, 0x4a, 0x7b, 0xe0, 0xae, 0xe6, 0x8f, 0xf3, 0xc5, 0x97, 0x39, 0xb9, + 0xa0, 0xd7, 0xd0, 0x5b, 0x72, 0x4c, 0x1e, 0xe6, 0x51, 0xec, 0x87, 0x21, 0xb1, 0x28, 0x81, 0xfe, + 0x72, 0x11, 0xc5, 0x0d, 0xb9, 0xa4, 0x03, 0x00, 0xad, 0x04, 0x18, 0x62, 0x8c, 0xa4, 0x65, 0x8e, + 0x68, 0xa3, 0x06, 0xf6, 0xf9, 0x8e, 0xd5, 0xf2, 0x33, 0xf7, 0x03, 0x24, 0xed, 0xe6, 0x8e, 0x33, + 0x71, 0x0c, 0xe1, 0x98, 0xf0, 0x45, 0x18, 0xde, 0xf9, 0xb3, 0x47, 0xe2, 0xd2, 0x57, 0x70, 0x65, + 0x9c, 0x06, 0x75, 0x28, 0x83, 0xd7, 0x1c, 0x43, 0xf4, 0x23, 0x4c, 0x62, 0x8c, 0xe2, 0x24, 0x5a, + 0xcd, 0x66, 0x18, 0x45, 0xa4, 0xfb, 0xcf, 0xe4, 0x93, 0xff, 0x10, 0xae, 0x38, 0x12, 0x18, 0xbf, + 0x83, 0xfe, 0xcb, 0xd8, 0xf4, 0x0a, 0xba, 0xe6, 0x18, 0x06, 0x18, 0x90, 0x0b, 0x0a, 0xe0, 0x68, + 0x17, 0x03, 0x62, 0xdd, 0x75, 0xbf, 0xba, 0xf5, 0xef, 0x7a, 0x72, 0xcc, 0x5b, 0x7c, 0xf8, 0x13, + 0x00, 0x00, 0xff, 0xff, 0xb9, 0x8a, 0xe1, 0xaf, 0x89, 0x02, 0x00, 0x00, } diff --git a/pkg/repo/chartrepo_test.go b/pkg/repo/chartrepo_test.go index f6d6df74e..9f1bc995a 100644 --- a/pkg/repo/chartrepo_test.go +++ b/pkg/repo/chartrepo_test.go @@ -54,6 +54,7 @@ func TestLoadChartRepository(t *testing.T) { filepath.Join(testRepository, "frobnitz-1.2.3.tgz"), filepath.Join(testRepository, "sprocket-1.1.0.tgz"), filepath.Join(testRepository, "sprocket-1.2.0.tgz"), + filepath.Join(testRepository, "universe/zarthal-1.0.0.tgz"), } if r.Config.Name != testRepository { @@ -118,8 +119,8 @@ func verifyIndex(t *testing.T, actual *IndexFile) { } entries := actual.Entries - if numEntries := len(entries); numEntries != 2 { - t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries) + if numEntries := len(entries); numEntries != 3 { + t.Errorf("Expected 3 charts to be listed in index file but got %v", numEntries) } expects := map[string]ChartVersions{ @@ -145,6 +146,14 @@ func verifyIndex(t *testing.T, actual *IndexFile) { }, }, }, + "zarthal": { + { + Metadata: &chart.Metadata{ + Name: "zarthal", + Version: "1.0.0", + }, + }, + }, } for name, versions := range expects { diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 4e59b8d58..174ceea01 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -231,9 +231,26 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) { if err != nil { return nil, err } + moreArchives, err := filepath.Glob(filepath.Join(dir, "**/*.tgz")) + if err != nil { + return nil, err + } + archives = append(archives, moreArchives...) + index := NewIndexFile() for _, arch := range archives { - fname := filepath.Base(arch) + fname, err := filepath.Rel(dir, arch) + if err != nil { + return index, err + } + + var parentDir string + parentDir, fname = filepath.Split(fname) + parentURL, err := urlutil.URLJoin(baseURL, parentDir) + if err != nil { + parentURL = filepath.Join(baseURL, parentDir) + } + c, err := chartutil.Load(arch) if err != nil { // Assume this is not a chart. @@ -243,7 +260,7 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) { if err != nil { return index, err } - index.Add(c.Metadata, fname, baseURL, hash) + index.Add(c.Metadata, fname, parentURL, hash) } return index, nil } diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index 17a1cc209..ba426b174 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -278,27 +278,35 @@ func TestIndexDirectory(t *testing.T) { t.Fatal(err) } - if l := len(index.Entries); l != 2 { - t.Fatalf("Expected 2 entries, got %d", l) + if l := len(index.Entries); l != 3 { + t.Fatalf("Expected 3 entries, got %d", l) } // Other things test the entry generation more thoroughly. We just test a // few fields. - cname := "frobnitz" - frobs, ok := index.Entries[cname] - if !ok { - t.Fatalf("Could not read chart %s", cname) - } - frob := frobs[0] - if len(frob.Digest) == 0 { - t.Errorf("Missing digest of file %s.", frob.Name) + corpus := []struct{ chartName, downloadLink string }{ + {"frobnitz", "http://localhost:8080/frobnitz-1.2.3.tgz"}, + {"zarthal", "http://localhost:8080/universe/zarthal-1.0.0.tgz"}, } - if frob.URLs[0] != "http://localhost:8080/frobnitz-1.2.3.tgz" { - t.Errorf("Unexpected URLs: %v", frob.URLs) - } - if frob.Name != "frobnitz" { - t.Errorf("Expected frobnitz, got %q", frob.Name) + + for _, test := range corpus { + cname := test.chartName + frobs, ok := index.Entries[cname] + if !ok { + t.Fatalf("Could not read chart %s", cname) + } + + frob := frobs[0] + if len(frob.Digest) == 0 { + t.Errorf("Missing digest of file %s.", frob.Name) + } + if frob.URLs[0] != test.downloadLink { + t.Errorf("Unexpected URLs: %v", frob.URLs) + } + if frob.Name != cname { + t.Errorf("Expected %q, got %q", cname, frob.Name) + } } } diff --git a/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz b/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz new file mode 100644 index 000000000..90cb34bd5 Binary files /dev/null and b/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz differ diff --git a/pkg/tiller/hooks.go b/pkg/tiller/hooks.go index 996253384..05abb66e7 100644 --- a/pkg/tiller/hooks.go +++ b/pkg/tiller/hooks.go @@ -44,16 +44,22 @@ var events = map[string]release.Hook_Event{ hooks.ReleaseTestFailure: release.Hook_RELEASE_TEST_FAILURE, } -// manifest represents a manifest file, which has a name and some content. -type manifest struct { - name string - content string - head *util.SimpleHead +// deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning +var deletePolices = map[string]release.Hook_DeletePolicy{ + hooks.HookSucceeded: release.Hook_SUCCEEDED, + hooks.HookFailed: release.Hook_FAILED, +} + +// Manifest represents a manifest file, which has a name and some content. +type Manifest struct { + Name string + Content string + Head *util.SimpleHead } type result struct { hooks []*release.Hook - generic []manifest + generic []Manifest } type manifestFile struct { @@ -71,7 +77,7 @@ type manifestFile struct { // // Files that do not parse into the expected format are simply placed into a map and // returned. -func sortManifests(files map[string]string, apis chartutil.VersionSet, sort SortOrder) ([]*release.Hook, []manifest, error) { +func sortManifests(files map[string]string, apis chartutil.VersionSet, sort SortOrder) ([]*release.Hook, []Manifest, error) { result := &result{} for filePath, c := range files { @@ -113,6 +119,13 @@ func sortManifests(files map[string]string, apis chartutil.VersionSet, sort Sort // annotations: // helm.sh/hook: pre-install // +// To determine the policy to delete the hook, it looks for a YAML structure like this: +// +// kind: SomeKind +// apiVersion: v1 +// metadata: +// annotations: +// helm.sh/hook-delete-policy: hook-succeeded func (file *manifestFile) sort(result *result) error { for _, m := range file.entries { var entry util.SimpleHead @@ -128,20 +141,20 @@ func (file *manifestFile) sort(result *result) error { } if !hasAnyAnnotation(entry) { - result.generic = append(result.generic, manifest{ - name: file.path, - content: m, - head: &entry, + result.generic = append(result.generic, Manifest{ + Name: file.path, + Content: m, + Head: &entry, }) continue } hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno] if !ok { - result.generic = append(result.generic, manifest{ - name: file.path, - content: m, - head: &entry, + result.generic = append(result.generic, Manifest{ + Name: file.path, + Content: m, + Head: &entry, }) continue } @@ -149,30 +162,48 @@ func (file *manifestFile) sort(result *result) error { hw := calculateHookWeight(entry) h := &release.Hook{ - Name: entry.Metadata.Name, - Kind: entry.Kind, - Path: file.path, - Manifest: m, - Events: []release.Hook_Event{}, - Weight: hw, + Name: entry.Metadata.Name, + Kind: entry.Kind, + Path: file.path, + Manifest: m, + Events: []release.Hook_Event{}, + Weight: hw, + DeletePolicies: []release.Hook_DeletePolicy{}, } - isKnownHook := false + isUnknownHook := false for _, hookType := range strings.Split(hookTypes, ",") { hookType = strings.ToLower(strings.TrimSpace(hookType)) e, ok := events[hookType] - if ok { - isKnownHook = true - h.Events = append(h.Events, e) + if !ok { + isUnknownHook = true + break } + h.Events = append(h.Events, e) } - if !isKnownHook { + if isUnknownHook { log.Printf("info: skipping unknown hook: %q", hookTypes) continue } result.hooks = append(result.hooks, h) + + isKnownDeletePolices := false + dps, ok := entry.Metadata.Annotations[hooks.HookDeleteAnno] + if ok { + for _, dp := range strings.Split(dps, ",") { + dp = strings.ToLower(strings.TrimSpace(dp)) + p, exist := deletePolices[dp] + if exist { + isKnownDeletePolices = true + h.DeletePolicies = append(h.DeletePolicies, p) + } + } + if !isKnownDeletePolices { + log.Printf("info: skipping unknown hook delete policy: %q", dps) + } + } } return nil diff --git a/pkg/tiller/hooks_test.go b/pkg/tiller/hooks_test.go index 7a7d2a2b5..658f859f4 100644 --- a/pkg/tiller/hooks_test.go +++ b/pkg/tiller/hooks_test.go @@ -194,7 +194,7 @@ metadata: } // Verify the sort order - sorted := []manifest{} + sorted := []Manifest{} for _, s := range data { manifests := util.SplitManifests(s.manifest) @@ -211,10 +211,10 @@ metadata: //only keep track of non-hook manifests if err == nil && s.hooks[name] == nil { - another := manifest{ - content: m, - name: name, - head: &sh, + another := Manifest{ + Content: m, + Name: name, + Head: &sh, } sorted = append(sorted, another) } @@ -223,8 +223,8 @@ metadata: sorted = sortByKind(sorted, InstallOrder) for i, m := range generic { - if m.content != sorted[i].content { - t.Errorf("Expected %q, got %q", m.content, sorted[i].content) + if m.Content != sorted[i].Content { + t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content) } } } diff --git a/pkg/tiller/kind_sorter.go b/pkg/tiller/kind_sorter.go index e0549e2f4..f2447143b 100644 --- a/pkg/tiller/kind_sorter.go +++ b/pkg/tiller/kind_sorter.go @@ -84,7 +84,7 @@ var UninstallOrder SortOrder = []string{ // sortByKind does an in-place sort of manifests by Kind. // // Results are sorted by 'ordering' -func sortByKind(manifests []manifest, ordering SortOrder) []manifest { +func sortByKind(manifests []Manifest, ordering SortOrder) []Manifest { ks := newKindSorter(manifests, ordering) sort.Sort(ks) return ks.manifests @@ -92,10 +92,10 @@ func sortByKind(manifests []manifest, ordering SortOrder) []manifest { type kindSorter struct { ordering map[string]int - manifests []manifest + manifests []Manifest } -func newKindSorter(m []manifest, s SortOrder) *kindSorter { +func newKindSorter(m []Manifest, s SortOrder) *kindSorter { o := make(map[string]int, len(s)) for v, k := range s { o[k] = v @@ -114,11 +114,11 @@ func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifes func (k *kindSorter) Less(i, j int) bool { a := k.manifests[i] b := k.manifests[j] - first, aok := k.ordering[a.head.Kind] - second, bok := k.ordering[b.head.Kind] + first, aok := k.ordering[a.Head.Kind] + second, bok := k.ordering[b.Head.Kind] if first == second { // same kind (including unknown) so sub sort alphanumeric - return a.name < b.name + return a.Name < b.Name } // unknown kind is last if !aok { @@ -130,3 +130,11 @@ func (k *kindSorter) Less(i, j int) bool { // sort different kinds return first < second } + +// SortByKind sorts manifests in InstallOrder +func SortByKind(manifests []Manifest) []Manifest { + ordering := InstallOrder + ks := newKindSorter(manifests, ordering) + sort.Sort(ks) + return ks.manifests +} diff --git a/pkg/tiller/kind_sorter_test.go b/pkg/tiller/kind_sorter_test.go index a707176b3..6996731ca 100644 --- a/pkg/tiller/kind_sorter_test.go +++ b/pkg/tiller/kind_sorter_test.go @@ -24,103 +24,102 @@ import ( ) func TestKindSorter(t *testing.T) { - manifests := []manifest{ + manifests := []Manifest{ { - name: "i", - head: &util.SimpleHead{Kind: "ClusterRole"}, + Name: "i", + Head: &util.SimpleHead{Kind: "ClusterRole"}, }, { - name: "j", - head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, + Name: "j", + Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, }, { - name: "e", - head: &util.SimpleHead{Kind: "ConfigMap"}, + Name: "e", + Head: &util.SimpleHead{Kind: "ConfigMap"}, }, { - name: "u", - head: &util.SimpleHead{Kind: "CronJob"}, + Name: "u", + Head: &util.SimpleHead{Kind: "CronJob"}, }, { - name: "n", - head: &util.SimpleHead{Kind: "DaemonSet"}, + Name: "n", + Head: &util.SimpleHead{Kind: "DaemonSet"}, }, { - name: "r", - head: &util.SimpleHead{Kind: "Deployment"}, + Name: "r", + Head: &util.SimpleHead{Kind: "Deployment"}, }, { - name: "!", - head: &util.SimpleHead{Kind: "HonkyTonkSet"}, + Name: "!", + Head: &util.SimpleHead{Kind: "HonkyTonkSet"}, }, { - name: "v", - head: &util.SimpleHead{Kind: "Ingress"}, + Name: "v", + Head: &util.SimpleHead{Kind: "Ingress"}, }, { - name: "t", - head: &util.SimpleHead{Kind: "Job"}, + Name: "t", + Head: &util.SimpleHead{Kind: "Job"}, }, { - name: "c", - head: &util.SimpleHead{Kind: "LimitRange"}, + Name: "c", + Head: &util.SimpleHead{Kind: "LimitRange"}, }, { - name: "a", - head: &util.SimpleHead{Kind: "Namespace"}, + Name: "a", + Head: &util.SimpleHead{Kind: "Namespace"}, }, { - name: "f", - head: &util.SimpleHead{Kind: "PersistentVolume"}, + Name: "f", + Head: &util.SimpleHead{Kind: "PersistentVolume"}, }, { - name: "g", - head: &util.SimpleHead{Kind: "PersistentVolumeClaim"}, + Name: "g", + Head: &util.SimpleHead{Kind: "PersistentVolumeClaim"}, }, { - name: "o", - head: &util.SimpleHead{Kind: "Pod"}, + Name: "o", + Head: &util.SimpleHead{Kind: "Pod"}, }, { - name: "q", - head: &util.SimpleHead{Kind: "ReplicaSet"}, + Name: "q", + Head: &util.SimpleHead{Kind: "ReplicaSet"}, }, { - name: "p", - head: &util.SimpleHead{Kind: "ReplicationController"}, + Name: "p", + Head: &util.SimpleHead{Kind: "ReplicationController"}, }, { - name: "b", - head: &util.SimpleHead{Kind: "ResourceQuota"}, + Name: "b", + Head: &util.SimpleHead{Kind: "ResourceQuota"}, }, { - name: "k", - head: &util.SimpleHead{Kind: "Role"}, + Name: "k", + Head: &util.SimpleHead{Kind: "Role"}, }, { - name: "l", - head: &util.SimpleHead{Kind: "RoleBinding"}, + Name: "l", + Head: &util.SimpleHead{Kind: "RoleBinding"}, }, { - name: "d", - head: &util.SimpleHead{Kind: "Secret"}, + Name: "d", + Head: &util.SimpleHead{Kind: "Secret"}, }, { - name: "m", - head: &util.SimpleHead{Kind: "Service"}, + Name: "m", + Head: &util.SimpleHead{Kind: "Service"}, }, { - name: "h", - head: &util.SimpleHead{Kind: "ServiceAccount"}, + Name: "h", + Head: &util.SimpleHead{Kind: "ServiceAccount"}, }, { - name: "s", - head: &util.SimpleHead{Kind: "StatefulSet"}, + Name: "s", + Head: &util.SimpleHead{Kind: "StatefulSet"}, }, { - name: "w", - content: "", - head: &util.SimpleHead{Kind: "APIService"}, + Name: "w", + Head: &util.SimpleHead{Kind: "APIService"}, }, } @@ -139,7 +138,7 @@ func TestKindSorter(t *testing.T) { } defer buf.Reset() for _, r := range sortByKind(manifests, test.order) { - buf.WriteString(r.name) + buf.WriteString(r.Name) } if got := buf.String(); got != test.expected { t.Errorf("Expected %q, got %q", test.expected, got) @@ -150,42 +149,42 @@ func TestKindSorter(t *testing.T) { // TestKindSorterSubSort verifies manifests of same kind are also sorted alphanumeric func TestKindSorterSubSort(t *testing.T) { - manifests := []manifest{ + manifests := []Manifest{ { - name: "a", - head: &util.SimpleHead{Kind: "ClusterRole"}, + Name: "a", + Head: &util.SimpleHead{Kind: "ClusterRole"}, }, { - name: "A", - head: &util.SimpleHead{Kind: "ClusterRole"}, + Name: "A", + Head: &util.SimpleHead{Kind: "ClusterRole"}, }, { - name: "0", - head: &util.SimpleHead{Kind: "ConfigMap"}, + Name: "0", + Head: &util.SimpleHead{Kind: "ConfigMap"}, }, { - name: "1", - head: &util.SimpleHead{Kind: "ConfigMap"}, + Name: "1", + Head: &util.SimpleHead{Kind: "ConfigMap"}, }, { - name: "z", - head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, + Name: "z", + Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, }, { - name: "!", - head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, + Name: "!", + Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, }, { - name: "u3", - head: &util.SimpleHead{Kind: "Unknown"}, + Name: "u3", + Head: &util.SimpleHead{Kind: "Unknown"}, }, { - name: "u1", - head: &util.SimpleHead{Kind: "Unknown"}, + Name: "u1", + Head: &util.SimpleHead{Kind: "Unknown"}, }, { - name: "u2", - head: &util.SimpleHead{Kind: "Unknown"}, + Name: "u2", + Head: &util.SimpleHead{Kind: "Unknown"}, }, } for _, test := range []struct { @@ -200,7 +199,7 @@ func TestKindSorterSubSort(t *testing.T) { t.Run(test.description, func(t *testing.T) { defer buf.Reset() for _, r := range sortByKind(manifests, test.order) { - buf.WriteString(r.name) + buf.WriteString(r.Name) } if got := buf.String(); got != test.expected { t.Errorf("Expected %q, got %q", test.expected, got) diff --git a/pkg/tiller/release_modules.go b/pkg/tiller/release_modules.go index 73b95e60c..b5fbbeb44 100644 --- a/pkg/tiller/release_modules.go +++ b/pkg/tiller/release_modules.go @@ -166,7 +166,7 @@ func DeleteRelease(rel *release.Release, vs chartutil.VersionSet, kubeClient env errs = []error{} for _, file := range filesToDelete { - b := bytes.NewBufferString(strings.TrimSpace(file.content)) + b := bytes.NewBufferString(strings.TrimSpace(file.Content)) if b.Len() == 0 { continue } diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go index db7c0b568..44d5d847a 100644 --- a/pkg/tiller/release_server.go +++ b/pkg/tiller/release_server.go @@ -30,6 +30,7 @@ import ( "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/hooks" "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/services" @@ -299,8 +300,8 @@ func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values // Aggregate all valid manifests into one big doc. b := bytes.NewBuffer(nil) for _, m := range manifests { - b.WriteString("\n---\n# Source: " + m.name + "\n") - b.WriteString(m.content) + b.WriteString("\n---\n# Source: " + m.Name + "\n") + b.WriteString(m.Content) } return hooks, b, notes, nil @@ -347,12 +348,36 @@ func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook strin b.WriteString(h.Manifest) if err := kubeCli.WatchUntilReady(namespace, b, timeout, false); err != nil { s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err) + // If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted + // under failed condition. If so, then clear the corresponding resource object in the hook + if hookShouldBeDeleted(h, hooks.HookFailed) { + b.Reset() + b.WriteString(h.Manifest) + s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, hooks.HookFailed) + if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil { + s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete) + return errHookDelete + } + } return err } - h.LastRun = timeconv.Now() } s.Log("hooks complete for %s %s", hook, name) + // If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted + // under succeeded condition. If so, then clear the corresponding resource object in each hook + for _, h := range executingHooks { + b := bytes.NewBufferString(h.Manifest) + if hookShouldBeDeleted(h, hooks.HookSucceeded) { + s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, hooks.HookSucceeded) + if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil { + s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete) + return errHookDelete + } + } + h.LastRun = timeconv.Now() + } + return nil } @@ -373,3 +398,16 @@ func validateReleaseName(releaseName string) error { return nil } + +// hookShouldBeDeleted determines whether the defined hook deletion policy matches the hook deletion polices +// supported by helm. If so, mark the hook as one should be deleted. +func hookShouldBeDeleted(hook *release.Hook, policy string) bool { + if dp, ok := deletePolices[policy]; ok { + for _, v := range hook.DeletePolicies { + if dp == v { + return true + } + } + } + return false +} diff --git a/pkg/tiller/resource_policy.go b/pkg/tiller/resource_policy.go index 89f303197..2102ab66b 100644 --- a/pkg/tiller/resource_policy.go +++ b/pkg/tiller/resource_policy.go @@ -29,18 +29,18 @@ const resourcePolicyAnno = "helm.sh/resource-policy" // during an uninstallRelease action. const keepPolicy = "keep" -func filterManifestsToKeep(manifests []manifest) ([]manifest, []manifest) { - remaining := []manifest{} - keep := []manifest{} +func filterManifestsToKeep(manifests []Manifest) ([]Manifest, []Manifest) { + remaining := []Manifest{} + keep := []Manifest{} for _, m := range manifests { - if m.head.Metadata == nil || m.head.Metadata.Annotations == nil || len(m.head.Metadata.Annotations) == 0 { + if m.Head.Metadata == nil || m.Head.Metadata.Annotations == nil || len(m.Head.Metadata.Annotations) == 0 { remaining = append(remaining, m) continue } - resourcePolicyType, ok := m.head.Metadata.Annotations[resourcePolicyAnno] + resourcePolicyType, ok := m.Head.Metadata.Annotations[resourcePolicyAnno] if !ok { remaining = append(remaining, m) continue @@ -55,10 +55,10 @@ func filterManifestsToKeep(manifests []manifest) ([]manifest, []manifest) { return keep, remaining } -func summarizeKeptManifests(manifests []manifest) string { +func summarizeKeptManifests(manifests []Manifest) string { message := "These resources were kept due to the resource policy:\n" for _, m := range manifests { - details := "[" + m.head.Kind + "] " + m.head.Metadata.Name + "\n" + details := "[" + m.Head.Kind + "] " + m.Head.Metadata.Name + "\n" message = message + details } return message diff --git a/scripts/get b/scripts/get index 8b4a83a2e..88a6fa1a7 100755 --- a/scripts/get +++ b/scripts/get @@ -130,7 +130,7 @@ downloadFile() { # installs it. installFile() { HELM_TMP="/tmp/$PROJECT_NAME" - local sum=$(openssl sha -sha256 ${HELM_TMP_FILE} | awk '{print $2}') + local sum=$(openssl sha1 -sha256 ${HELM_TMP_FILE} | awk '{print $2}') local expected_sum=$(cat ${HELM_SUM_FILE}) if [ "$sum" != "$expected_sum" ]; then echo "SHA sum of $HELM_TMP does not match. Aborting." @@ -191,9 +191,14 @@ export INPUT_ARGUMENTS="${@}" set -u while [[ $# -gt 0 ]]; do case $1 in - '--version'|-v) - export DESIRED_VERSION="${2}" + '--version'|-v) shift + if [[ $# -ne 0 ]]; then + export DESIRED_VERSION="${1}" + else + echo -e "Please provide the desired version. e.g. --version v2.4.0 or -v latest" + exit 0 + fi ;; '--help'|-h) help