diff --git a/.circleci/bootstrap.sh b/.circleci/bootstrap.sh index 978464efe83..79d1940778d 100755 --- a/.circleci/bootstrap.sh +++ b/.circleci/bootstrap.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. +# Copyright The Helm Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,5 +15,6 @@ # limitations under the License. set -euo pipefail -apt-get update -y && apt-get install -yq zip -make bootstrap +curl -sSL https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz | tar xz +sudo mv golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64/golangci-lint /usr/local/bin/golangci-lint +rm -rf golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64 diff --git a/.circleci/config.yml b/.circleci/config.yml index df9786d823d..7e43b1f8931 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,33 +1,35 @@ +--- version: 2 + jobs: build: - working_directory: /go/src/k8s.io/helm - parallelism: 3 + working_directory: ~/helm.sh/helm docker: - - image: golang:1.10 + - image: circleci/golang:1.16 + + auth: + username: $DOCKER_USER + password: $DOCKER_PASS + environment: - PROJECT_NAME: "kubernetes-helm" + GOCACHE: "/tmp/go/cache" + GOLANGCI_LINT_VERSION: "1.36.0" + steps: - checkout - - setup_remote_docker: - version: 17.09.0-ce - - restore_cache: - keys: - - glide-{{ checksum "glide.yaml" }}-{{ checksum "glide.lock" }} - - glide- # used as a fall through if the checksum fails to find exact entry - run: - name: install dependencies + name: install test dependencies command: .circleci/bootstrap.sh - - save_cache: - key: glide-{{ checksum "glide.yaml" }}-{{ checksum "glide.lock" }} - paths: - - /root/.glide # Where the glide cache is stored + - run: + name: test style + command: make test-style - run: name: test - command: .circleci/test.sh + command: make test-coverage - deploy: name: deploy command: .circleci/deploy.sh + workflows: version: 2 build: diff --git a/.circleci/deploy.sh b/.circleci/deploy.sh index 6ad91109d75..bbb08e6f0cc 100755 --- a/.circleci/deploy.sh +++ b/.circleci/deploy.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. +# Copyright The Helm Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,8 +20,8 @@ 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"} +: ${AZURE_STORAGE_CONNECTION_STRING:?"AZURE_STORAGE_CONNECTION_STRING environment variable is not set"} +: ${AZURE_STORAGE_CONTAINER_NAME:?"AZURE_STORAGE_CONTAINER_NAME environment variable is not set"} VERSION= if [[ -n "${CIRCLE_TAG:-}" ]]; then @@ -29,36 +29,21 @@ if [[ -n "${CIRCLE_TAG:-}" ]]; then elif [[ "${CIRCLE_BRANCH:-}" == "master" ]]; then VERSION="canary" else - echo "Skipping deploy step; this is neither master or a tag" + echo "Skipping deploy step; this is neither a releasable branch or a tag" exit fi -echo "Install docker client" -VER="17.09.0-ce" -curl -L -o /tmp/docker-$VER.tgz https://download.docker.com/linux/static/stable/x86_64/docker-$VER.tgz -tar -xz -C /tmp -f /tmp/docker-$VER.tgz -mv /tmp/docker/* /usr/bin +echo "Installing Azure CLI" +echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ stretch main" | sudo tee /etc/apt/sources.list.d/azure-cli.list +curl -L https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add +sudo apt install apt-transport-https +sudo apt update +sudo apt install azure-cli -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" -${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 -u _json_key -p "$(cat ${HOME}/gcloud-service-key.json)" https://gcr.io - -echo "Building the tiller image" -make docker-build VERSION="${VERSION}" - -echo "Pushing image to gcr.io" -docker push "gcr.io/kubernetes-helm/tiller:${VERSION}" echo "Building helm binaries" make build-cross make dist checksum VERSION="${VERSION}" -echo "Pushing binaries to gs bucket" -${HOME}/google-cloud-sdk/bin/gsutil cp ./_dist/* "gs://${PROJECT_NAME}" +echo "Pushing binaries to Azure" +az storage blob upload-batch -s _dist/ -d "$AZURE_STORAGE_CONTAINER_NAME" --pattern 'helm-*' --connection-string "$AZURE_STORAGE_CONNECTION_STRING" diff --git a/.circleci/test.sh b/.circleci/test.sh deleted file mode 100755 index e0faf9c1822..00000000000 --- a/.circleci/test.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2016 The Kubernetes Authors All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Bash 'Strict Mode' -# http://redsymbol.net/articles/unofficial-bash-strict-mode -set -euo pipefail -IFS=$'\n\t' - -HELM_ROOT="${BASH_SOURCE[0]%/*}/.." -cd "$HELM_ROOT" - -run_unit_test() { - if [[ "${CIRCLE_BRANCH-}" == "master" ]]; then - echo "Running unit tests with coverage'" - ./scripts/coverage.sh --coveralls - else - echo "Running 'unit tests'" - make test-unit - fi -} - -run_style_check() { - echo "Running 'make test-style'" - make test-style -} - -run_docs_check() { - echo "Running 'make verify-docs'" - make verify-docs -} - -# Build to ensure packages are compiled -echo "Running 'make build'" -make build - -case "${CIRCLE_NODE_INDEX-0}" in - 0) run_unit_test ;; - 1) run_style_check ;; - 2) run_docs_check ;; -esac diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..68334cf337b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 + +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000000..595b5021855 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ + + +**What this PR does / why we need it**: + +**Special notes for your reviewer**: + +**If applicable**: +- [ ] this PR contains documentation +- [ ] this PR contains unit tests +- [ ] this PR has been tested for backwards compatibility diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000000..d2babcc38c6 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '29 6 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/stale-issue-bot.yaml b/.github/workflows/stale-issue-bot.yaml new file mode 100644 index 00000000000..eac66b21adc --- /dev/null +++ b/.github/workflows/stale-issue-bot.yaml @@ -0,0 +1,16 @@ +name: "Close stale issues" +on: + schedule: + - cron: "0 0 * * *" +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v3.0.14 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.' + exempt-issue-labels: 'keep open,v4.x' + days-before-stale: 90 + days-before-close: 30 + operations-per-run: 100 diff --git a/.gitignore b/.gitignore index b94f87b2685..d1af995a39e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ +*.exe +*.swp .DS_Store .coverage/ +.idea/ .vimrc .vscode/ _dist/ -_proto/*.pb.go bin/ -rootfs/tiller -rootfs/rudder vendor/ -*.exe -.idea/ +# Ignores charts pulled for dependency build tests +cmd/helm/testdata/testcharts/issue-7233/charts/* diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000000..491e648a152 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,28 @@ +run: + timeout: 2m + +linters: + disable-all: true + enable: + - deadcode + - dupl + - gofmt + - goimports + - golint + - gosimple + - govet + - ineffassign + - misspell + - nakedret + - structcheck + - unused + - varcheck + - staticcheck + +linters-settings: + gofmt: + simplify: true + goimports: + local-prefixes: helm.sh/helm/v3 + dupl: + threshold: 400 diff --git a/ADOPTERS.md b/ADOPTERS.md new file mode 100644 index 00000000000..9d5365b723e --- /dev/null +++ b/ADOPTERS.md @@ -0,0 +1,16 @@ + To add your organization to this list, open a pull request that adds your + organization's name, optionally with a link. The list is in alphabetical order. + + (Remember to use `git commit --signoff` to comply with the DCO) + +# Organizations Using Helm + +- [Blood Orange](https://bloodorange.io) +- [IBM](https://www.ibm.com) +- [Microsoft](https://microsoft.com) +- [Qovery](https://www.qovery.com/) +- [Samsung SDS](https://www.samsungsds.com/) +- [Softonic](https://hello.softonic.com/) +- [Ville de Montreal](https://montreal.ca) + +_This file is part of the CNCF official documentation for projects._ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 34b4f8b1694..37627e716c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,72 +1,154 @@ # Contributing Guidelines -The Kubernetes Helm project accepts contributions via GitHub pull requests. This document outlines the process to help get your contribution accepted. +The Helm project accepts contributions via GitHub pull requests. This document outlines the process +to help get your contribution accepted. ## Reporting a Security Issue -Most of the time, when you find a bug in Helm, it should be reported -using [GitHub issues](https://github.com/kubernetes/helm/issues). However, if -you are reporting a _security vulnerability_, please email a report to -[helm-security@deis.com](mailto:helm-security@deis.com). This will give -us a chance to try to fix the issue before it is exploited in the wild. +Most of the time, when you find a bug in Helm, it should be reported using [GitHub +issues](https://github.com/helm/helm/issues). However, if you are reporting a _security +vulnerability_, please email a report to +[cncf-helm-security@lists.cncf.io](mailto:cncf-helm-security@lists.cncf.io). This will give us a +chance to try to fix the issue before it is exploited in the wild. -## Contributor License Agreements +## Sign Your Work -We'd love to accept your patches! Before we can take them, we have to jump a -couple of legal hurdles. +The sign-off is a simple line at the end of the explanation for a commit. All commits needs to be +signed. Your signature certifies that you wrote the patch or otherwise have the right to contribute +the material. The rules are pretty simple, if you can certify the below (from +[developercertificate.org](https://developercertificate.org/)): -The Cloud Native Computing Foundation (CNCF) CLA [must be signed](https://github.com/kubernetes/community/blob/master/CLA.md) by all contributors. -Please fill out either the individual or corporate Contributor License -Agreement (CLA). +``` +Developer Certificate of Origin +Version 1.1 -Once you are CLA'ed, we'll be able to accept your pull requests. For any issues that you face during this process, -please add a comment [here](https://github.com/kubernetes/kubernetes/issues/27796) explaining the issue and we will help get it sorted out. +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +1 Letterman Drive +Suite D4700 +San Francisco, CA, 94129 -***NOTE***: Only original source code from you and other people that have -signed the CLA can be accepted into the repository. This policy does not -apply to [third_party](third_party/) and [vendor](vendor/). +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +Then you just add a line to every git commit message: + + Signed-off-by: Joe Smith + +Use your real name (sorry, no pseudonyms or anonymous contributions.) + +If you set your `user.name` and `user.email` git configs, you can sign your commit automatically +with `git commit -s`. + +Note: If your git config information is set properly then viewing the `git log` information for your + commit will look something like this: + +``` +Author: Joe Smith +Date: Thu Feb 2 11:41:15 2018 -0800 + + Update README + + Signed-off-by: Joe Smith +``` + +Notice the `Author` and `Signed-off-by` lines match. If they don't your PR will be rejected by the +automated DCO check. ## Support Channels Whether you are a user or contributor, official support channels include: -- GitHub [issues](https://github.com/kubernetes/helm/issues/new) -- Slack: #Helm room in the [Kubernetes Slack](http://slack.kubernetes.io/) +- [Issues](https://github.com/helm/helm/issues) +- Slack: + - User: [#helm-users](https://kubernetes.slack.com/messages/C0NH30761/details/) + - Contributor: [#helm-dev](https://kubernetes.slack.com/messages/C51E88VDG/) -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. +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. It is also worth asking on the Slack channels. ## 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` +We use milestones to track progress of specific planned releases. -`Upcoming - Minor` is used for keeping track of issues that aren't assigned to a specific -release but could easily be addressed in a minor release. `Upcoming - Major` keeps track -of issues that will need to be addressed in a major release. For example, if the current -version is `2.2.0` an issue/PR could fall in to one of 4 different active milestones: -`2.2.1`, `2.3.0`, `Upcoming - Minor`, or `Upcoming - Major`. If an issue pertains to a -specific upcoming bug or minor release, it would go into `2.2.1` or `2.3.0`. If the issue/PR -does not have a specific milestone yet, but it is likely that it will land in a `2.X` release, -it should go into `Upcoming - Minor`. If the issue/PR is a large functionality add or change -and/or it breaks compatibility, then it should be added to the `Upcoming - Major` milestone. -An issue that we are not sure we will be doing will not be added to any milestone. +For example, if the latest currently-released version is `3.2.1`, an issue/PR which pertains to a +specific upcoming bugfix or feature release could fall into one of two different active milestones: +`3.2.2` or `3.3.0`. -A milestone (and hence release) is considered done when all outstanding issues/PRs have been closed or moved to another milestone. +Issues and PRs which are deemed backwards-incompatible may be added to the discussion items for +Helm 4 with [label:v4.x](https://github.com/helm/helm/labels/v4.x). An issue or PR that we are not +sure we will be addressing will not be added to any milestone. -## Semver +A milestone (and hence release) can be closed when all outstanding issues/PRs have been closed +or moved to another milestone and the associated release has been published. -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). +## Semantic Versioning -We also try very hard to not change publicly accessible Go library definitions inside of the `pkg/` directory of our source code. +Helm maintains a strong commitment to backward compatibility. All of our changes to protocols and +formats are backward compatible from one major release to the next. No features, flags, or commands +are removed or substantially modified (unless we need to fix a security issue). -For a quick summary of our backward compatibility guidelines for releases between 2.0 and 3.0: +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 3.0 and 4.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) +- File formats (such as Chart.yaml) MUST be backward compatible +- Any chart that worked on a previous version of Helm 3 MUST work on a new version of Helm 3 + (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). +- Go libraries inside of `pkg/` SHOULD remain backward compatible, though code inside of `cmd/` and + `internal/` may be changed from release to release without notice. + +## Support Contract for Helm 2 + +With Helm 2's current release schedule, we want to take into account any migration issues for users +due to the upcoming holiday shopping season and tax season. We also want to clarify what actions may +occur after the support contract ends for Helm 2, so that users will not be surprised or caught off +guard. + +After Helm 2.15.0 is released, Helm 2 will go into "maintenance mode". We will continue to accept +bug fixes and fix any security issues that arise, but no new features will be accepted for Helm 2. +All feature development will be moved over to Helm 3. + +6 months after Helm 3.0.0's public release, Helm 2 will stop accepting bug fixes. Only security +issues will be accepted. + +12 months after Helm 3.0.0's public release, support for Helm 2 will formally end. Download links +for the Helm 2 client through Google Cloud Storage, the Docker image for Tiller stored in Google +Container Registry, and the Google Cloud buckets for the stable and incubator chart repositories may +no longer work at any point. Client downloads through `get.helm.sh` will continue to work, and we +will distribute a Tiller image that will be made available at an alternative location which can be +updated with `helm init --tiller-image`. ## Issues @@ -74,111 +156,140 @@ Issues are used as the primary method for tracking anything to do with the Helm ### 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 -Slack channel or have particular interest to the community as a whole. Depending on the discussion, -these can turn into "Feature" or "Bug" issues. -- Proposal: Used for items (like this one) that propose a new ideas or functionality that require -a larger community discussion. This allows for feedback from others in the community before a -feature is actually developed. This is not needed for small additions. Final word on whether or -not a feature needs a proposal is up to the core maintainers. All issues that are proposals should -both have a label and an issue title of "Proposal: [the rest of the title]." A proposal can become -a "Feature" and does not require a milestone. -- Features: These track specific feature requests and ideas until they are complete. They can evolve -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) +There are 5 types of issues (each with their own corresponding [label](#labels)): + +- `question/support`: 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 + Slack channel or have particular interest to the community as a whole. Depending on the + discussion, these can turn into `feature` or `bug` issues. +- `proposal`: Used for items (like this one) that propose a new ideas or functionality that require + a larger community discussion. This allows for feedback from others in the community before a + feature is actually developed. This is not needed for small additions. Final word on whether or + not a feature needs a proposal is up to the core maintainers. All issues that are proposals should + both have a label and an issue title of "Proposal: [the rest of the title]." A proposal can become + a `feature` and does not require a milestone. +- `feature`: These track specific feature requests and ideas until they are complete. They can + evolve from a `proposal` or can be submitted individually depending on the size. +- `bug`: These track bugs with the code +- `docs`: These track 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. +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 2. Triage - - The maintainer in charge of triaging will apply the proper labels for the issue. This - includes labels for priority, type, and metadata (such as "starter"). The only issue - priority we will be tracking is whether or not the issue is "critical." If additional - levels are needed in the future, we will add them. - - (If needed) Clean up the title to succinctly and clearly state the issue. Also ensure - that proposals are prefaced with "Proposal". - - Add the issue to the correct milestone. If any questions come up, don't worry about - adding the issue to a milestone until the questions are answered. + - The maintainer in charge of triaging will apply the proper labels for the issue. This includes + labels for priority, type, and metadata (such as `good first issue`). The only issue priority + we will be tracking is whether or not the issue is "critical." If additional levels are needed + in the future, we will add them. + - (If needed) Clean up the title to succinctly and clearly state the issue. Also ensure that + proposals are prefaced with "Proposal: [the rest of the title]". + - Add the issue to the correct milestone. If any questions come up, don't worry about adding the + issue to a milestone until the questions are answered. - We attempt to do this process at least once per work day. 3. Discussion - - "Feature" and "Bug" issues should be connected to the PR that resolves it. - - Whoever is working on a "Feature" or "Bug" issue (whether a maintainer or someone from - the community), should either assign the issue to them self or make a comment in the issue - saying that they are taking it. - - "Proposal" and "Question" issues should stay open until resolved or if they have not been - active for more than 30 days. This will help keep the issue queue to a manageable size and - reduce noise. Should the issue need to stay open, the `keep open` label can be added. + - Issues that are labeled `feature` or `proposal` must write a Helm Improvement Proposal (HIP). + See [Proposing an Idea](#proposing-an-idea). Smaller quality-of-life enhancements are exempt. + - Issues that are labeled as `feature` or `bug` should be connected to the PR that resolves it. + - Whoever is working on a `feature` or `bug` issue (whether a maintainer or someone from the + community), should either assign the issue to themself or make a comment in the issue saying + that they are taking it. + - `proposal` and `support/question` issues should stay open until resolved or if they have not + been active for more than 30 days. This will help keep the issue queue to a manageable size + and reduce noise. Should the issue need to stay open, the `keep open` label can be added. 4. Issue closure -## How to Contribute a Patch +## Proposing an Idea + +Before proposing a new idea to the Helm project, please make sure to write up a [Helm Improvement +Proposal](https://github.com/helm/community/tree/master/hips). A Helm Improvement Proposal is a +design document that describes a new feature for the Helm project. The proposal should provide a +concise technical specification and rationale for the feature. -1. If you haven't already done so, sign a Contributor License Agreement (see details above). -2. Fork the desired repo, develop and test your code changes. -3. Submit a pull request. +It is also worth considering vetting your idea with the community via the +[cncf-helm](mailto:cncf-helm@lists.cncf.io) mailing list. Vetting an idea publicly before going as +far as writing a proposal is meant to save the potential author time. Many ideas have been proposed; +it's quite likely there are others in the community who may be working on a similar proposal, or a +similar proposal may have already been written. -Coding conventions and standards are explained in the official developer docs: -https://github.com/kubernetes/helm/blob/master/docs/developers.md +HIPs are submitted to the [helm/community repository](https://github.com/helm/community). [HIP +1](https://github.com/helm/community/blob/master/hips/hip-0001.md) describes the process to write a +HIP as well as the review process. -The next section contains more information on the workflow followed for PRs +After your proposal has been approved, follow the [developer's +guide](https://helm.sh/docs/community/developers/) to get started. + +## How to Contribute a Patch + +1. Identify or create the related issue. If you're proposing a larger change to + Helm, see [Proposing an Idea](#proposing-an-idea). +2. Fork the desired repo; develop and test your code changes. +3. Submit a pull request, making sure to sign your work and link the related issue. + +Coding conventions and standards are explained in the [official developer +docs](https://helm.sh/docs/developers/). ## Pull Requests -Like any good open source project, we use Pull Requests to track code changes +Like any good open source project, we use Pull Requests (PRs) to track code changes. ### PR Lifecycle 1. PR creation + - PRs are usually created to fix or else be a subset of other PRs that fix a particular issue. - 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, - it **must** be prefaced with "WIP: [title]". Once the PR is ready for review, remove "WIP" from - the title. - - It is preferred, but not required, to have a PR tied to a specific issue. + important work that is in-flight, but useful for others to see. If a PR is a work in progress, + it **must** be prefaced with "WIP: [title]". Once the PR is ready for review, remove "WIP" + from the title. + - It is preferred, but not required, to have a PR tied to a specific issue. There can be + circumstances where if it is a quick fix then an issue might be overkill. The details provided + in the PR description would suffice in this case. 2. Triage - - The maintainer in charge of triaging will apply the proper labels for the issue. This should - include at least a size label, `bug` or `feature`, and `awaiting review` once all labels are applied. - See the [Labels section](#labels) for full details on the definitions of labels + - The maintainer in charge of triaging will apply the proper labels for the issue. This should + include at least a size label, `bug` or `feature`, and `awaiting review` once all labels are + applied. See the [Labels section](#labels) for full details on the definitions of labels. - Add the PR to the correct milestone. This should be the same as the issue the PR closes. 3. Assigning reviews - - Once a review has the `awaiting review` label, maintainers will review them as schedule permits. - The maintainer who takes the issue should self-request a review. - - Reviews from others in the community, especially those who have encountered a bug or have - requested a feature, are highly encouraged, but not required. Maintainer reviews **are** required - before any merge - - Any PR with the `size/large` label requires 2 review approvals from maintainers before it can be - merged. Those with `size/medium` are per the judgement of the maintainers + - Once a review has the `awaiting review` label, maintainers will review them as schedule + permits. The maintainer who takes the issue should self-request a review. + - PRs from a community member with the label `size/S` or larger requires 2 review approvals from + maintainers before it can be merged. Those with `size/XS` are per the judgement of the + maintainers. For more detail see the [Size Labels](#size-labels) section. 4. Reviewing/Discussion - - Once a maintainer begins reviewing a PR, they will remove the `awaiting review` label and add - the `in progress` label so the person submitting knows that it is being worked on. This is - especially helpful when the review may take awhile. - - All reviews will be completed using Github review tool. - - A "Comment" review should be used when there are questions about the code that should be - answered, but that don't involve code changes. This type of review does not count as approval. - - A "Changes Requested" review indicates that changes to the code need to be made before they will be merged. + - All reviews will be completed using GitHub review tool. + - A "Comment" review should be used when there are questions about the code that should be + answered, but that don't involve code changes. This type of review does not count as approval. + - A "Changes Requested" review indicates that changes to the code need to be made before they + will be merged. - Reviewers should update labels as needed (such as `needs rebase`) 5. Address comments by answering questions or changing code -6. Merge or close - - PRs should stay open until merged or if they have not been active for more than 30 days. - This will help keep the PR queue to a manageable size and reduce noise. Should the PR need - to stay open (like in the case of a WIP), the `keep open` label can be added. - - If the owner of the PR is listed in `OWNERS`, that user **must** merge their own PRs - or explicitly request another OWNER do that for them. - - If the owner of a PR is _not_ listed in `OWNERS`, any core committer may - merge the PR once it is approved. +6. LGTM (Looks good to me) + - Once a Reviewer has completed a review and the code looks ready to merge, an "Approve" review + is used to signal to the contributor and to other maintainers that you have reviewed the code + and feel that it is ready to be merged. +7. Merge or close + - PRs should stay open until merged or if they have not been active for more than 30 days. This + will help keep the PR queue to a manageable size and reduce noise. Should the PR need to stay + open (like in the case of a WIP), the `keep open` label can be added. + - Before merging a PR, refer to the topic on [Size Labels](#size-labels) below to determine if + the PR requires more than one LGTM to merge. + - If the owner of the PR is listed in the `OWNERS` file, that user **must** merge their own PRs + or explicitly request another OWNER do that for them. + - If the owner of a PR is _not_ listed in `OWNERS`, any core maintainer may merge the PR. #### 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 +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 +Each week, one of the core maintainers will serve as the designated "triager" starting after the +public stand-up meetings on Thursday. This person will be in charge triaging new PRs and issues throughout the work week. ## Labels @@ -190,9 +301,8 @@ The following tables define all label types used for Helm. It is split up by cat | Label | Description | | ----- | ----------- | | `bug` | Marks an issue as a bug or a PR as a bugfix | -| `critical` | Marks an issue or PR as critical. This means that addressing the PR or issue is top priority and will be handled first by maintainers | +| `critical` | Marks an issue or PR as critical. This means that addressing the PR or issue is top priority and must be addressed as soon as possible | | `docs` | Indicates the issue or PR is a documentation change | -| `duplicate` | Indicates that the issue or PR is a duplicate of another | | `feature` | Marks the issue as a feature request or a PR as a feature implementation | | `keep open` | Denotes that the issue or PR should be kept open past 30 days of inactivity | | `refactor` | Indicates that the issue is a code refactor and is not fixing a bug or adding additional functionality | @@ -201,36 +311,42 @@ The following tables define all label types used for Helm. It is split up by cat | Label | Description | | ----- | ----------- | -| `help wanted` | This issue is one the core maintainers cannot get to right now and would appreciate help with | -| `proposal` | This issue is a proposal | -| `question/support` | This issue is a support request or question | -| `starter` | This issue is a good for someone new to contributing to Helm | -| `wont fix` | The issue has been discussed and will not be implemented (or accepted in the case of a proposal) | +| `help wanted` | Marks an issue needs help from the community to solve | +| `proposal` | Marks an issue as a proposal | +| `question/support` | Marks an issue as a support request or question | +| `good first issue` | Marks an issue as a good starter issue for someone new to Helm | +| `wont fix` | Marks an issue as 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 | -| `breaking` | The PR has breaking changes (such as API changes) | -| `cncf-cla: no` | The PR submitter has **not** signed the project CLA. | -| `cncf-cla: yes` | The PR submitter has signed the project CLA. This is required to merge. | +| `awaiting review` | Indicates a PR has been triaged and is ready for someone to review | +| `breaking` | Indicates a PR has breaking changes (such as API changes) | | `in progress` | Indicates that a maintainer is looking at the PR, even if no review has been posted yet | -| `needs pick` | Indicates that the PR needs to be picked into a feature branch (generally bugfix branches). Once it has been, the `picked` label should be applied and this one removed | -| `needs rebase` | A helper label used to indicate that the PR needs to be rebased before it can be merged. Used for easy filtering | -| `picked` | This PR has been picked into a feature branch | +| `needs rebase` | Indicates a PR needs to be rebased before it can be merged | +| `needs pick` | Indicates a PR needs to be cherry-picked into a feature branch (generally bugfix branches). Once it has been, the `picked` label should be applied and this one removed | +| `picked` | This PR has been cherry-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` -because it requires sign off from multiple people. Conversely, a PR that adds a small feature, but requires -another 150 lines of tests to cover all cases, could be labeled as `size/small` even though the number -lines is greater than defined below. +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/L` because it requires sign off from multiple people. Conversely, a PR that adds a small +feature, but requires another 150 lines of tests to cover all cases, could be labeled as `size/S` +even though the number of lines is greater than defined below. + +Any changes from the community labeled as `size/S` or larger should be thoroughly tested before +merging and always requires approval from 2 core maintainers. PRs submitted by a core maintainer, +regardless of size, only requires approval from one additional maintainer. This ensures there are at +least two maintainers who are aware of any significant PRs introduced to the codebase. | Label | Description | | ----- | ----------- | -| `size/small` | Anything less than or equal to 4 files and 150 lines. Only small amounts of manual testing may be required | -| `size/medium` | Anything greater than `size/small` and less than or equal to 8 files and 300 lines. Manual validation should be required. | -| `size/large` | Anything greater than `size/medium`. This should be thoroughly tested before merging and always requires 2 approvals. This also should be applied to anything that is a significant logic change. | +| `size/XS` | Denotes a PR that changes 0-9 lines, ignoring generated files. Very little testing may be required depending on the change. | +| `size/S` | Denotes a PR that changes 10-29 lines, ignoring generated files. Only small amounts of manual testing may be required. | +| `size/M` | Denotes a PR that changes 30-99 lines, ignoring generated files. Manual validation should be required. | +| `size/L` | Denotes a PR that changes 100-499 lines, ignoring generated files. | +| `size/XL` | Denotes a PR that changes 500-999 lines, ignoring generated files. | +| `size/XXL` | Denotes a PR that changes 1000+ lines, ignoring generated files. | diff --git a/KEYS b/KEYS new file mode 100644 index 00000000000..89ef930fd54 --- /dev/null +++ b/KEYS @@ -0,0 +1,942 @@ +This file contains the PGP keys of developers who have signed releases of Helm. + +For your convenience, commands are provided for those who use pgp and gpg. + +For users to import keys: + pgp < KEYS + or + gpg --import KEYS + +Developers to add their keys: + pgp -kxa and append it to this file. + or + (pgpk -ll && pgpk -xa ) >> KEYS + or + (gpg --list-sigs + && gpg --armor --export ) >> KEYS + +pub rsa4096/0x461449C25E36B98E 2017-11-10 [SC] + 672C657BE06B4B30969C4A57461449C25E36B98E +uid [ultimate] Matthew Farina +sig 3 0x461449C25E36B98E 2017-11-10 Matthew Farina +sig 0x2CDBBFBB37AE822A 2018-12-12 Adnan Abdulhussein +sig 0x1EF612347F8A9958 2018-12-12 Adam Reese +sig 0x62F49E747D911B60 2018-12-12 Matt Butcher +sub rsa4096/0xCCCE67689DF05738 2017-11-10 [E] +sig 0x461449C25E36B98E 2017-11-10 Matthew Farina +sub rsa4096/0x9436E80BFBA46909 2017-11-10 [S] [expires: 2022-11-09] +sig 0x461449C25E36B98E 2017-11-10 Matthew Farina + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFoFERgBEADdhgM8EPo9fxnu2iW75r4uha2TrhWaO3EJIo53sa6U9nePIeWc +oWqjDZqYvIMJcylfocrVi4m6HdNcPrWo5pSWeKd8J9X8d4BUhoKFmJdHqWzgokwW +Rk06Doro2FHFyHoPPrI3a1HGVWA0xFhBYqSbim4j/Q0FouS566MofeRGnnacJ88z +Z7yErN5Gy4jk7pOgwvMewoGpEd8FMcyYSJfSjeoqdIZYp89EKTLbgQZuOJ9yVZnY +c0mtpH57UbkrkGv8hRuViWSO99q/mpMQyWQGYVoTV4QM/0q4jUbkRazaeY3N4hGC +I6Xf4ilWyNmmVODI6JcvWY+vXPtxIKjEjYiomVCF6jCYWWCA7cf3+kqJ+T4sc0NF +fseR/TAOkDV/XsZ1ufbSHBEiZTIjLvoAGJ+u+3go+UysVVCw4L1NSGFeDrZ97KSe +w0MeuV2SYfdZ4so7k4YDNbBLTVx0V/wl+laFtdjo167D18AYw54HIv3snHkjABfY +7Q06Ye7FuuKzdrj9KpmzUYnN3hRGqe84GIcM3D5+vElj0vyg8th32Dig5Xi38s0M +sz7hPg+oFk7csslMVAnLtWYvsv2FMSKB9FUHYv9AJ6yjYfyLlQgjjda0z6Sq5zpu +qVZqTNSxEIZFDKfTgQV6rocIK5VKP063KS6qwpHzPxKADaLTUPOWeum9/wARAQAB +tCRNYXR0aGV3IEZhcmluYSA8bWF0dEBtYXR0ZmFyaW5hLmNvbT6JAk4EEwEIADgW +IQRnLGV74GtLMJacSldGFEnCXja5jgUCWgURGAIbAwULCQgHAwUVCgkICwUWAwIB +AAIeAQIXgAAKCRBGFEnCXja5jjtQEADJvSx67Qz8gTxvUH3HaMsXaeb6BG3zLJXj +34pqAGNkKB4/ZgpFVYE1R0QuvYn9CbFpD1UcSank3L3xBroeOEUN3kvOg3D6Bv8f +mtwtW1TDjaWDTa0mZ8icanjXVNfK3K8pAwni2FPrW/tesEt/8GI48ZxPMzHk1qrL +8mETLRn1EBL3vq5qPDIK87XhhW9WAgwsadn6BQKSTSVVUACBAlV7EbqE4DHqhwYz +D1HrEIAtXkkb9JJejUnAbiOqPmm9s6iWC13K1P27FB8EEYiKxL8kb7xv5xW7+Pmg +kb03OqZtZYu9Fl1MF1zVQe4mXVflcbj7mYU1kb8vepD6bOUA89z8FggU2Q38cxkD +TYQsxpGwWz3nvEu29KbHmjQja1+G5D8kQ8bv1mNdiXQbOz51v2+7vowKKUoPQfp9 +n8Ez4dxWVrFtf218Mtt8wbYmmVYijLIBDArYKDeVqNNua8YC9641DcvRdCCvaYEx +Q9vWKjpAWmXKy2bb7TQ2TjGRh+Ly47z+PTluqUeYuBREAN4Hd4xwiClRbhb3I9To +YTJkPOkaOR967zBho5orA8xww4hcsufhjqsoU0/MGbG6jvJihHFR9Jq+0gVzakca +K8tGRSA8l5xdjow5dVOPzeXuKDPuvHEwa63TWsH5H8s6iembNT1H9bate8wQT1TN +9PH/6sthz4kCMwQQAQgAHRYhBFER2nPfEtjoEspGLyzbv7s3roIqBQJcET6LAAoJ +ECzbv7s3roIqozgQAIG5IqJ7hYjndCLW2MBLEa9oA04QSgF9qcqfiG00tjhBVwEK +YE6r7BUgC7r7dP1xVa/+5lVRATfiJ+Raq7udm/RQsamyp9Q8xBOuavPcJDZMX5m7 +OqPZMs+TDFPYM914GIWPAQf9ehaHHnmCNZXExxYlnZBPFsOcLYSNGH/xQeiA+q3F +tCOdRhjcpbt4rcx+Jq/l6X3cxstFwcYeljhvebblpwcVNJVArVrWZmosFl3rz3bs +PKfZKAvjV65knRkra73ZjN+YEYMMr6MzvVh/cnigk9XHgu5Y7imLv9qf1leyFCaa +oJoQDAcHIfs/eQmaEbYUyw/jX53/PyGqXlmkW7D3wqAGH5yx+ske7otCiaHHoTK0 +vHsEvO9b4dLtr0uMMNRO7St+3EtMa070s537XymG1HSeW8QbVEg/+w2YW5DyTe5p +WaNJS6WUc7UuIgEWvgitVxhUheZRumh5/EW673yI8iUchGslAuL1W5R1rXQfMPVA +BsI8D8pWs9EKjP4Lpu1Wgoxm0O4kaAxRbbHjrIYLtoRRrakr+kfqjZ/rJM89JQpl +NWNBZ61IDKROj7U2kLAxCJSB3RfAuqinyFGjxod7ENW7u6z0SCdupybbmylAfD+T +t3Z2DBB9tjxNnsgb2pbcm8cDGrJOZhIDdcVChvMXnHNxEmXbHvTKocci0t4viQIz +BBABCgAdFiEESdCchsPcjaPwoHYiHvYSNH+KmVgFAlwRP38ACgkQHvYSNH+KmVgP +rxAAkhggTXggRwpWzgU7PRsj347DqtH3f/2EfTOhAi6PGOiw2EFocTrx47WHAjs6 +XFT+c0yHCv58fGHKrrfeOT1VCjk2xf0NSdf00CTHO+DqepNiXzFYCJ0fUTL3w2JC +ugrfhwEdVH3TYJffFlmi0VZVCrGT3ZU1H+N/mVcd4FniOPWaGYoSG15iift4cAO/ +CynMFUbl5NYCuE/z9lR8o/3KSu7vuffLsvXdkxCX6fjxkSWcBKgH7ts7OWyPv9H1 +r/I295CoG9ZmeKVtScY7lamb+vOw9ryHbTACo0aprPQ1kCjr+3JIJdodNkRQvzZX +Ayxmc/zWSmPlJ7zjVkmoLaU7YmN7dPaVpQiELQGKhm/TyH++ZxoA4Rw4dwtqqk86 ++F5ncsqJ107IW7ce6lnZVEvUBD4DHkMRQQZOA9hWBxVeDznjXzfpNNTB07mtzArG +nrbbnNu3epUPthZlhQ8C+dZeBOfGzyr3Aj6CQqKMziiL2Tf4Coa7PhHRBs6rf1PD +xNhnnybCvaMJEMSyX6b/lqb967yVI6g3TXQvi0cGGvYmwEBOiKkXSRHtQBjC1Ocq +qUjzg1dvyfJu84S0kSt2oEHL5n1TAvIrwqNNOwS6CL0x2pSLOVhZmpummSqybvsF +YJjctDJvBA7URB9asMOK3CS6UsJaVzUFkybxaYIdUPylh1mJAjMEEAEKAB0WIQSr +olKVmPZibEINM1ti9J50fZEbYAUCXBE1mgAKCRBi9J50fZEbYEcVEACOTG1qO0m/ ++8T2S8rskKDrgoXMi22x3n4SqdKIA5TwWdWp18nVyXIxUWvI1cS73WupHNtEKTLc ++yObvNo1N3syj/5c14RcRLUcWTFKs596TcUP5/xNH33j0nFplKplBP4MegnduXsB +HibxiEycpkTFVxc3xbW9KeWSzqEHxxOXE1okL0SDWTj/oNRToaDc4zdm26veZd25 +ycxqRkksZZCPuczqb2SB/mDqHx1jl4z2B6CzN3OUzMk40a77xwZXKNGTO4+fMEOJ +Flch8YQXh+gPbS1F/Q7qCrQOkhoV3nI/0CxNgWNcPrUd52xtGHzgxbdrgT7L0XMO +/KmIu1O8E+znjOxcSAklwh1xLsT01193vbVyW2pcmmtqo1ku0taLlw4T7VHQNb88 +uOKucXlA10L2lFFnqBWLOuZDcVpgywMjIrKTPoEpDcVPaBUDQCFBZE9ogA/Edhlo +mxGxhtzG/O6wwFcLoleMH1Lf6zMxhwOAIvkWVjsuQ312uVy1RNY7b3UFrxOw8/qq +UBy6AFE/dp9PF8BIQ37NHKeAlvCexEedwJi4RwH0hUQkBhxBeNrTOEE7cCaZ9Shz +IWhPKxSRKKblYY4fpDzl2uMBwdetk9jfZF2ofoSOKXTVh+YJ8PzncD6xJVesbMIW +0aPkERdmz8JeGBclBR0miED+zidofWCgD7kCDQRaBREYARAAqiqhYIA3ci/sJ7y3 +mJaQ/lsL2nsy+RgW52ETpLp3tIO2r3rxNn7CB/kJhPimDIo4OJSV2bl3Sr2llgwX +PrBQ+Z5bCUV70uc1U0vvJEW/r9tkyOu3YV7VXWXtaQWkCgxIqWgNJvU5A/9/6vz9 +u1RdMZwxpjy/4HuWvHYRXlJmeeca/BEoaYWMRlECuJjIBcAzuVJTlKBT7x7U4Ptc +qqZGbzr0+zU39y1kMXu/ayldlsF3k6DKYZYNaa8cKNqorV0FqBVm1JZSjiAAWqGp +tmYxUmv/riY6cP28tP3G6noH1XqzEvZ3fdYIsGM29YQ1Y1vrVrrBVju/aMzss498 +czxMtp8e0sudHt+ommUDkA2WBEPuqJPIcOj+7bvFiv6smyxcU8VmsyEapknq+Dq8 +wG0w3fGsRdy8puc5COz/3xuiFlHQ97wtnnmyWbmdQmx7EfZcGWFfnK6HwEXAbcjO +aaFwSISK8ROgqoKfTss6/8Go+vbmtKJQH2w1fQArnPHGu9qFM/sBNhZ+ieiZ6x1H +CdU3qvuycFZMSsMhk4ER2vJdeJ8tu2jUhMOIuA/VUgUblCJkAaBE9wXaiibCZ/XT +XBXVb81v+EpLsoc5G/wrg35D5U/Gqqc+KAABK2zHa4L7rIs6jb2daeRrUBytsWm2 +Exq5sE1Uf5mioHtZpbr6rKIGzT0AEQEAAYkCNgQYAQgAIBYhBGcsZXvga0swlpxK +V0YUScJeNrmOBQJaBREYAhsMAAoJEEYUScJeNrmOb2oQALYcLV3wFFR5v9zpEPdS +haOIpYyuFBkN0FoID+w7Hb7R3pyl7c6nLI9tyFEkJBM1faGke8vKj6HZSfcyX1Lo +2rBL+yW7Gu8z3uEbkTnPFew9LnutGFuFTnbpVdLcpsbm2lG5yhdmjvJBKI4CfX4Z +UFlhyGtwqsl+1lpUgvOuMI2HjyHcFbzkhiSRDQvtXCgJu6orjzEvqiKNM4MM7PMJ +AwU0Lf3NV/p1H2mFllfotmXVZ/TjXuGcOYH56gcf4XpkuD5Vb2Qhu7IbR6TneC5j +yPdC0yQYcXqrpYhNBmlbXIoEL1m0xXhrFVPxS3QeMfkhQOqjvhaxBGCt29YJaTfQ +ugN7I1YfEJIxTap8xzEdJ+80YL3iNCIzaWSsd/xUKpobHSsu4RU1cv//S+5qD3WZ +NfcUoBgmfPC7NXCoKrEVXk5QKh3efKnAkMQrxdWRiwSuenf4Yk4fWXcTyCXsMPVB +qjcZRuOpow7tU9AuBoMyJ1XrznHoubdnc29iGN51Hrhvp/uNxjsCgPgQtpL/8znk +dgfzXU5CYJDYHa6fubUTHVZfLKbzBEI2XY1nqVu+QEO86tkY9Ef4PFMknThTAJDC +ph3xIx/sBb5s3c/XH9JgWEiyO3rMEzZecgF34OJgwnc5gl63a4k1cF0cxzkCZYi3 +k6XI/RkkRzdN1CSdCapbDJDvuQINBFoFEeUBEAChZUqlI7FLQIY6GEo0bhJ4oMp2 +jQi22zb9ZmqqcmRbWfNKfCfm/cXNDabccqzPRTWezq6hVYYPz6cSnzXpxPBIQufZ +IoMVLKDbTS0RTFVwQsYu9qGdZ52J2bq6qMWK0I2n6lECNkbOB0bZ3aPxe3yw4McP +6u+SU+b0ArMvIGqq1cmKSpkAQB0kBK/gGzEj26d30jMSN393BZ/ESEs7PZyaie3O +CdT71Cmh6xNxv0IwmgbUo54diXL9hEYTrI3hPyCKFeAoiTjlpz9ah7DPoOHgd9lD +Rd4a6VdMrdz7m5aFWo/NVuoty9spGYLG0p9N7zSaUAdO/96mn+W18hbL7EkU7/Db +Ubt5ZP34YOI46aI8YRZKiTq6NI4WglZDxu9PFGoCx4lyvhgKOwcQHySverAyb0Y1 +qeNCL9uk6oBHB2bXlAhBBOORtL5rGD+ICCuCV4g1ZEoN7sJBMxNMXORzRZ1crdlr +10lld/Mg0udl2Hgatfx+i+Y0ae/W0Ibr417H5q7iHr85ivTQ6mRU3hMuzQSoWZK8 +vixjvOK401Gre22q5jq1IPinACcu6VUto9Wbo8C1msSsWgHrqLRFeqp18BoIVY5s +QCvcsGlyD7MdJQohpmJ7al/kNVOidhGf7TtcSolWF7gLZacMRYbGWhbDhpOIhIpl +jiWTg8oWRl9KPbwzBQARAQABiQRyBBgBCAAmFiEEZyxle+BrSzCWnEpXRhRJwl42 +uY4FAloFEeUCGwIFCQlmAYACQAkQRhRJwl42uY7BdCAEGQEIAB0WIQRxHyjVEOHg +vL1fa/6UNugL+6RpCQUCWgUR5QAKCRCUNugL+6RpCSgsD/40XzObgPRpbIRQaJL1 +FgynrXUh3dJHdqB5Yi/pYshFuI+nnjpAGTyYyk75WlfvUmzY4HgNmh9yCjWketc0 +SdulPkWQ093Y38bQ9WGVQ7NLnZ47AUTuImqEdKcR4wu9F3nGD+cyNWE5fao62tYd +hlzrP1rLz8kALtswc9PVYLEKnqNCBtlGoWdeW7K1lYVG4666/uYvHzOzsUQ0MqVT +HDjpvxEcVRA0EW47m2TVj6IYAsM+0J93aFRr4OKXf4bu1ejxRz4Pdx73QsjeZwlN +5F4FpnmegdUbNR3azeGcF0qiOjPCNu3xi5lDFPKCRZLnCAqMsvv92Z/GWryNAuDj +H9tsmbDUwYXc1QUbdsu+p2jVm79yPgJUIvcy/kwOd0/GYUDOme2NvhF252aOO6Mt +OnTCrQoX0mIY/IisIjwi+2LEpQVyNDu7AGu581LYFGhBDUqiy5CyQ2neHS+k9iq2 +06dVdqETpiybizUZm2aQ8FlRV0j6PVKrqAzi0cMYJC+Gh/fNvx61goJ1tEDdh+LK +Mw0Js7OCtH7Wu1D0U/qDl3137PIBSv10BZ3SkbZDqivV5YhyGhvEewiXsbamE6VZ +AHGZ5pfd/0tkqAW9UQqw1AdqYBsAtE4yeU63xPcz7B4VyyIdRNxnjQiEg+SEpDyy +Gl2kGtt+cIbEYZovTrrW2cM0FzGhD/4rRIDfd+IvhZ86BbYoIv4oreiZVjIhFAYI +7e0DfVliBXNOHFErghu3FisUrfTM5g7RHA0Snk8OGO/Yu2mSXYKVvygIlfi3i+7B +0eZxhZEOsHXgO3v4WtY5/67Q1XXF9J7MY9Ke9gqp0E8HRFsECfEoSCRdaaic5PIT +veUEkHs6q6W+J5ULNTqdWsmSdgNWQh3Zbhh0Ih9m9nioAlZHaKnEZXGt8GsUimr7 +ffRuYgxF+kuWT8UwQu0Tc47QrYgZIpxH4WI6Rc6qKAo/4DLK2Q3Y15kJFqi8He0t +U7fWXMtrdQxxkz94WTFokISVVRZxSfZ8VkGjVHAgk6NVBgp+2zjiwfwS16qbOUOY +ikR3WTCbyStdePLaXgAFxA7g/pl5/f0IF3/IoGdTGjWoRqnBZG7NfP7bYF1CKe4f +a87Z47LriyL70BFosJqBNMJUEorS9w8sBbnmMUdpGMyk7PH386W95ib7AEOtRttL +uzYetY4LljxgMsloRgYX+Kg5i6fkntG6rod8LNYg7jWObWaIqlPoTo1RNoujYAnE +qdCDQHoUOgtZ4v6+QaxI3WV1KPBsPb7SAjuphubIQVK/6qHse9OoWVwWAABXHFqX +2qV4dyq6mq87ohTcRrZqt64ekD8H3Qe4xkYSzsWZTc0qovhs+G+dSTJ709xuV2EP ++YMbPW0/IQ== +=g11H +-----END PGP PUBLIC KEY BLOCK----- + +pub rsa4096 2019-05-15 [SC] + F1261BDE929012C8FF2E501D6EA5D7598529A53E +uid [ultimate] Martin Hickey +sig 3 6EA5D7598529A53E 2019-05-15 Martin Hickey +sub rsa4096 2019-05-15 [E] +sig 6EA5D7598529A53E 2019-05-15 Martin Hickey + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFzcLlgBEACsmjtsbfMuKiKBl3yV5FsQBxvmNyhIwUJMtjgm5CMFcOLD+jDw +mExfsE8sM5fqfS5P7NFHn3V6NY/GyKNH3DZHGhYwDw/vG6JfHo1s9IzhjySuWEtL +7GUCJBKXk2cDfk4p0lHRgEtoYjG/sRMgk3y7WTR/W0McxllcrQQBB3RREbz8y7r7 +atJCeec36SSZgXqsyXAESx5dx7qRTdIwObPTCGxBdj2ZkgzT3D35EExdi9I8oM6L +bYOyUPy0aEj/FX6HVBOIWNGB0z8TYXjwY6/3gJG1JhaFZK1zvYogJ3p8jO07bTwo +/AzYAG4NoV4TqTyFPmb0d0+wE+lZOWA3FfF0YtYnNe3KPmPJZ/TXdTO6kle24UTy +Q9GK2s8QB3V9NA09/YoSF1qdjRfL5jo7XnRJztfFgIqW118I4EKSF+kz3hCMxH1Y +iCvHIHFQs+WX6g1bXHDI8JWe7VDiCVYwMxap8o/vtEKoETH9fjOEO/f/YF68hqpX +7eYTacDEV72qikHz/O0hNyeS1m/AnavPrd5RQi53vOT/KhwM+wC4a1bAywQUDZDW +KkSEkTqjzcSryj3DJR6EZ9y4F11Kt4TZoxHvh59UCcVyaTZPl/YdcRWom6eGo/5U +K1MFeF7fTK9ZVuJnvG6av2/W7Sbz9KaJxLHhUNAQ+ytdVkN9xfXrx1HP7QARAQAB +tChNYXJ0aW4gSGlja2V5IDxtYXJ0aW4uaGlja2V5QGllLmlibS5jb20+iQJOBBMB +CgA4FiEE8SYb3pKQEsj/LlAdbqXXWYUppT4FAlzcLlgCGwMFCwkIBwIGFQoJCAsC +BBYCAwECHgECF4AACgkQbqXXWYUppT5IFA//b64QqKN/ookqqeKEUMUOMoZUTi2t +4HPtzX/nqOXDb0zyIyaJaJlgxz+LuoN8CrSrwnmTY/ibKsFS7xkFRIeKYSb9b2no +NPb8F0SVtxYFQJ8d4WU1snAWFJd8aMe3+z8w15Mqz1Sd1lS/sN5s101rbh8jtFZD +NnAZqyfUgIhVq243XfhP4/mHPinpXjjF+APlMbdsOqnWgxzp8E9hpCd/YLb6KY0j +JbwryzH52ha9ZDMdMipH557+Xutcl4Wyn8RsJy38J0qBvy2p8AMZIYotw6pSCedi +7Iva+EitGSXXgRWbR6O68JvUgrFDOjcPKSQy7AlwhTase+b4OA9c3DgSxR5SMBR6 +OLYaIuDeVY2Zjr0ydFdxrfQzlHget7axRH0aaMimyCNfRa3HJea8ffF/Ssv2meUF +IPIhYLn7SBrVoTISu38S6WkhBBkDiHAW7nqV+mWR3cnVjIzIjW56bI06NZ4kqtvk +D9TX7b+KV20cSjjbSGI70023oHFoJSpLsj9+otvPwNrYC2oD0qTLBfNMkpcktnnw +I2uynQrPNbQVeA+cKrECJeyl2yAC4WXvP4ZefvFZX6RnL9HiiZ+pDyBt6Yq3A9AA +NhRd8zEAKNwH88tFmWMinTzCZz04bKvql+E7A3MAaR8WS3BG3JfLXMqOKiMfCHr5 +4Gn3rD4UGtFfxoy5Ag0EXNwuWAEQAKuxVJDOjG+xuaaO2Z/6BQfTaz6/zgzql/pR +UHInKSt5ts2LGdRhfvsNBzGBhoneLWZ8PivHRGSZFsFj5Nzy9/DIkopdHSZhP/zB +aqihHgFJTKxKBfrhP60bYQGBkHNMVwqbFuck24DUCzrMyJXG15f252aY7ByCIIem +SHbmPww5q6HPEPS+hHE4ka4N4s+vqL+oK8ktq7lnZCX+AZ4jIuMAoh/C851hLcr5 +EK+a6tXa2yRJtJfj44GX6+nBVm2w+3eHqOpD7JM7NqWmo41+qg3t2J3zHQf/0ejP +ej+OcVdEBD5zlJL+CNZ9PCMBUOrb+IbqY3ybmJieipOJtOCY8nwUyCueyTmq1tso +OwUsGB9hIsVY11wNgoNgrA6PhExGxcM5S/0Rt4+y/pwFjnqYLXBXyBSjXzzmpjhn +zERjmANlI8QLKHDdShgboDUt3Ynw+D/peTS9iJMIPuUTrcGcKgw4+6FNKACnJ5l7 +Wvz7apgD8QmxnSZMquul23bGihhbQMITWvdF5KEHE06Ah1bOzB3KXBEVx00Y0tO/ +hsY8XH4T/pEKv9FsIF6R4o2k/xm6jR9eZutABVIrizMHkZzjjo1ZC8b15olrZvLa +/DtNHzV5nPPSvGZPcey9BYk6b5GGCfT/EiWtJz8Nxm7/cCYRvuuZnGCxriH6XPww +v8kPNihfABEBAAGJAjYEGAEKACAWIQTxJhvekpASyP8uUB1upddZhSmlPgUCXNwu +WAIbDAAKCRBupddZhSmlPikmD/9UrspSeSjwaXSj2vCpO1pWm6ryVQc2ZzyMnXvq +j5HLwzaVsN8HM/YADK5FL6qqhxrROOZdSHjS92sxk2Rab23gGRKbwDUJmerheZ4B +ZXG40fDOPv45PZ8V0Kn9bzliNpPBFPjoaI8X1AKoIXyUqEy98Y/zhnLDhW/+yPrO +gznPfO5ds75+u4xOx9pTfGpdwt6qhfCdNHUoZWsAw/6pafqrCIvbHjGvmMJyYENS +dl6sPYBeiDkJkH67sGvJghjedhNznnXJ8+sm701eTqZkmpxzc0jvzwgnnYb0rAzS +uU3QNj9w5HcGQd/pk29Ui8A4VWLJOUcDCVa/CIQMQqQDPYJKxaj7XgE+dQ9MxQ3a +O0wgpEo2+4BaZ4I/qP8CgaE9q4IopMhNKPR1IeEFUmTsIzLVAktS/InshFWWUp5e +mEss8kiqxU9bAGZvWopllCaPJQTDZElQpW84Z0afyVLPp47CoKcXBSMsITFt3mRf +ZXAA6h8UlSgC7FV1YT4p6qsHqQ3cLERdTSrQFLmaCb2yRCR2V9d0RiMaIwUmnbld +g1jeR4weO3LLghuWpfZHruDrDU2ZvOAObQIQdHBFmCHejA/gilf0MUdJ1h2gApuJ +m3MUub704EDCTSqz9LJc+4/NbA2esZj7mExCtsMEqaoHW7BU4ws6BRHTyeHgi+Le +1qneNQ== +=oCPv +-----END PGP PUBLIC KEY BLOCK----- + +pub rsa4096 2018-03-14 [SC] + 967F8AC5E2216F9F4FD270AD92AA783CBAAE8E3B +uid [ultimate] Matthew Fisher +sig 3 92AA783CBAAE8E3B 2018-03-14 Matthew Fisher +sub rsa4096 2018-03-14 [E] +sig 92AA783CBAAE8E3B 2018-03-14 Matthew Fisher + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFqpgxYBEAC1+yf/KFw2AQlineurz7Oz8NyYMlx1JnxZvMOFrL6jbZGyyzyy +jBX5Ii++79Wq1T3BL+F/UFhgruQbbzL8SiAc8Q55Ec7z/BVxM7iQPLCnFRqztllx +Ia1D1dZ9aFIw4P92kQgOQGPOgIxFRwEPA0ZX5nbZfL/teNhphW7vHaauk9xEJddm +Pyy3l9xCRIKQVMwuCaLeH0ZZpBllddwuRV4ptlQ30MpOnaalQda9/j3VhNFEX8Nj +nu8GHn+f4Lzy6XmhHb++JB3AIo5ZfwaUS2xMrnObtvmGHR3+uP/kblh9MzZlmL4T +ldclyGaV7z9Z/xGwnX/+r7xna/fr3mey3GXm29BOP2sUBBQCba05X5nYUd2TjWsZ +OZtE6sLuzUzeOTLEDu28IJoiaYnLKDNzDmuVM26xAYVWXUdCGgn+1rAp0t5OGgHm +qTexvPmckgp3yw+tcPUkR6nh0ft7pmeoK53AQHMt6fk7plZCTuu5UvxZE/oDzt4X +w9+vSTD5GzsNGrTYLTYUSL0muK+iM/uuJtFNJUREOucXfmWxulUsxwOB0st7hnLs +4JmFSr3av1en1WqqdiXswOrdK2msTm4J2+fsOU1jnyF//RJmj+1KPpRDCBTzpAFS +SzE/rRaLZBVE8k2vT0L6yBXvGJ2ONK9TkGT5fnyXu8zDu1d2Koj0c+6m9wARAQAB +tCpNYXR0aGV3IEZpc2hlciA8bWF0dC5maXNoZXJAbWljcm9zb2Z0LmNvbT6JAk4E +EwEIADgWIQSWf4rF4iFvn0/ScK2Sqng8uq6OOwUCWqmDFgIbAwULCQgHAgYVCgkI +CwIEFgIDAQIeAQIXgAAKCRCSqng8uq6OOyTsD/979LDS7ONHIHNoRf7Uud40To0S +/domtZM0rXUCBdbe5R4/xah0HvM1u8aN4OC6U7i0LCXSmEOZxQLKxKBWfX4/d6k7 +lBwuQBSlcM6cM6nDfPInT0C3o8caP8lOGeNAdOkMxrqiEO4gHNP5BvWCV+jQSU5X +uvGhKNTMcpaf+DqZAFbR6zpdL7t5JCK0B0RRhFfaGWb19t3REukI5OF5M5SN7EtQ +XWK/1fyzsltrjTSXgMWuxtJjBchltjme/S3XpHeeoSCm1WWh3a140tCC662ydU1u +EZIlUrn8dfMpH0BY6bb0/4dhHvCJ3bw+zZoCzFJM/LksjP5i+Q4mUOD8PvFWh5aS +46F827YiMdqD/eDMr1QRe66fPw5EtWTHgnf3PX+NmN8lgn2o280AkRXqkrCgl580 +B+lFwZ6hfan2F8RIHXNbF+9Zvc7Nh8bG8s4I8s6uiufmsmOuFdp47J4//q1W0HcU +0fqajDnEhExtGkgwIsum1Ndwq2sWZT/ko7PYyC3J6mbr/MXTvd2TxtnMgG6kpyPv +p3HlDaBw1aO5vO5mji4RTsoZi12MITIyvPsFWh0WtXkJLNaJ30bFSEx5fiJILxu0 +bBoBK0LUhB1Q+8G3Kea3+q3MuOQFnFfjPlMH6q84jpU5Lv5BaW17IeZ2kIfVYrcG +vBvtZ5VHDzY4EhGmlbkCDQRaqYMWARAA3wYv6jbE1PjXwIUWSSO9zxQLBKg7Cn7d +g+wwKx+N5DHjSdQBous6DGwN/wEZfXJOn14S9Yg4p4owmiyJDn0oqJ0BLdsMELoO +imCIZ+zn3AjCWdk2b0oCOhyTwhaVhVgi8yMQruMSUG9/3lkVoFae/GMC32nmE2A0 +BOnj9fVIhIrDKt9OSeTXXRNVaRvNFo9ry8S1hDxgfQ2unD6J0mMPhLH2O7CRZDFW +FyH09E/rhrIDvI3Z7mZw2ufGKR0YEu7fJ0BBBSbIqUOMsUnQNWomb2j/QZyYmhTS +Hg9YRB807H3b+5GuZim+DSUk5DQV2IENEg9LDYvhDftE5COYB3tZUnvEpOvNybBl +URxD8Kgqlb3j93l2FcD1QrIGW5VCmkkuD612ZG+NjMq0ZXlQjv6gxAYir8GTKkWt +tS1OatDm6qe6xEFypT6nlvxOYFxLeFkVVGt4H4QW6+MXvnwMofL0G6fOhRvdlq3R +US9n3WqzTpCwfvJs2lhYi+c3/2nwCx5G42OT9Ix0UFkYwxhGk6PRleKOMsw28PFr +a8DVjyKGOVn+9auVhPXYQcN0sZqFl8LBDkUtaniiRD4WKH91aKYgmX1qo8sJZMhx +t/ZoHOfoHDEEa+kLqfsWu3htyTP1gleCAA8kDcRiy1v/G8v3+p2ioI6q1qegigbr +AqTHcWNOltcAEQEAAYkCNgQYAQgAIBYhBJZ/isXiIW+fT9JwrZKqeDy6ro47BQJa +qYMWAhsMAAoJEJKqeDy6ro47T7gP/j/3R9hPg+kJCErlEKPqxsEOxxlaHx+f4UGg +Zm+P6QK2SrqbrqcPhoKUXeHlbCMm2euxKTonIawgCIr44kCZvp3B8pCGUCR+M0mf +aXGO1O6EJ3MmtlbXJ+OyBAhxpklUWdM6favuzi62fAmvwEKQf1reG/9r+toJb5N4 +KwrrdZNUaLJWhb6D0fwB+1fWJbdRnDO1rozcA+YJGhhunpxF2b2nZ5OtqNuGmbqV +ofxL6/0lM4HqLNcUBlUyQihjk1+hzfWji95SlzIxP2EhH6gJh/e+/EDCaVVV00CM +0n/0dEB25nAuSMGgUx2utNmfCUP84IErGzSUlXdzN20aW5xiBFU3/uSWyz80IGuy +WeyRzksmphGdLwef+sWLKGrOJh+DkOxxpFMRaIqGEG2YViQCg3gyzjiJuI/XAdlK +AhqwVKfRke24vgifd1tN+zeFs+m28Hpw7989vky1hDvqdpK5/fiJfqIBsF0jir/H +AgtqmbiqemX9rUa3uDkBsvyu+Ou41l+wL6ahj9Pnu0+9hQnpeZERIyhq4LWn7gGb +xk5y63wrvGbeS5lev//012oSzWQfSdFWqQVzMTVtOojGFWgvwRCwZiWEPQkRIV5r +VNXtXPUdKiOEkWin01ZrwDPEyBjr3pcnu2mbgLeJETODnCRi79KA5kCtd65JbNF7 +Qknjx8fW +=jz9T +-----END PGP PUBLIC KEY BLOCK----- + +pub rsa4096 2018-08-06 [SC] [expires: 2022-08-06] + 76939899B137D575D3274E756DCCB9D752D35BA8 +uid [ultimate] Taylor Thomas +sig 3 6DCCB9D752D35BA8 2018-08-06 Taylor Thomas +sub rsa4096 2018-08-06 [E] [expires: 2022-08-06] +sig 6DCCB9D752D35BA8 2018-08-06 Taylor Thomas + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFto3pMBEADAO8mWocOAqBUHtiLBnht3+vLnjLv1LNs2GBdMCDRza51/SzFN +NN5pAETGbFl11zxpm9rBkyjI2xVO4OqI8TNIn6vYPTh2YVBs9UB+qRqjJt94fm9C +tWdQ3/27I4PPrCIw5CxjLKst/GO0BjS/J228wP1JtUeyf/QH9K8hDFeov0y94IMM +s7NFRkqZJ6tXjlDCJnDkPm3wERgY3S2I8bgr/BlGFEWCmjqD75PqHuJYjh4mmXhk +KTeYcJh42INPzCXd3bnvF0NwfmAE70fsSOZz7H3Ox14Gs+Tn+jDC8+Or4CCaqtyE +276d8yyyDXBlDN9IjwhjlJPfx/zMtvD+lAkGV89NwbZ+YnyUNenK2V6H86Efe36t +MxvFCH7rOKjCNjKUE0NUbxXfYig5u6xuZKcBcJmjXbmL2dFUIaMzm8jf1NlLuzjw +k7IVAw2Y9ZcVO1eNgeVxI+NdRsdz8qgBmDTvRhxh2n/ppc+5DDVhiffGqqlIZmYN +NJ2bUhW0x0R2OHgOedMyKnYDGgXPI3hnmY/t48ErDwxTqVNoo2tVU1YWS3eP63oa +8ZAiNsvYVWFWIAUi1Q+ADAhj1GdISg4VU5N97joFZA/POZrRtS43OBDIvCpaKjF/ +bu2EltTVBdGZj6fW8xndZIum9cIHMlMi+gyq/o9kipFyZ7zVEM0SB8aI6QARAQAB +tCtUYXlsb3IgVGhvbWFzIDx0YXlsb3IudGhvbWFzQG1pY3Jvc29mdC5jb20+iQJU +BBMBCAA+FiEEdpOYmbE31XXTJ051bcy511LTW6gFAlto3pMCGwMFCQeGH4AFCwkI +BwIGFQoJCAsCBBYCAwECHgECF4AACgkQbcy511LTW6gkqw/9E/DZMckYjml9gN6f +Z7jyZSzO9zP2pVKvcPvaXU+kcyKPR6r6seYt4uSOdosSsZs/xF7aSPoMezDyNli+ +W0t27DCXtnbk+LYptw6AaevkUF9+Cxe/gfXSQDxU6jtOV00KM4WkJtJ7Zty1dvk3 +PsnpPhbxUAWwULy0wF9Ab9RAXMyz/7TrgWP70EY1G/KkETUHTdSkxaoUPs67F9Y8 +c5qVQjgFVqSeN90h58w/4SF7KkS4EOy7RRyfzaBuyQPPi3fOtvsfAY/cSOVn2PBF +Pj1RPoTREKEa0nnp9TtrlwP7v+ooIvwDeemjL1c6tlTBW67T6UM+W5hcvjegQg1h +uLOdRtiN1HlTvOZngtegvbegGviwpdXahrNxN2mtYCAAYNELNyQOAWERGF9TUKeb +OC1HLbZwXdmPiUlUfPN3aAnMH46qe7eSMAZK203ciZlUxowFuE01X+M3WmLESdP3 +dxv3TACiC55mGBgZm/d/1CK83KBWMlzbgfmop65xbxi/tmpJbYdqoTeidYtUDo+L +IzJVjagvfED49o/U86C5DBr7u0mhZqnAxaWEWRBRgFi1Bnl7w3zSYYhdwGjiYTJ9 +/hejac8iqWc+RC9AJh4HW6itB3jPoEI90aVb1y8hm3UOBQTMEnI+dpvZEQPWSBnd +tWzzQS7et8Tlq0J4/wRVcEXAlmq5Ag0EW2jekwEQAK3KxoH8N7Qc0vSkMQmo/NfO +lEE89/KobYLDvyQMfXQJGF143eaaW2IHcE6OIT6E9IX9vnt00Lfzm0Jwdd3ur5xf +l3GJ9r0riYVNzQ/9kMx4JkoXJ+kgaL2kVTykKERkHUvgRcLkgqVZWMMGz2sUNqYE +XUBtEnYVsZmxQNE8X41NI26XP/1e9jctn/FgAPXMtkLrXRfRIlKzyQTz7zCX1WnK +xeMjFjHkdBD4dP2ohoEkk2y5lDI1hlPginVPZHPShawxd+TE0Vre5i0Wp6Y+lKWL +QtsZKefTbqBDDJlwXmUZc+eu5jEuB7qKDcwH1s8vtZhRVaKFr0kPHEZ3Ka+ODWmt +/v6Zhh/tFkxq0mgh5OHycN3GvwMKp/fEBkRl9pM4RuHrf41XB4+/Joi+CottP5WC +1So8ydbJnG2lYknHxPMrISvACDMLPwWp9IFpr1u4nPkkuAlCuzUkev9r3dJQNq3G +hsTqpQgkHQTd5+QBabE1543gWOCjz9ap80RnIlSfylskR5fWy8U2XnOo7kb5TtnC +hZtJfjpCxHW894VEx3oe8tHaUUdmwrcK3g1DSu5KrtWn976g9tOlcpmfTjqNYEJq +93jD1aCb8yx+LHk5wypQhLpn76AE6YCJebQVxmc0AsqFOlTTRU6yPQF46qYOZD/+ +spUs4bXbxmLghLTO/VmrABEBAAGJAjwEGAEIACYWIQR2k5iZsTfVddMnTnVtzLnX +UtNbqAUCW2jekwIbDAUJB4YfgAAKCRBtzLnXUtNbqE4lEAC+uIwA9vkHHpucTLBq +UiwI4agcY9D0iOGohO7qJQ44MitIsiIqG3Qn1Wps1sdGdoxmFtTE8W0tMhZ+XpTd +ZkL3G8EIhB9gyuel3H1L2vD/6YX3P9Vv4JlcpNDjc/c2i/U+/05kBMwtrgmjB/3T +W1368I9uzfAS3SPYDUsx6nNv6iHhDYDEGOOuBWv5VDvrnYcBbysxkevB6SDZrs5d +7fpmALMsUt8le/y9sn5TFH2CB3aKHGJHMv0RxV5iEXwq7jHPeRJCamzKTCx8/et+ +8wn8Wudk+FiqrSH72BaRb9j7n7KoBuBQB30IbbocRNwGHJHsmuyThGBBZh9Z37Qm +r1qoSNRl+ZJy6QoAO6DVPS6FERDDXYPwrHiC8EbomblcVMYjfI9/Ln+rSB30/OtC +4t+v83v1TPerc1FCXJc2lISs1KLlJnPh5Ykq6IffH9nUALmo5tK5FUDUUhOAxFhe +wCE4fJI+yNIcMHotk5XSxbeSUVFaXDb4Pue/9DjQjnF5iSQGnbveEmGUaXxncjf2 +cJgcNZjd9P4XKqb1hNKpFwgm47dr3TH1/KmkFlfeBK4S/GpVsipWiB9vX4RC28EB +QP4bc5To+ohqwuOLw6hRo0YLf15jTJknCDtfsgKQ6uiR7ai+z6fqoH3kycCCcsPc +Y2/8LdVLydI6o8cZJDEpEexPaA== +=vtJm +-----END PGP PUBLIC KEY BLOCK----- + +pub rsa4096/0x1EF612347F8A9958 2016-07-25 [SC] + Key fingerprint = 49D0 9C86 C3DC 8DA3 F0A0 7622 1EF6 1234 7F8A 9958 +uid [ultimate] Adam Reese +sig 3 0x1EF612347F8A9958 2018-01-02 Adam Reese +sig 3 0x1EF612347F8A9958 2016-07-25 Adam Reese +sig 0x62F49E747D911B60 2018-12-12 Matt Butcher +sig 0x461449C25E36B98E 2018-12-12 Matthew Farina +sig 0x2CDBBFBB37AE822A 2018-12-12 Adnan Abdulhussein +uid [ultimate] Adam Reese +sig 3 0x1EF612347F8A9958 2018-01-02 Adam Reese +sig 3 0x1EF612347F8A9958 2016-07-25 Adam Reese +sig 0x62F49E747D911B60 2018-12-12 Matt Butcher +sig 0x461449C25E36B98E 2018-12-12 Matthew Farina +sig 0x2CDBBFBB37AE822A 2018-12-12 Adnan Abdulhussein +uid [ultimate] Adam Reese +sig 3 0x1EF612347F8A9958 2018-01-02 Adam Reese +sig 3 0x1EF612347F8A9958 2016-07-25 Adam Reese +sig 0x62F49E747D911B60 2018-12-12 Matt Butcher +sig 0x461449C25E36B98E 2018-12-12 Matthew Farina +sig 0x2CDBBFBB37AE822A 2018-12-12 Adnan Abdulhussein +sub rsa2048/0x21DD8DC880EBB474 2016-07-25 [E] [expires: 2024-07-23] +sig 0x1EF612347F8A9958 2016-07-25 Adam Reese +sub rsa2048/0x06F35E60A7A18DD6 2016-07-25 [SA] [expires: 2024-07-23] +sig 0x1EF612347F8A9958 2016-07-25 Adam Reese +sub rsa4096/0x2970B7F911395FDE 2018-01-09 [A] +sig 0x1EF612347F8A9958 2018-01-09 Adam Reese + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFeWdukBEAC/j4xe/59W2CYAzXBgh0kuhdI4t9B/4CzYxWgpCqNqXN/IfBHn +JUSiTKdfwU9+cNfcviDdV/UjyxbWxyvX5Zm/4Ik6XhbK7y+Cl/35TBt6d1MVNr+n +DPeS/uJKNtb27/NwCdihGzWL8UQ0Aah3Y7EZfpy3KSTNfSfEY35XbJTHGlFMGarW +nVArY387C64XNIO+n41NJRnLDzZbFJMv/Eq/psXLumAaav5+PuOelrfaWGNpke9C +AgV7DoyFcK8mTRwISqIjrV9S6ENqzUFu+VcqeOw8bzNnYDwdNY0kgBQpvfiKpnzd +yhYjFeu+OdT+sM5sXUgmM9IdB4wAbpZ2dM8uWjGe7WPSj1B5t3Bp6DtcIHl2ICcv +lpjjrXXKwv1wdnhnUKjlS8NPjO/XGzTUnkqiO9fvbVrMEh9CRCrzn1OuZaH9RQZq +vFBIp2XfEaFaUdvPSDNyDE+Ax1V3+cCVX1+mIIYrS7lK8X3DoXhBZbuREnxvK2X1 +hzw5Ye4GlAw5WeNJNusHmGtKvhayLi7xYjqsTAN/kAcyHm7d3xXBHYsasTpX5Bc4 +MW1nnTjFZzX/r+cOZELWnwAmkponf5PmBVefWRGvhhUtsoF+aw91pme1PF4S3QZW +orre/udNUF3JEbMHhstGlATUMvtLyFtdR3WH7ol2IEVCIGJmI5L6Bj4ylQARAQAB +tBpBZGFtIFJlZXNlIDxhZGFtQHJlZXNlLmlvPokCUQQTAQoAOwIbAwIeAQIXgAIZ +ARYhBEnQnIbD3I2j8KB2Ih72EjR/iplYBQJaS/jbBQsJCAcDBRUKCQgLBRYCAwEA +AAoJEB72EjR/iplYja4P/2eJs1aaS72z5FbdTktxX1/Jj9fFaniBVWakcUTZigOH +pq2oJWUnziLmUOI5sE89WsEt5tmhGCF9b4105nIPG4BVaLAvuiPBF69n/7eNxMh/ +5DZnpooPLwaT3w5m6Fqkouaqs3nWBTJ92Ramph9G/j3rmrf3lPrD3xXF8fXlIk+w +r5n2mdoJvvoezwTIts6iUAFf/hCOecmtOF2yc0Tjzqb2lsu+9OHOgID960cQmzEq +xSJrDsXGdDPkOjTQx2faEmd6jMFzImaqkGj+Ry+rq8yzlHaQeor4aeIAGncZDjmM +hYUXnO0ZITqVfvfm6Gu/c3NyNe4+0SpTWwTKxLv/Od3jtFMvmf1pIjNhcfAdCH5/ +HY8jxl58TL5BcmDK2tzpz1Tc9aa3hICPl3hFbwRDRbFZ/bEOdCjhAhPUmaAl8ia4 +H/XimRzqsr748G5ZP9gkSC42/3nvGgGNZQVmwedw6rOaA9EdWqv3FPE+l1ssbosB +VAmMnaP3M7iXt+ijA6vLeRG478q4rWt63uDYDswJDJv1AXAKjEzBsB13B2JqfN0G +m8HY2vWkaAuEta4fHRgf5hLJtPaJLjHeZ0s/c44KFKqkew3PVyaOnUG6WCpUrfjD +FTh+j/LMKxnz0CLpIj/xbSsfnJgbeNism7YeeEQcvM9z76mRMvDL6G33X9n1Wdwv +iQI0BBMBCgAeBQJXlnbpAhsDAwsJBwMVCggCHgECF4ADFgIBAhkBAAoJEB72EjR/ +iplYejcP/2BgJMc+vugdd/WkDJJj4TVskbn/VWvEp0aO/2ztADMW0uKs8DeRZFVk +eWbueBobrzWP2Cg3HN282E3lsQHqPOI5VS9wvbVj1NSesH/OcOcc2ukimHZAjg7g +cLaECJkXbjuzvKtFDVHRtzWFyJMRPPdrXcY7fzPV8bcr76VeJSz2klK7SI6xCDJz +fbclnyE3ctLVWd5Jmm31xT76u5WgCX+RA6wH7mxET3rEHaSXI66TFzmL97tnM8Ke +jCl2qRpOJpoUbZhhIaYa5BE7nRmPrwQ77za6JvuF7gxV1WyFkuwOGgKGx9zziyOG +Grmp7qZnxpfWXmBSFXdhiWUvWD1PvWT75QZluGXN2hVJEb6f3HAaK7q8y/2QPBWP +1ttnJ2lGpDfEtZCA+RUv6CfuADPF2B2pMyyWC54jT7QPfokgl6tQPotlyiGmiLup +Kml6hd7afS6QKHFeyZYpVVk2CCWXsiFw6qk9OEGgP9eyNQcKtXZhnpql63YUjvxH +HbTt/7OLlgbyN6AmWLRtVpb9onLEskhWJ86yeaYIQoSEP4qNZNBoekMMg+NM6QeF +CEfRPtmvG9X9kSEbeLazyV4xzw2SGjNbmQCSExGr5e2pqKYiEjlHiAXQ+OaVHkcG +0c43snOrCiD4c/rU6UQdPy8QMwjutoHWa5pe5hk/S5HjncmBwHfhiQIzBBABCgAd +FiEEq6JSlZj2YmxCDTNbYvSedH2RG2AFAlwRPcIACgkQYvSedH2RG2Cyag//bZFS +TnCa2WuTB7hWWaatEFdFZx/OoWlzwVsjh+WjAOsJa0TMRGI6VTIPYyLapuEY7+Ii +xL72wsAdjinnhcsBTAydcyx7RJGGhMSiWYRMVP6a+rlUAQJ/YmC0dB3HREMP7aEa +/Qgu1r05RpcLEDpzLsmbMmj7qA6Ugh5tuV7tVvHyQye/7jYADCguDRWC0C09lfz6 +lmQYEnFNo3V1meSxyPFTwp/S4gCf/sc3UTWSGTd8DE8lsQW30m7R1+zH3bw5jsiG +GZMgkpszNpWtVB6zc14csv8okl2tBwFTklauayIVdOprXWvGSiOPGUH2QLWa86CZ +9Wh295EHkHfB+dzB4Qn0m2QBde+r7Hhuve/OVD98oVqVbGa0E3wyX+7EdZY6TUY7 +XIXkQBqlHsdYFHNRiXT9A/mpZh2lXOd4/0m7aT6/Z+J3aV61Pq+azTLsdTs9FFIe +bhAjAjE0gTX7No5+wRVeRDA5zR0ZCEqoBKPx5HKSzxt6rlqbeMsg87fDflDAphAq +52D4CIxEtWJ8YbnUYvPG+emoY9hNh4x+teCwmHL0LHksQrg7bvGJxsh4driKef7f +LItrNLgFC5we2u4KwmPYA3kXcCTtelkzkbNoYE7cHtLyfCeP5l03VNjyx5X/RRec +7SW1hdfx0xLujD+HfPx62sfd1ml+Qv2/Ib2NUdyJAjMEEAEIAB0WIQRnLGV74GtL +MJacSldGFEnCXja5jgUCXBE99gAKCRBGFEnCXja5jrqvEADQAzvSRqjrGeDmx2h3 +S/aF5lLrFC9LhyFaFO7WRh+6hyAIPRKIICCHH+Or3mAxaQ5mi+7tF4s9UrtRu5FT +1gBDSu8hqGCVo0spCmbilQ9gVx6dRMjSS1UykiMWcNxksHhrzDF4hLSlhVYGUwkJ +JQekDcgNXrpnXF11GUt1nr59MSTfvtGb/9vgMkLC+uQeyJtLlx8E9VvppKc3pNKV +xYv682woSHy0TjOyzgA0MIpDMPcozR+E7h72pLNU7z5KfmxVlnJCBU6w8HlZ1ftc +OT3TpA5q3OBhYpz1xpXUA0ZQuRsApOOssvKLFonpu03zeZCTAq1Zhsq0Q0N7zUly +uPNKIB2drq7CAJ1z7R8tJ2Ouc7R2yDbuVK8M/xjgWBlOsz84cYlwFpAzlvboOhTs +ORn0rS+i3Ng8ZaN2yG23oOmXKOpHeKBG39iigAa+vvsxxOCzo66cMfpse/spas5K +OLBtS2gh1n8uXetGqrslXd0puXOyg9T8nI3z61QmPT6zVk8I/Q3Otcui50PfpA8Y +I6B6Lb3vBC/weA5D1ryLA0qNW85z3lpzCZ2c+6rAP92cVA4KR6BB1znAzK/Cf+Bb +4iy2bt2Wl94zJ5YvyAjv0KAhMde4i7z0+AJrM4BQlULSjupn62NT7nAMuYZbATFg +jVzlIE0SLwOapcBsfblii9Zly4kCMwQQAQgAHRYhBFER2nPfEtjoEspGLyzbv7s3 +roIqBQJcEUHmAAoJECzbv7s3roIqbnEP/11fn6f5lF6zsNpB/JF6sbsPrAD/bL8+ +QxIFdPQK/acmP5SeExGh7xj9nZvnzKAm4XTSbHTyZ5WNwEq7Vgk3JY0v07suxrvf +udGgTStmdZl+d01k4NJ11BGYBj0SQ4DG75Egl/57FsrSH6i5vjz6eqR/DJHxMfCt +Ws2SCbKb4aQGlXTPiXFfgGtyFLWEo+iVmySEpEtrn8m5Gm4eeVtg7IUh1DU5KiFK +xRkVOkdC/kyWAY3ig+HzbsVM8Xn3Q3S1ES7qusf+iuJoK4VJQ/HisFdBK3fOxgjo +y8C3m/XFpX11wZ7nJfJz2mIauhoasz/EAvaaczRjVbvmg3Wpm1ogiaxmn7JnS6S2 +0GIeFj0pJudNrDngn591URE1G32kzPaAmOEYeUMP/myjNsSYjlElEemAWO37O5zV +WFmKIcwysdPHnXJ9NjiVDOnpO/t7Xv4ZesJ88+Q/4wY/ESgkZDeA0yHMa33eSCte +SyDv+s1psYbOI7LcTo7ONbf47C1YNEA/Qil/WTcFTCiys9WibDe0KP/aoW0okWxn +hOxQUZV6ZNwUQ64pIpmkWWMV6jYJPotcc6NozQtqkBr/ukMx9KMGozJPfo3Rt51a +xc/oChdnNbhXnOSbdKy1xRo1BzTUR3uELJngLnBbanvA6y0koq3Q2vc23f/oFtv2 +gXwudV/k3ZqltBxBZGFtIFJlZXNlIDxhcmVlc2VAZGVpcy5jb20+iQJOBBMBCgA4 +AhsDAh4BAheAFiEESdCchsPcjaPwoHYiHvYSNH+KmVgFAlpL+NsFCwkIBwMFFQoJ +CAsFFgIDAQAACgkQHvYSNH+KmVip6g/+Px4J3cY58C+XXpnseL8cySMmDBD++pkD +gxaB1OdR09L03Iy27gCXDYBsGUu4x4iPvhEAq064uMKjYp6L/nhbHhvtoziBWL5m +Gd+RJVEzIaW2a2HDlIZ8fuzLiFjWbHz3URKYqjbT9TP3lMTHkBacx3HZ8M+9yUdI +ppsqhPu1xgD4jDXXioLeojca/vMlTo3dkZ9zSjAhqEQRDMzN7Xp1ZzPs+uCjEJHG +09y6/CvPH4gCEIl7Fo+m+vcLhMpRQypWTkyXVPghbrOvVkWpO2zCRFkmQeeDDldL +x5LfxHhMxQ6nMpvX+ecEWa427Jq6stRplNU3MXCFrQ40nP7ZTniNPJw8BfpBgRi9 +FubFpa308y3gYdluYDV3H61SL9hF/3XzguwB//kK4ULCF6Aa8cyFYjqB/cosrLs2 +U+325fY9eZOjCzykRIpINyexh727AAIqPto2J7jnjIhIywiYj0ivfvg84aoYYUqu +kUAPIBHAH/Em+vOYoGwsVMwrhG8U/rBr/VsLIGDC0qCh/AhVt2pvgZT7OQc3CHz2 +wb4NwmShF7ySaSqBJQ3FdRfC7bun5NSZX3hKNXMhppWxtRJU5PjDtg2Y1syb3+IY +5S2gtEAlGFLjnEfYeIBF8RTmdty5ovQLu120JYmu/tCN0EY8HniuIqkI6aqG2V58 +bFuOtoQVXEeJAjEEEwEKABsFAleWdukCGwMDCwkHAxUKCAIeAQIXgAMWAgEACgkQ +HvYSNH+KmVhLSA//a6F3PD6IzElQMTPwGG5RoeRhmAb6dee6xEJe12MBeZlHOvBF +DE5PAfUPoIWVvoaSLPwVIMoEJDpzQ9MyHpne1I+Zy1o9S4dUZ/c3W7rlH4a6e9lK +zcATK++k6FpWWIZ1Ff5ta9uGpxjQu9ojTixojzM4V46MCn7JxfvFiKGvGeXDHHYl +InZKSEmzYOODZzxcYT/U9C6mWADEmMx4M2xgv3UFMAotecXAqIW5/uRZ1h8Xh/eR +ULBGp9MSvnfxD665BqCJHNLh9/G+xr9Vi0ic239nqRUia+zI9tvO2JE4PxC82btk +m7kRNCo67dDg6flCYv/37IWc11RwYo9sKp2S3mZqQoFi5L2JKwZy7tzI9B4/Hrwl +NG0GlYwsiLpRMOWEwMixuLEp6vXRDriu63xKTNbhgtJrGS1FkmCeDuP1f1eRdCIH +PIwy/zQxfVqVWNnWzDs6esxi59L+nUv1soRDGstfAWfzN9wzpwQbmy6EqetgMuBl +F65+6f3LGcZ0Dp7tNM6M82/vvk78JOORJU8WigSQZAX4AB+NB8Z+MwpTmX/bG+Gb +FtcCYxd/zHVyxWUJfembfd8fGJtY39oI8vCdYNe2Jq94wohYmxE6Koan+4CkoAwx +7/67+VWSZwMOJcJsaMZP4qOMLnkrmImlF66AYe2Wm0oKsLibhEFBpSP38CGJAjME +EAEKAB0WIQSrolKVmPZibEINM1ti9J50fZEbYAUCXBE9wgAKCRBi9J50fZEbYBQn +D/403RTgmZMx2pkGFHdVrQFmqXoIXOScO5x8XS8OjFugjycYT6aNeHjQwVllKHLf ++Ig5saTkXoiKS03T61GUXuwPwLVTzkeeDME96dPqo+k83H+D4MifEtDxF1dZQi70 +sZPw+ITlzZOmpyQxDJa+rTbewvM+ULoXs6GNl+jxPpMlKcCpu2OwQn98SOibfDmj +HNCYiF+Gj1VM+xg5MB+ROkLzDFDpUux2M8fJZv+fgiTfcnWL93lWxaKhBlg4ZFC4 +KkdnB4wVyazEucsQQgpsJamjK7y7jslfkVZUwOJvpuqKYLDt5yITUXx5PyqrAwJe +0824AudafjAptcNYRtv51tSIeCw1mxAsiNBwIJmEW9JYwDED0SOH8OQJfWgMEIZc +4Zepa51s3kKYgdh1fkguIrsSERnaUOq0qHlhywOad1rElduWmPWti1mumCDna4gc +1ZH/YR+HyUp7ELF7sJvGQH3bNB5jziHtmqz6+nmH2XxRTY3Mhvkhp9ow5pWxyD6Q +xkXXdPw6a5ZNGTEmmDbi2FEykJyFTXvTsFPvSKAGSXeaUqGX514hK0ZamTfLLyH1 +xdEg3BUNI2jtgFfB6BlneCDlUppNzulfhAR3AgJmIiGImTG1l79nqEgk7O0g3uMC +JL61Vrjn4pFuBU/SG7Rx4WcnMXBfUo+caUQQbnP1QUGK7IkCMwQQAQgAHRYhBGcs +ZXvga0swlpxKV0YUScJeNrmOBQJcET32AAoJEEYUScJeNrmOZRkQALfTx1/VoQDN +MIIwxDW0vku9DLD8AciUs6V3B+IA2ISwbraHHji6kUEoVUMSCnTEIHJVr8L0oeMF +/87o7yUvYgUtWOt416icqGlpA1dGtQLLffyUNv3eCjW9db8+snLZUHsGkeLCowBI +eb8fcPMkNmNNW4YQxIs/di6spV6rCqR+PpuqsiyAHYHOl1z2RpSUA1wUt5oVcVrf +36so+m2gXwtnzx9Z331AlXhSrULPwD+lvd35+gEWpXw2SwD6Rd427URvAUWI7ai6 +eP78OWi2PlAAFbqEuMrwNvYC79hxwB29vEJgO2V3f95UA3d2bJU17pvK+/nYpPWA +uuqLDN3Ydqkoa++5HoqIdeh9uW4oiiGnMAkvH24012GpiXN37T45cH8LnNj0XooN +tYuiY/ZoCTw4gawtMwSWSl/htDrkLQKiUSiKdZqBLVXO2wRSjLjFPJEkq+eIBBcc +hvzj2C4a0sZhk4W1gHESeeB8D6IblgMm4oLa8Fn+4YhiwvbK9Dgja/7iiICP8bbO +3/9smCWOnnsixho5Jkd5IXWF0+tcfHVR/l+M8bCf+c02IGE/mD7RMFcxv3jdFYU7 +/HsbwU9fCDSCAgXszwM+232kALGeur0riRJ42X1RNOzh82cF1wzYyxHR8JZMtcIt +x2MWc/n2wJO3swXnItKI1Vy4fjRu2PcJiQIzBBABCAAdFiEEURHac98S2OgSykYv +LNu/uzeugioFAlwRQeYACgkQLNu/uzeugiotag//YwAVNMHNLmOeAzSOzEf6Z6yK +2WDgEhsUt9Ykhy2pSc2vUD7jIXSAPTJYI7yY1flDmOe3kEVXXeVcPYAli9Ii5Eq4 +DYJBC0FGboMbzdwh8P8RZGnhusB9MSlXYi2DnWH+oKGS9dQFnhpzn/lm0nl8tpL3 +FrnwhlshNpgYYqIa29yO1EHskiFVLD6pL1W/DM6lMFlmTMjRb+y8eyZtbpCIdrY7 +uhDRVwvJPYegj34KR+8OMo1iDvbckee5AR1Dc8L44KmB3Nm9AgW9o+bEz/kYz5pz +Eyv9ibthtagBpxU8kyfSuwH1Z3X3qzgpq8QVNVH0Y/5+sWAOEi2NkCHR2z4W757f +HO16Xz5SZQ5jBrqNFm6u7CLy0X96Sr5FRcefZl/gjqlgNqNTt/iJP48nfKvrEkn2 +1OlFKRFitEC3QOWDTeI4uDFgS/OIUQq7AgqAaxERR3/kbAaVh2R71AsYuXyD57dW +Eo9PJkle9gSMvWUqRC8/0eSR4zgrwirkmXNQVEj55l2Z5y1kWv95NB1kozJb4aJW +TboHAHzklAMK1Gw8AMkjsA5PZyXQGkM+kXzUvE4TLC3qnsr5w05yJT2teOaWGK3s +3ZOtu2WuvuOXo0qd7oBzkF9850LQ83wEuwIfc7XcbaB0pyb6B3EtyZ+pMlRrzREg +EiX+bB98Q3qhNopjIDy0I0FkYW0gUmVlc2UgPGFyZWVzZWRlc2lnbkBnbWFpbC5j +b20+iQJOBBMBCgA4AhsDAh4BAheAFiEESdCchsPcjaPwoHYiHvYSNH+KmVgFAlpL ++NsFCwkIBwMFFQoJCAsFFgIDAQAACgkQHvYSNH+KmVh/AA//YVA5eJBbCQQKp1IA +VWf1vqLdE13hxlw4MZOf4+2119l8RHKwS/mio9ZfmtoTHLqgiDFPEARQZQf5fjmr +Vl4QqZbOzlhbU1bFCE0i2I4Lypj5TAY2j6WRKwc11mKYmWM7gayMjvKvPrL9s+nH +sFC8foAkYC3nBeHR26AooLUjOi+jKD554vLKWRxgHMwS54s/U+n3OejxTF87Wdi9 +fB/65tlTw0vt2lrAf6LUaKjKj4sef771TMgXYuJijkbvzP4ShrezBPAdWabf13du +EK+O1FVURkTZYSpOB0etDyDV6DXD5amz9NNO/N+bfV0/2dNY0Ez3cjko8WuhQRu2 +Q2PrJ5CLRN88KLgrDu8lFLmrQY14OrnWaQb3zVA8LMPhg1jUtXGB71zbhmM92BPU +rRE+zO8cWOq1ERCw7GesQ6LwsKTI+ceXBmWqH0woxbBQlE0A2RhUam2LS7/etBuw +VLBqXM3/rGYeIWL8j1fx7yF7xnrly+7BBR46B1rdGPupmRG8U+xu6H5qnHgl5VQK +j63XHRyP8jew4ZkUSU1+4ueoruWDLTxa85DDpYKux/+8NQyhlfBxw2BJOegzBEF9 +0ddcRg5jkH/rdFXz7lFoZe9oE/wVzPMmzLqroKvwDI3krNuTL1QB+j5fAWl6+N5W +y3afspmmDA0ql1+6Cmr6UhLh6C+JAjEEEwEKABsFAleWdukCGwMDCwkHAxUKCAIe +AQIXgAMWAgEACgkQHvYSNH+KmVhpXRAAlauaug8H776O0qOVX7njKwyHUoJS4Ddj +PUA0XzmFjrLrC4CylxQ5zVnQWi2QAh1FEDVrTWX889kkbPPo+9RK82bkdwMP8+GN +Bv+Vu2SnJX4haDXooyT1BsmKvN5ypm/G4Xc0oWFwCXFJDxYtEhKKq25PRtP/KS89 +HOqvsD2SDfK2xpufXR6zyvCeXRwQX3iiyq8tR566aXpUg1mDcCtJpb1HGk4M/LO9 +9Ph1aOoHaqSAB85MK61rnYFNqRGZB3Ge91j3Xp188YZW6WFmC+YzdAB0+qGfWHLO +mT8HmI1X2mPuHdRtYk3AYYVgSSLJDwMdpvYoethPUiOGLraDQSufdEkAcMwuU2n+ +NuRbejtssInsdJuI9ug6hvbkDkj8gD+khPnvg/epSuOGGWckM6SOwkel5lYRH4pk ++Qu3zGj0k0mcOBucvQMpzGJfSac4bhj5TNOAyAMMjYGCQpRaJh3ZhI7mUfix2ex2 ++d7xru/amMjTZ7WQ5kpz1EQN7aeXOgtNRZQy9G93dw+cZ7WBJT1MQ0KwhITs0KGG +b078Z+nwuKVeTDPGxNaYNcYPFDjmfEEZ1khLrD0hT62qOjkO8KdfNcgfEn/xwb/q +eQoHvT1y5iIyu66DGiuFU0Kwbtq46/5rgT4EgZKX9D/j5oywMTtkOCalnM14bSkg +vD2WUX+zMbCJAjMEEAEKAB0WIQSrolKVmPZibEINM1ti9J50fZEbYAUCXBE9wgAK +CRBi9J50fZEbYBVcEACat/K5p4dxhimNvLfUNRMz6t5mW1P0nMeLPQ9R0thp7FAX +NIHRGyaoT7Kn4EISw0j2Y1icsAgg0G4tx00jIrwnFh3olK1bbUXeIgq9v3OR6rv1 +rW68C9KMMtsg+IPrv310MWqhxh1+yfiQFUFbLLTMUaZBXUCRYYt02vIbM06NNf1z +mXaBef98KB8PGpYZ9QAhF2yDHVPgSyIJs2cUamiyEyeJhuXuullbrV5m5XhdxY7N +bgseDGuQcmx7gPmaVJmlYUurFy8N1amodSnAthWyfINUGu42SszDqDagz2XF9R/I +Eq+4/noOdktyHq9bPGzSwTcdFoEpji9ufiT69TXYSZG+oH2kBCkhIX+Pt5w68phD +D7uK04N9CdNLUhEUQdZHXm+NWv5GGbXjEzkpZ4raXVe/i4hBDRL4ayVmDkbfVWtx +FBSyMV3LX7Rcr2rSFKBv9Yo2yBQMx/V/tYMeE9i369Z7jhslEsJc/4tFtLtCp8ck +il9j/Sj5KfYVYxzzl2g1OWDGTcpX9AO8W+T72iSbF2d12lSxa6XQJIumCZk9A0MF +WbNTVK3rbmreFwo9q/1xIcu6QakiICqUSnBkU6yM3V4AR6v4Dco9xtJ4f8DHZ0c9 ++MrS1LOw212EQo4TR/fflBg7hPVhAd+4LwZvjaa/Pn8Om+eUyZjucSiWuVroBokC +MwQQAQgAHRYhBGcsZXvga0swlpxKV0YUScJeNrmOBQJcET32AAoJEEYUScJeNrmO +M38QALMXs9/RAJwnZbwqyZBPI18Zmih+k/2OiryfOCfC9J5kE7dHx+MeSr4AVi0Q +rACXG1kLuvieXSq+kVw85NRqGWufEEXyK4730YNFFaBUH3KIBUc/zyZcIBLUlnkg +Gj/lzI0ZKxysEp4gMjPsXPVSAl3aRcUPofbjoNz7HQP4E3Lhy7XzOj+up9bhqquL +i1QKoOYddhrTKnXyONtM0VmJpYMgefVqR2CExJ/8XsNEknYpHbpynU7KpziJ1OYG +xacP44r5T1B1YeEQFfrtumMNPbsdKU9RnMo8AUcUnYE6DlrMNb+FWefuuRNg0qie +hfBO7eIqNHWwaEmlAw80FVa2HoHk7EALDpo5Lp78V/0CHRwdNgIoxDHM46AMvLqq +iFQ9wMsTHqVqWHLFAfDxgfjM9pWuxXk8R5+8KHyHQ+dY/tYrNBrqu0QV90pGN1k6 +sk7UI8B10SgqOwzOiddthiq62wmUuKWGNq3mepgAldPVJAfpFN2tEBx6/H/UUwBE +nH6t8NQcHjZw4zh3g2BRq6Ze7vk2YLlCRTKTOBWpfv8qu5DXz66V0/GcQVGC4LIF +Wtjh0mckHdSRME1JJQdMcSO3+qlE0EOOhPpB/aIVERyju2lXQbXXh8uRMkaDBlJo +HPgqpRwQ3ThHbiL3WkpGzCjod6lBxUZLauYZ21pl4X302sz7iQIzBBABCAAdFiEE +URHac98S2OgSykYvLNu/uzeugioFAlwRQeYACgkQLNu/uzeugioEHQ//XPCaFz0K +N5TJXF0/3s+2ufTYFXeHc9G7EEBfMk1kv/pObFgXx3H7V85XUyMUrj/BBEG96y6R +aKcsbkySGhL+l5meymPSrRGY5xMw7hYGrvzpNq99VT3msH+j/Mqz3in4EmgXev/b +7ZBrEVN74M46294//QiWSRaTO8bfKpS3kEixShJQcy4gRDkvjl+FgMxevjWsH9Bf +0y7pY3A4TFgMDqCd5R4Ptf+D8wrY9Tc4Hc+BM6DPfg8b11QeXFlAdBqW2tlwmnuW +U/joLeFXwwsQa0Dlg/vveGVfO4KoBMcsfFxQ3XleKIRH/mcSuQFf016MDhI5bZYP +T7SvkPK0sVkmJt3wGJmuJiTM6HEvMyjGSXYfAHJxePNetQS6oI5A9bw24NPTTHm8 +sPrEd5hIPLZ9kx9y3MwsTjx+/AZ67u4/BrPsFzNdyDp31aKT+g8vP3YTgESs92cy +vzNGNgJp5grvtDHc/lqe7rQWJYCO6uf9SnuWYQpAW7jnI6rMXctFFDCLwVFH5VGM +cbq7CjBbQ/fY9fREiWl+TeKQSBr7DV+ssqRxUfzZSYWRnZaDajRQS041qCFDyUhj +A26P04hT2n1x641ytvO1wvFa8of76Dos1USMeUFV3eQicY98C4p4sxEBCUmIBaOk +rTgaEDezUt63yR66Uc3p7PsjDaFwjsALKny5AQ0EV5Z26QEIAL1rcALBlQxGsY5Q +RhIvi351MeZsK0A4hrDQp7pFFjbqlA52UUkkDuyl8/1zES8ITe+l48F3NiDDGS5s +q6A9ubHCMCjz/NIHL9bTsb/7wyQNRBO+nuqBBvZg80LsWT8b/jg2fLXghIbWrg+w +r2UcxAV+ObOkVC+rnkxWrbHCnss+e3oEsgkO+8VWpROoRFMsGTf7lqOwgTaYYxe8 +VGo5y8OiMIPJdFDysp3VHu8lnGJZbix2awsJUqyEd+OKqYNKqfY43PCFpVW2m7pp +A85UvwdGVEDSy1iymjjZKHyWXb7emKweBhWFKbL7kpNSkwqV8qutGLfdO/jf6+4r +xRtwBkkAEQEAAYkDRAQYAQoADwUCV5Z26QUJDwmcAAIbDAEpCRAe9hI0f4qZWMBd +IAQZAQoABgUCV5Z26QAKCRAh3Y3IgOu0dN9iCACXC+h3mueHUFTmkNUG0c4OqemT +RCmaXIbt46kBnzYXx0AsHeoZEYXWW62Sl8auHfaL8zPpOEFwBCY0HCVDQ+joWPJo +EnHvPZs5DusNnVNkCfy/T7ClkTW8py95tIUfz1aJxcM8q6cXCQuCR1DciK/t1hi2 +c5NOIVHmQGZ4k/o49iEdgq3lZB7EumKxMYItQk6WMl3kX/7Nr9B1oc4SZ/7hhEn4 +rWA33Qvld1qeZmm7lUZGZP9y9U9I6AoJARHwvF3hvFjOvI0O7L4LxU75ee3W3vJJ +1ZkPzwwLBY3T6m9CIaqOOtxeQg0dlfRBX6DVpOB9ogNnFYwwmc1HX55FKc5J5bMQ +AJKy+Gs61XNZalag+l9huvilhiUxffg3nijjLcF0Gj9p7JJrqlG2MODTpLBABYul ++yckitJOU8MaIznVOIBTH7IfBtqzS8RxNiAZnpEWi8KhXV6U8nqhz7r62iPGTa8X +8DpHWLcIJyS79CagsN8XkJRKG7d8R4wBHvv4oumvyTk6C44Uxg/+pX10hV39Ct/r +BEnt6aiIdbkxfDSdEub703l8SBOjaPeXnpAAPcvY/f3h6f/pGfYFqCdr+vvRBf0k +Z+DpWXRAYwbl4G7sexffwlYpC3cxLM7ZyntD2srC1XXGY5fGfSQNhDb3PsHCbbOb +jhM0vksTgCE3D+4JUx3FciNSuZMcL5oGP7TxehjJGJOQT4ehUQg8B00KAeYKdase +p1AwECB7G0SvEMUqjPkFpWSjArZ57BDui8I8ZvpGNTVfZWGgzMeh/E6611yhxfus +dki8YND/u9WfjAQ2scMUCi3/7DpzDLP68cp2UGuGXRMs+I5cvwYKdlWbz1r1Rydm +2eShFsZE7SnUwlEeaypm4IZGUcbmLJYK/qX4lFsJ4oa6VdfSUPx7dUUGUbjqBgyc +q2gdHSkDsnY9xRmIThE7UarDVeA5GqM/QVXB+xxG8tjabUV7HV4YLURdVKDa4Gdp +1+bpKSEHugsBBXfgpTl/UnloW9VbhyvjYWtUTsWm0tgBuQENBFeWdukBCAC4LXGN +UKmFNwyk612coxLXln38Ezqr9BkD4SWPeD0uFEKyBlrTndQUlfGq+2eEmvxGzeY/ +ElPSgm9+xQSiWEaPRxFfJ6J5gzbVJAOJZJ45KLkfKokoj/Ao0wLA1GwqJx86kmUL +akR8zSZAv2XgT5Y0gE6i5sKmUBPTanJu+QBxi0L7/9W644PdbZmcxoiNszQ3zSVF +WcoZOB7p8r9QxgW3EeDyfzfi+zvXRgI2hCkGvrxOzkgQurgs+EEypVkBcLwYUHWM +woYzI+J+ny95jQpEhSYo9MW/uwGua0PjMpcMDA0ddqaqsc1pSUYOMsaq+Ddfv/EF ++/Fwn4KjdT73XazXABEBAAGJA0QEGAEKAA8FAleWdukFCQ8JnAACGyIBKQkQHvYS +NH+KmVjAXSAEGQEKAAYFAleWdukACgkQBvNeYKehjdZeOwgAtixW73UK6gyyBsvC +PNW2n7HjRc02049cUcHz+s0D+wMa2xpYIN1EPQBTrcpL7mZZeKmxKzYA9vj3RuaW +ocoChTBAmQzinTFT173kV1MpQgbSP0sgS+6/p2tSJ+HxmzzNsV5UMwV61IN8xbFB +N4t+GzWyIh4etBkpUiDjzZg9w9E1pAD9UaNAmNGfv3bt3+A8w5H3KHqMWxfl6/+Q +Urw4j3v86ShJknPeQ3WHsO9J53QottQuWidswvZ3QG7bAZUjbUPwSCcbjllooIKL +M4ZPc//4dEnvFl2FLQeIxWm8B61wNA/BZJAAWd1r6tkztulKgkL60NuvkwiodR7p +pQ1t9rDjEACwzg4ijOl0zN/TE1XxgRaf9avhvQ0mVcqU8Hp2OKFjesdYMsgroXtd +0KN4S00QJJhTpdgT7MMRCZATzPw5jzdnqjxJoJuwYzaszMTqKGPnFJdBnPQutyiX +T4gp56u0wH6CmrPFwYHKq6NNGr3bPuYG/d+pCwt18Zt13KmgEWaEdgDmfylTrnQk +hWzmhAHgCzwn/aJw6sN1GkCfQD0cxdUrAm3Ttt582ImLpBB4tDhPlroHtxw/KTPN +SMCM0pSQ9jompssPvFjYRMExqLsLZAVWrpK0uvrWom0pkWzvjBqXC4EczxpjLepX +1AIi+hHYDzW2MizcTEe5jYUpwAr0N44Cnw80RwIHJM1O3XLQpaVGW91hgLjWp81A +5FmqWPO7Qo+EQtg/zAa7F9ukHGsl+Xa/+Lx6PuoRwOV2sKfFfJ+7xolvwFLta7Lf +Hu55PURVcw24CMCCyQPcOOoqZEhiAOwNtDq22c1T8x1GyvI9WsZRLT2XGCntDavA +pXkYs9ZKW6OQ5KWKhkw7ocvTF4Aq5fBrv7noWtN9mp4mfMBaOsZRsuaqQoKRqwvK +RJDZ4+wzc/Chy/N3fSa22n7QLxHyFDqBSARBGy4hoXgaf3Zqk3SglTvZK1wkIpyB +hqHZQIYxbE5/KRJuiqcZ//UtmNp/q7FFu/Ytx22lsE8wWHGzZJdavLkCDQRaVQcB +ARAAzA+ZDFUZ739XOAiZGunhUyQ3g68sN19x4M+Qay95ZPFwl3HLgV46WBDY3x87 +DMpvYYJqLOF/tKlzRymm+7QpyLtIWKX5f8TKGKrV0+8vY+h7SyKaRVNbu5HqPDU8 +ViXzMleQxgy6T39HIuHdAPo9ceEOGM+XB0ESpA1eRjeRJGF6dC1Ric8nUZRMnmTw +y8xGugv0n7ET47v22cW8TVs2k/ociPVLCF/Qws1FeJRp0CDbg7YFcbqoD4cV1On5 +SypMRnSmhjm9GI3hw2JNM73XLH1lSuHKKIMtUifaKkpUL0RP+Nq+QYAzu8ruUuwX +pEy/WyiuP+qj67rzQOsqRDUUMAVtAr2FH27kECAHDxHlFAB/ukp0/WAh0oT7tX25 ++nM+XcWCoNFRMDhPAAYhlWDyn+iPuCFPdzR5Jgx3hyvgKPDRmrIwhs3VmWEF/dPT +XpCyIbgSSCEF8JOv0h8m3K69tWWTxv5j8j1gVlZ0mVdv55lnqQtybxPoVnFrAznr +g/30+vsyoh5dH3cc9MteUh0qYRqDH8Q5wc0benZFRwxH9E3tV0P7NhO7h1H9l0Cq +wwyrOPEdnySUD0xdBupC2zoqdjCB8l4RQidryWcPcItSs0J6p79NLqdHStBJZogf +EzfPqL4J0y2Dv4EFQs1LCPlxaLS7TMrjZKdecrsmRHJwofkAEQEAAYkCNgQYAQoA +IBYhBEnQnIbD3I2j8KB2Ih72EjR/iplYBQJaVQcBAhsgAAoJEB72EjR/iplY7AwP +/2APBujg1Q/pXeDxLgxs8eGYV6DpTtAJkOYF15A7cQ/2WcmSJ8GywCpjkVgItqLf +UT/mI01vuJMaQM/aOFQiRHmlfdS7KEYzc2W5zLb/PA6XK8OjELGP2ZgMsTSy8MOm +ILtxxhPlGRaQWI7zEA3YDYfRg+uP10z8KpFlOg4tNdbXaA7RLdz+x/zP75Hv7C1D +9wJMLO0I4fmK4sepGq+Zk/pFpuXRMwjO0eZXLSE6sO5P0YF6HrU8TReXAE7gHuzB +gcKIYF9oNertp4LhplYhrHkN/rg5b/CbRW7+C4jbwszYzQL2S2Hx03TFasp/jTgb +oX5X3ISY0zDw53aE4mcI1nhYPosiY4BQ647C8SkwLZEixb7mS0pW8HdELRIBJPDQ +llursCS2hDZsBPS1PcvZsAkrTscsUADvdryZHqo+TkizO+HO+oRBRqltAPHTiBSl +13Hjd1Bv+wX/hexVe+Ru1i5i6e495nsvFx3S3b/iCpPpmRYXiWBoW2taR1WQz8/r +0OChc/OrJIg6HZ+sTAnoIGFFlc7p0hrf5jKaO6p+LQCHc6IAcKXvYBLxMOK0i6BR +BlA4kJPTfla4LmKRg/T/xow/naen/aM9mQCs7k2UAoeqNZ6IfQ6G5BZ81H9JNvHC +beriLZDBuRy1LJRjBmZEz+UDBgZoR9oz5DOLh8dGVpkt +=HZO9 +-----END PGP PUBLIC KEY BLOCK----- +pub rsa4096 2014-05-13 [SCEA] + ABA2529598F6626C420D335B62F49E747D911B60 +uid [ unknown] Matt Butcher +sig 3 62F49E747D911B60 2017-08-11 Matt Butcher +sig 461449C25E36B98E 2018-12-12 Matthew Farina +sig 1EF612347F8A9958 2018-12-12 Adam Reese +sig 2CDBBFBB37AE822A 2018-12-12 Adnan Abdulhussein +uid [ unknown] technosophos (keybase.io/technosophos) +sig 3 62F49E747D911B60 2016-10-24 Matt Butcher +sig 461449C25E36B98E 2018-12-12 Matthew Farina +sig 1EF612347F8A9958 2018-12-12 Adam Reese +sig 2CDBBFBB37AE822A 2018-12-12 Adnan Abdulhussein +uid [ unknown] keybase.io/technosophos +sig 3 62F49E747D911B60 2014-05-13 Matt Butcher +sig 461449C25E36B98E 2018-12-12 Matthew Farina +sig 1EF612347F8A9958 2018-12-12 Adam Reese +sig 2CDBBFBB37AE822A 2018-12-12 Adnan Abdulhussein +sub rsa2048 2014-05-13 [S] [expires: 2022-05-11] +sig 62F49E747D911B60 2014-05-13 Matt Butcher +sub rsa2048 2014-05-13 [E] [expires: 2022-05-11] +sig 62F49E747D911B60 2014-05-13 Matt Butcher + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: GPGTools - https://gpgtools.org + +mQINBFNyROIBEADL6FVlqQPC2DAZS8RGYs9Kiqpu486QI6070Nq1l950XxUdudkm +dM8TH0FluDkq/RtQQmVHIwBdL4n/pH7EfKTUy4ggYIs9v2VPhMp7DVlRVKIXKoHl +qQu9I2VI3UNM8j+cQkisFgVrzHi93SHxRKRfJM/qPkQYmzsnBRH/2YAodSOmWybf +TZJToPtkRXqPMm+ZAAtfyhwvwPiXfSnB3/0t5K4WCdhQP601l3fifyaZVVF9GX3Z +n54i080HXYhdxr32n8xPi+EDPv7Sh3XuQZ+zmYmSTxZ12mBIZgzwCJH9Uy9XzmE5 +LrZhf/s4mus5VO7ZxqOr/pZ2edzu3Hae9SwVa96kntHK4Oc5Ja6AYK17dibRG7m6 +1AInGbpJ5oJMvm3MwQbxLXtonZuMr3F+ivdBqrnwjpGHiTfeeuBGasx3WIJwBvzv +94rldvEERAc92eMNEW4G+9tK0w2R8SYP4njWZUKM7ngXRPxA5/vRYj8pzpr/uiFi +YkkzOTo5beueqdAyqaV7GOG2bmzt2Lc5PSGXK/Ew8sLAiOV4ug2QysNMw2MdjF7v +ek6Hco8U+Ir5YQnt4B+t9piDg3w45WGdNfAe5roPZtB9yYox6Iy34fo1GmX4qUf7 +3i99UrZ/B+wgCRjHxsqborquMZnX9S0BeQFm2RV/0S2l5A5NT6yHB4B0hwARAQAB +tD90ZWNobm9zb3Bob3MgKGtleWJhc2UuaW8vdGVjaG5vc29waG9zKSA8dGVjaG5v +c29waG9zQGdtYWlsLmNvbT6JAjcEEwEKACEFAlgOZHsCGy8FCwkIBwMFFQoJCAsF +FgIDAQACHgECF4AACgkQYvSedH2RG2DUXhAAtZGIlCFk8GkhxoUFZgcR+AfgKG39 +bcEdDVulGX0r7LpVO3pF0V7KrY/Hz55fVrQjF6UMS6TF4dB/j4U4ylIdv9UUyQUJ +O9bPJwcYLbSLURqA75NeA7XVSHwvbm6hTCcdnwPxvxkfisd/YUN75mlDNeZEEXs/ +/n+2AxlPX8eQt6n/3RlYYGrekle7EUO8IJcqS8jfSloxkUBO201BubU8lg9bmE4W +uiav6Dgqs1V4q6jheGz+c6BD/PYOysQiet+1Ot0GscvIKgW0w60q7ilzzviOK3eg +fiq57W0Oc3GI4ihrfH3ppC3kcJFyAe/zle25QxWHZWfnNZ12ZElK7vIQnl1JzwVb +sICj+y3j2MUkczHtf4KJZe/7lr1G+No1mFYmDu+GZqT/eSmADOF+IrkoCZgR/jMr +O9Kgfh5U9B7spbHGkA7eZvH68FHRxqvXnUgBFS5hE9oQyTR2EmBF+hpx8T4K2uIo +NSCoq5pTD3HNEZqBZ+E1NQCGv1a8YiyLjeI32vljH52pjtfbW06Nfb4rI+/BrMU2 +82gzVHxiN9O5Ba8YmBLbkZYYTW0+9rF5w0brTxRS4IfokNNeanIJ+w7CuUhEyf0O +yOa0DRxUEvHIq6UFibJWzei2dzBIyHovdIQelkmFr2Oq9LDqsBtSZH5quGzeJ2N/ +atK1HR1GK5CNwRaJAjMEEAEIAB0WIQRnLGV74GtLMJacSldGFEnCXja5jgUCXBE3 +0gAKCRBGFEnCXja5jrJlEACarEjVOWmmZlNkHqajs2rEUJzM+qKThr0QMOd8UvYh +ZpC0+IPOXLjwXpKiZ+7sPw8YaGHw5NK36jWPy+cPdoDppZfRYHp+/0cmK4GI3DH4 +9x/jW3yG9g8ckYCKYscrhev3AeD1UwjjiiQhS5m15/TTOLPGtu4kcWyeTcdgFMo+ +sdiD1w81XA2/zCTJptsDw8AIxJEk+rqBP46qy7kPpawCsO+x1f17tleZ+5pZPYCu +G3vuaC9ggcKIp9K2oifH/Qn1YE4G8Dz9KqDsS3Ucg50PR2tpd2nXQCoWatezNxED +tyNblmx29JJFjSMs9nKNdddDmwWHM8+CNBS9mXRs1BttxtWAPmz2Y/9U4wvQ6V0H +NxZd3JUItOqxkoxVavdMQrbRDLgI8qVXA9LXABJaJ9SccOJfAK+zJVSVcOqrGoVK +7jQyBoHsMbbGl4P2EJtFNIUOiAvo+y0cA6oboAYnqgBr3ghOXWa7uiLB2zFhREro +0VoGlqCjbH6JdvjDcC4Vf9mxtVP42605phBmd6OCDXjTmgn+KToRLKd2i8b/eafZ +5djwipOpyHHxIQ5N+qSI1jxh+58P8x7502kMTHzCoAdxnWk2CT67Imggby3xh8IM +jJmvah5NM5a0eFIGZs8HNuhkbtJBuF6WzVoieBbin+O6/7zvNaS3x0ZcYJPZUpWa +dokCMwQQAQoAHRYhBEnQnIbD3I2j8KB2Ih72EjR/iplYBQJcET/bAAoJEB72EjR/ +iplYOrAP/1b4FsE7QxzYgU2ulBkqDGe9eSWPwuqWORwqpYNPy9UNYKrDn7LO4mfT +GKvVozfR2e8YjDNsP9PfqPjk7OenqiWkzDgwAZFKoFxbu0RFtxA+aMNVOG6ks6g/ +LJH3uvKxqaK0oUTntB9YusdS5B7JOcSzDo9uw+2mRyavxs7aitJmcMmrU6GySmGu +t5Nutsr0j1k5vB7lFNu7PYmc/rQyF7UK45+Q5RSzW7lsvudR6VM7qjE+eHfOOB+t +9Kym2siSrCcwsBsrqGtumXksG3KUFubDr6VG7nUX1y0CkZ8FdtdWnsyssuJy/cUz +sGhoZIXhnP8LAvVS2/0g5U+94K3TfFPrlhq8Dgt4EWOr8icL13QY1ZhlQNW861KP +HEOTtUoNJPg7DafrkB377cfwANk8K3iJAMrWK11TR1obr5brMPFvRqeb1OsDeTmf +PGJkm3DePTKydUNvLwd9FwdG9wsoZVGrn7aRQ59OUn5IdAnuZ5Q9eWnEJf03pTNp +sJ/6cH4XCLy7bM1iim6oknLKpUFWRFxOgMKVeFNQO1h1D96u21bYDXnbKyc2vlIw +sBZbKkHsxWr8AzmCOrWb1DTJO5sYTpsBQkQANQt/IUpNbg5eMC7zdyHUpLEyqQ2E +kfrWOoqoTowXv6xZ7Wdd5/OJHwn4PnsKac4ah3tOMzhQYOAgel+/iQIzBBABCAAd +FiEEURHac98S2OgSykYvLNu/uzeugioFAlwRQU4ACgkQLNu/uzeugiqARA//YEMd +eLItDPOCtLlEYJZ9N3VheUA78IER84cena7RDI38Rra7sh5M+msNJJTYH+mXK1B/ +2Y8tIHo870I300vQLLDXXjGDFWuQRDIXgNkVpk8M0msNqtvTps1Pmf7fxpSeI24a +dGwlyz3oCUELp8bXuyY7LTrNMa8LjNSbS5TdCF0xteuMZdDyD03jDO/fz44Oabtr +fdaIrzDRbw42AxnzR8wrhlR55+EFxWizWERqPLxhXYYhcGk0PyGdZUzcP9YJmjiV +h0605ct+ykiIm0RQ5/YGWkKRC8LIRDYW7NB9Hwv62kjw2pSKcOWm9kaGHOjfieCd +XnBvAPv3sAdcfgx5bP44a2Sh7Bsh8BIqrbsAAG+9b07h7IMM6MCFFxd2smNsp74n +gPR8k4GF7vfVvZherYCB3EhPLoudoxf/u0Ock2Ssa31XStZ+jHb6a/keEPFGnygg +opNDfw5BlUsys7wSEDOSTE3cdiE7B0hWxC5Xw80r3SxONk3jPczraSG/EVmKndX9 +quFboecUIXGBbsx79tUolKTMOQrVP7KIM9ltbpvQShy6RYpWa0dKTRuUgMijqiB4 +A1SR5gvVgKs/xzy2Bw9TAH2ayGc/r+mTpwpa6eOhu3NOYhqqkENlZ/IsKnX+dX55 +UvSVexlttUIjxCKjvH61Pmdi8meNhEesjVYnsbS0MWtleWJhc2UuaW8vdGVjaG5v +c29waG9zIDx0ZWNobm9zb3Bob3NAa2V5YmFzZS5pbz6JAi0EEwEKABcFAlNyROIC +Gy8DCwkHAxUKCAIeAQIXgAAKCRBi9J50fZEbYCnkD/0WpXKEaTdXwqy7fm87An1H +H6HcHDR95+Ldu8XgmSZq4nbkDc0wjDdBD5Tp25QSUznzJ4pKO/Wd7l6C4fhqTZn/ +vldDpRXl23bqvRHmWVkXH/EKZxh1y9TnID7Ysy9H9qRVdFm/yjM9EqrD++/vowYW +Sq6ekosXdjTZWuXVBnirnM/MwSZ/3w1tyK+zfbzA5XR/pscPbTO/UuKdmUbwz4yt +QjSQg+awJ2iRko0USvDG1t7PyMdDNfF+gbzp6qdI/NUo+XicRzCtmxfKR88vD5yE +FD0DY/6xl9172XpB3h5aI1jg2LTDLr0IIlO2KHRkqs9piqJuHL8uA870ZMvLJN9g +JryUny7b5PJlmaYDJPc3TmiMUUHTkrcmJq4Knlh7WtrDX8avbc6T8lWOCakn3cNC +X3O7RW37k953fF3GSgv8otDlySANW20fG5bPN2gvElfHi4LFP9hAXESZUDYuOasu +dRUHMkqc9BAMqqgrgrrY9Qmk1aE+udVTcVICRoUoZyBFVzDsRQL+c1zVBk/kJ8oL +e9cqcdpZbkvVDLtPEyA+b4icX41woqiTRfK28BbKCSwSXkqi+vo9pk9Uwy++S7OS +D3EOjZhox+Zi2Ijcpzb++B2mxX5yroRrPWvHrxIsAKs8ogO9undz+rJbqgZr1PoF +rV+wpMe0ckRECvGqEz0BiYkCMwQQAQgAHRYhBGcsZXvga0swlpxKV0YUScJeNrmO +BQJcETfSAAoJEEYUScJeNrmO1T4P/jAUMiKYNqUlYpCV+mvzVwUQWIyPYdgzqO9R +AmvI1ELCDT1BGB9pLeeUwFXQX/+8+7lGAVLynL7FPPVkkatblVIQKFgvL7XmU6gb +o73DpslX6hn+clYeYXUs37XToffVIFVwIQkWusZ+X9BkM2TeV5fgoJ4mhCh8ys5g +RHKuXYnqCIHfPj033GIhSn1DZRecKPWeb07zYZI4SHsBYEM7xfN4eUEXOjIlRXea +O6hS6N3vBTinn7LnHkRDD9mUemTruBtab2F9Nk3+njzpafMb4IprD5+GGdRacGOq +VNWFlZDYyy+3Qv5A7mXBYGCaTtH5Jlz4oEibFXvvVzD3IgwFCvmU1S+UD+9l+u+z +Nk3F4l07BuulhX6Ek55CoI3kbMCovFjPFrXWghT+/XQy6GaEhQmQ12rhUDBBjS2s +29NImvHyBGX/FHY0udt4fF/h5O0eRw7zqmGeen4yOu60cEi9MVesRz+GZcbdXupe +RghrhXhfE6NHcp7ciyK0+Y8f3dpeXVw3na4EOraR4w4ae+SJUt56Sbudqn7S6Kxj +UCKql68VWHfhh1ibdbv1wl8bAHqtSt0FQlG5mUAcN6R/COO6uK07H2rJWtmIiDAG +nhcSyLY+5SjD7LtRYvZr+SP2EWQ8wHopjkkGfvG1gXl5NQ8gaVRzErkuc4S3yHMC +47j+0GsBiQIzBBABCgAdFiEESdCchsPcjaPwoHYiHvYSNH+KmVgFAlwRP9sACgkQ +HvYSNH+KmVhSoRAArTJp7zUs6pp/+JTFfJsRHbqUBP13KAoZCtaV6auJf+MA6mFD +TD0DpVKdKBGjKna+W/qFn/8lpIjxL6YtQ3/W1j+d+uhd2OPb44atpXNuxArpCqoZ +zAyx0ELmgP1YbZ/DIRKv0v0nFsmP4jd14pcclFKGLqh/tK66n3+mOH7zSqltljV0 +9A4evtkI/29/Jj2I31j2rthk+gJmAYiksXVIZb3Hoj4VjFaW0D3/d7Bc5LaUCY2Z +6GXa208UjBfumKRtSWGXDaz4LmxoS3+H3xfnm3APQIryaSc8daBY0BjDwORa2gUB +9rddEtSWbVZvJoIdAa7shLvR+eYubMCOjmHc5cV3rG0AF+5pymOv+Z9pAIj8Uzfs +kXtmIkoXPRfubeb6rNx66fKakgjXqtcGfe0VdYg/VJiheVedPmqBvePFvuUGvROa +TzDdKxKqi+AR3+JALfcue42xbNCTqWW+iercuKz6gpNukfwuDciNMrH+Ggg8K/FL +x/NEQbVTA+IFZyuBtiRv37gDNf+gRK1buA1OJg6rS1US8CE/brOWEhSDXN1wJ9wM +JHtM6xj/Td/L8v7BYOXbq1ffuuXeX7OOa2NF86yTthS+Hx07y6ivaBRIWhf0DA6I +lQkoKtJJ8dgWtzRgH/Dl18nhgjdqhyaQXnBclxv0B8M3tbpeoJYBRTM/7haJAjME +EAEIAB0WIQRREdpz3xLY6BLKRi8s27+7N66CKgUCXBFBTQAKCRAs27+7N66CKqz6 +EACB4UuPAH70NzoHo9utcD9bzMj0PRi3GKh6MMm0CsumM360HfN9RftOrB+Y3mjq +Oyl4onqz4hWKWWQayUsI3T0YiDwtV3zeGkvyKGMB2gZN/duZplHiSj95Jv7HPQRL +kVo8rrEPboI+EdCCOypZIu8K9vfs/fTrsx14dEy85cOqv4J2is28zOapFoR+79gN +pErktx0ftcv7e2fxXQB5sUAa8k64bRNuVoFXz1HH6T+7641DwQutGAEFWug/Ythj +vytNBlcq0bxpzVwC2RAbPrnJdRu8f1XM4jBx9mJz6NfHGvSjEtlAuc53Y9DvJEcZ +uKwrN2NmtJ0dkO81NaU6B6oT9dwTaJ/6hwHq0WNvPeDcoUZxrh0XXyuhjR/p5MoU +/0TeiwA6HByO+/wQRL5ZODUag8xlsnXHMxwz6F/mqo6OirJzflJdJkm9kL4UKjCB +r/jzOmf6WVQEfjWTFmv2empmxT3Z4ahR60DLRCGPlc6v7N7QshbH74b/NfbP7CPt +SNqQwiPzSTiNjr1SZhMFJ/Zu7HS+/ysXyPw6Ku0s+8zQtkstV9+Oo/mpfm27yDih +scWIZTmc3RiZmL6eURA9tijdB7ZNuXxTyKkClUfnkiba9zdBCZT+n52zXY9aR4OE +KEYFXe/x2A2hON02AP/lzgjEpg/3vaSfLrzk0wMeh+yYjLQpTWF0dCBCdXRjaGVy +IDxtYXR0LmJ1dGNoZXJAbWljcm9zb2Z0LmNvbT6JAk4EEwEKADgWIQSrolKVmPZi +bEINM1ti9J50fZEbYAUCWY4aMQIbLwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAK +CRBi9J50fZEbYCtzD/9WqoGpj/rKKoqoToj3hInc3Nv/Nxj9quJc2Z4gxmnwYlB+ +KhZeDlfCytkFFYXgl4bB6KcpnI/OW+hynxR8jT/wIvD5E3wIUCRVJfbdmKBiSha5 +KMDgLGmVpJbVG83s7mN6BlgYPxUa7dFXI43mRBkt9hCnH7U4vwx5rtLlR5FEU6EL +sjYiWc/zyjqZFLHbnlJ8tt09zKTVDF4SfdJz1vpDCD1exY7LZtsaL1SpE+fuTq+5 +/Z6MvMQk4bJcEbXzrIF1U7C7xIoTv/npv+eb0xdiPto1UKs6C1o2rIdvxbrDc8zz +pWMRSPjBaOuey01rFKrkpSlxuX1h6HQSDyN2Q7WmeezLh3RgoTwrEnmy/Qi5Ze21 +pi2ygMtUadxTzZRi/IC77s4FOlrnqx27AonEzRQHTtKXhLKrrXD6HQTerf7W9v/l +24O/QIAdX/JlhYWHQGPHAWe/3o30XkeM/Bhlt29SAnxeWhTo3oa1EudXrAe745eM +rA/pdAHWgqIBi5KZP0j9nQRtxXN/ZP3ASKjs1CKw4OnwpIWotUkK0XgMemJTgBYR +FmNUPpUCiLTiZxfJbcQt3khDOfQ53iR3xSLP788MHO5/zGqKcOgnjFlyc8QLdLSb +db52l79ZaeXamakEEfaZRnR3wtZjWvLk+JnC9nWEVflICnLwKpLoph+ceVe8j4kC +MwQQAQgAHRYhBGcsZXvga0swlpxKV0YUScJeNrmOBQJcETezAAoJEEYUScJeNrmO +ZJgP/2yhVoDQbp6T1ngsl079C3ZwyDY//TfKXUwAJJgHo84IdrLWhYYTCo1/2nm5 +rAqmDlq3OJsUMucwj8opocEIBM2HWcRcwFJgwC3Caq6w0vLzmt9Qm5eGIwSPGH/Q +7w1YQj+6x++xyYuVdmChVIIgQy5TP2cIuM+c+T2Zq2vTGKV6VNKQpVH/o0ymB7zx +5ZSJdsQGuNWDvZbwsVrYsbbgEy72iO7fVvc3aYUVXL1gvJjAh4GUKApsLWhJGG/G +HoYvl0PSTpb++HOGwtwbG+GG4ELbISfyrs4JfMUvRA2hd7MZD5BTvO9hzWeFAOJR +ze5gsDkUCMeJ2D1yoVONHhmaZ8k9xy88p/NOC3iYamoixO6vVkOsCbOhzHRVlj1a +VV4Zs1RZ6kMgm0HBgGFjj4IjWZy39G+JWfjJuLpRAVOwPv3Km2ertTtJJjkSaTA8 +TRG+/ZjgEse+Gio40aeBhm/2LvM4A1oHe/gzZXQZrHKIl7sy9ijQowj1wuqs+oqz +gdjjBp/DcU3qbTo0vJ6ACvRjQVcIhIdOypkn31uhUSA1CtHT3TNau4/D+B7YNtWZ +egeVXWvzFZukXkYzvbDjMn9t3PYHMPKaPYynBGuFPO0fMBXouh4qOfXywFemB+zu ++ccZol3zPzBhdxInOhWi6wjMBSY1zae0S3Co34CSylZ2lu+kiQIzBBABCgAdFiEE +SdCchsPcjaPwoHYiHvYSNH+KmVgFAlwRP9sACgkQHvYSNH+KmViCHw//dGg9ochq +Wuh344h8SSqq7G5d+Hch7EIMCykPlDCkmO0/FsKEmMQ2nMySpkm1zM59pHvADlbu +tbhtLIk8kAjsfyZF2alpTCn1gxRVe/aXBsgmAf/Op2jf92zPkov8bXw7x1oFZ3ew +fWFR8bwG0OEK8kr9jpkCs2lRv7kG6g60ptsCWDkJGiXpEyovUF0W3ZpCU3RBVUIC +D8xMTBJXOiCYtux7uDpGJ9iYBGD0eUWxg5OUZs6Gmid2sr+rV4WIoBJEgGUMq37f +d+loYNwm36GQmU3ytWx3ZduCruNRf5XSdws+nJU0rPb51CiacPp/g9PR/9f9i9/a +yzao/7pA9/0mfiAXHveK39iNqFH4V0B4hOUzRWWWJJNvZw195LridOcmmgOLTWQJ +iWErD0VvzZRrf5vf8sdsRoXx1gHrgb2ana3ThfRl+7gE9jgkrEZxGZBh6eaxyxpc +eTJBtGjcAgATlKSZSrm9ZLI3Jyz+1R+uEj1LPY6rc05c1XU2l6ucoMGRvu/Vs2m6 +XBiyJeqX5yARdVbiTMbmGI39SxoZ4//KJoFjs71+FxT+sZ3syfQyQJjaS8++qB/e +zmhF4Ab2wh987um42q3Bzl3NXnERFO0Rq5R3ksgBb2ns93Sc6WQPV62pUFxzeHX5 +BcW6vVjL8jkrJuMipKetoZGm5Aimf3oydJ+JAjMEEAEIAB0WIQRREdpz3xLY6BLK +Ri8s27+7N66CKgUCXBFBTQAKCRAs27+7N66CKrU0D/9tCR/N64xwcY1eq5tEjFb0 +9T0L/aP39hcCOWeMwD2AcA5qSM1Fr/gqCs18db9JqZOcTEISrStzQ/ciGj3Dsnlz +7LjYVicQjwNK39YxedfAuU1kPAd2k4KujpE8o9b0Q25nsTOth1ZZyXJIIsrywwVS +CG0shyi2GASuZOXIZOdkYI/SIPojZdQKG8Czd37Lo+2mPDqG5lTL+LUX0UoqEFR0 +AJfsBkZhUodUAJSxzT5sn9ZyBOabAbdFhiwjMHTyvFOdFzIfc5pS+/OqQqdhwxcw +/FyCFyN5WgC6nRxEZqvW2jbB7xphLPWOWxWbogwD4QACQO6ih6pyUAvkcPGRTJRS +j9AuJ7cX1iul0hwxjFifoJGnDyNB0oDo7qyfcOQ5lKlYE6BWQiQHcE8UX4pn0Tk8 +z18pSjh79LxFUgKH9Rvv2eIjE0V/GYFh/RiPxopBRGcqnpo4F5mOvLbeNMN/lX2s +3g8rkpVLa/QWf/d+goak/zVWqLmC/5OMTFnEWrcTu6dVycoEiyfKgf8LRZ4YrSbv +jkeHNuYwM5KVgv+umLOw/p18mUueYmEvpsmQ55Ri2UxxhNWizm3xEtLo1jLGfBUu +7RwuZ3AuR8HMBeOHsWe1/MSpZgmbL0V31mfC0M25CrBs0qBUB1QxpCLv+cSa3Acy +CMfQS9ln8Ydzm2sHPsLZA7kBDQRTckTiAQgA12JICQ4oNax8PaljKomTwuFTCrm4 +6j7Z7HsBM579lqkQmsNaBu8euQF6C5WJUE4aflBIa4Q8vqinZirkdUNvkj2jGdKW +XG+KwGluvbd8IhCvD9ITV52/Sj0V1PqZMcKktRpEczn1KY5BjILXKbtlp1eVa7Ha +VMHHge01c2TH6jttOtasUFBkT0jD/Zd4fO6l1e9cN3e7hhIO6HGqcrhNIaHD1ikG +6VjJU/ndP5qkzwErqlWF2H+TThWaY/PO0zXp5pXQ8geBWPfnw4B6ZvKzoHM54vd+ +aotgoDrNpWMkksm16oAvctXkg/WSt3mzNIHQHQZSYN/uorXek9R8664MmQARAQAB +iQNEBBgBCgAPBQJTckTiBQkPCZwAAhsCASkJEGL0nnR9kRtgwF0gBBkBCgAGBQJT +ckTiAAoJENzV9eXvMsNFID4H/j9fGdHyPQLDvH363lsGx62l5zlX1vL8rjleZMTR +D+JRQJ3MjSgEIdEE8gYLyRmetPsrbQKpOu/uGVs+Ef/SDFj89VhK+661DfHwcahN +XHPTjcNi6OUlE2Z0DXdxgb4czMZkDf79ga/sf72S1uJNQb83GfYN1QfLq+MXsBmF +LfYU5RkoF7obgVQFAs5HYf3RqCribdNEhGEZPPG6wNcp5DC1UvrpotldqwHZltFS +dPPPUT5S/kpcRtqL/bilPc4Pb7qKQR1Huacy1ca1DAEP+TvhvgMmm6ExVAYiV1TA +ZBfOUYC+Czn7ZOGJ8Q4AN0yno4IOsmwBrxaUx9+38I3rb04nLhAAyUUZZGp4Zfj1 +bJ/pOxZ2H2BqX3fstN9tVvZu47D2DoeF6T6x02HIV7oVQ1/haMnjP7rtsWNjrl32 +RkMkbvwqsnQwcZJrylQTxYuzy4IGXak/tlEcesspsG6O34pvPoZ1c+q92jofPOzl +W2xnSTtKlt0Fu/m2WNg6s8tfec7emi69J6Pl+XMAmQkihXF+j4QuXYzSV4G97W2t +AMxo5d3GIQ4UzcxhEvTH5s/S12iGT0xfy6G3yEqTzFgByA94BWN/plzgaaV0bNDs +uK16Sp6c8gldHs5o5uI9wtJa468dj5Ll9zJZOdC3UN3kGDY1T5jnctnfgLpU081c +tfz7tr1URFiq2LYlxpEUC/OUFyilHuM17RacLLAM6+9s2bYFD2uAOfQyUJaUD2z2 +/9I7WRaDbL0DMhn/QZPdhLZSMuuoaBEu99NWBGHMfVpDmmPQeBLTS5l4Q7lbrf6f +zLdb+3Vuhifl6w4UTPC7Wb4qowjtIiaqdtqpsqm2LE62xsvd230wWT+ipGhBx/B4 +soOh2lVXkEGL+nEPTljBxkumkZOTxJl/EC3cFEtVKGCw9Rid8nUmc/v9LcKaJQDO +AZ0oAMc8eSyxKGaW0pePlHn8k5cds1w2ZsSCXuDGNYHATp4Gm3izEGLRsex1KYq+ ++dysRCx2EZMDsaCAUbXNrt12FNhgzh+5AQ0EU3JE4gEIALyimTnOi0q1WouENJKQ +RlpBsZ25Cxp+kc3Ttws65cFYV+3682KMRelDvZ073JRlyMbEmAsxCitrmsKfI8+9 +3TVg9XS5R9RynMpRiyi1m6sHLbeXG6LaWaT3gyzu9VC6EGoadf+l8/emQD2WeDJl +fHJr+QivlGM7hdMvjewj7Wp4+x0JclhNsgjYEUkF4ajy/q+A98YyGFybpOwqRoLv +U5WXQGxuh0LiUjvpLyrIEEFcvASCNOAJgpN92G7nsNDsXpWjwmUDYwM9uJipbM+M +kyWJykiaii9tYg/AzsFN9Cr0+w07IEX8IfWmY9iGjOC3eISwX/vx/jtrI9Mj8Ei5 +RKUAEQEAAYkDRAQYAQoADwUCU3JE4gUJDwmcAAIbDAEpCRBi9J50fZEbYMBdIAQZ +AQoABgUCU3JE4gAKCRAZcP/x+neS2tEuB/4tvkwlS/aJJles7+n9gzlcWHeRHECG +0zTrmQr99uTvBaYewB6gSJK1YrM/ocOH2k6e2EAfYw+bgBXcOpb3NQePZ4vLCAkl +6J40ktwyWBOs8uCAdBX5Ngkxhiz5oNaxQqnBU+xfovsbQJrxj0S28DBXGDR6npI1 +vqjrsYBoPeo4YZu6pUAp6wW+7eC4eHVK/NIogw0XxA3VRzwvzLK+aUI5RbzyWwYY +PDfzXrQRqeUqCF2bnnsXjDHxfqjfoWrnK+ATGFZgjbF2wHhPDRRHqAx/ggn8K/R4 +rvhEKFIqxQHrfZgQgsWregiv46Ph8DBEGcniqeIS5kRQi5y71IB7ndb9cB4QAJIb +BQaB5GhsjVw5bQTsZWMDLoweaR4kqP2eXgx6HuhRw1XcP0ZNNv5//L7tv6tmeXgb +RO17JzCw8+g2ZFq8Wbd6v9MFKefh+FT75Vvb9jV6h2NtQlteKQ9mpXVpcxZ0pKDb +hPzrjcI3Xo/zjYHFjTk2VAxWVPtamBN2eCGc3ggWifYnmuCctxHlTZNyDyrfPwJ+ +Vj8VuTsjd/7b8VVLd2lpzF2m9M25z4zNgxzldAAr4F+bIqjPJUVY29pZFyKKqcBG +zCYBTlB+yiVqjXOyyYQKwE3nkG7UrlsQdQEI/wjqBJtDpQ/w7NLPKwx2633dVQAT +omPKujL3klFlIdof/5+JUDzmg2mC9ATCJ4sgTAIodo6hHACQT2OuKmAHuCI1oqBs +7A3H1pPk3HZVKy7LbdQTy7QTzpBiUHklOKWlWj+ugWeABTZZK5U9cm9vq2mT+rcB +Wu94GriSlDo3vobC78nMDZc68eV18onQpWTlzRsTVVfOjll/8ddtruVkCVhtfRxE +ANQIfZg7P8oNxVDAX+jIsTDxjh8r+S1wsUQcTNop6JMicDbxrBRB13vYIY0Jg4+Z +9WUiKCaM69kbgcJ7tTp0skcJ+rYcjVkTz2/P33/FA8BMDUwCR2FovRnmq9pVjAAP +hS0eN8yqaR533ire0Ur5Vif6+z4A0ifVTZ2hY96B +=nEJu +-----END PGP PUBLIC KEY BLOCK----- +pub rsa4096 2021-03-12 [SC] [expires: 2037-03-08] + 4AB45F1CB0D292975C6371436E2A23D806B6E6DD +uid [ unknown] Matt Butcher +sig 3 6E2A23D806B6E6DD 2021-03-12 Matt Butcher +uid [ unknown] Matt Butcher +sig 3 6E2A23D806B6E6DD 2021-03-12 Matt Butcher +uid [ unknown] Matt Butcher +sig 3 6E2A23D806B6E6DD 2021-03-12 Matt Butcher +sub rsa4096 2021-03-12 [E] [expires: 2037-03-08] +sig 6E2A23D806B6E6DD 2021-03-12 Matt Butcher + +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: GPGTools - https://gpgtools.org + +mQINBGBLvGsBEADHfZXD7feUfyNQoCwmDYCmygvIGKJxGkgiyxecbGieggOGVbNy +1N0F2w/HHHW7uanlCsrB/wKnSmkNxkp5m1vfcmg+AorjshBJZCjvNZAX78yOGOZk +7UQivwPhRWvJ8fnzwTd7ls7bz7mggPT0wVuBsrHtr6mfioxxmVq5ChTHKER7uFRL +23bd11x6hurfURgDuYPrCaLyrvHmQs7CCe2pxJVLFH4kXyzNoea4jZEbOPGNLXB/ +war4QJaXtk9rLqEQ6fp0iM/s7N61eEcrj18HDLj9CTUB66UMTlDKUZUV+36502Ae +I6lrrFSx8KUvK9fcpdcxXKYoaY5t6BIBUS2JK8fCrTgyBdTPQ1J7z5N4GvwYonf6 +FBsQpC2aY7wBAqFEbZ8xhdB/A6gY17542OSDhcto3ovdrbLkPaPKHUDz9WRDdR1U +VKAkNeqaf6h00cyEjM/IN8+Ni+Bwz1hUrwN/9qcKkhsaJK+D2z/f+Fq08+8wHm7A +rf/azwtiTT21S/Qwmg+ISkmHJiUueuL9IIIJv0tsgxZ6MsYF9tP2NxjBcmtketTE +h/oygKhFDiK8ybSRftCatEzJuf53cfe4fNIJpacUbD/QM8tGgwrXOpAz26Flm8Ki +drw6re2mvxnDKOua7dyukq+JHR5SBEzKv8WmaNEgzEDxPdaMa6+7mLcVOQARAQAB +tCVNYXR0IEJ1dGNoZXIgPHRlY2hub3NvcGhvc0BnbWFpbC5jb20+iQI4BBMBCAAs +BQJgS7xrCRBuKiPYBrbm3QIbAwUJHhM4AAIZAQQLBwkDBRUICgIDBBYAAQIAAMZ7 +D/42lpQArXi7unDfG1K5dksGWv50S8dPy93APKZkxSqmO/LxMxOSUUq6N5NSh5FO +WV3o9Za0u0IfKN+cje4ldkRGaxAEmoPLRaB26lztv9AzkaBUh6c4q/MsUiuExJMN +l9P7los6B8kCtxddq3TjTXf1FVPxT3U6Orprmh9BNsIdw/N9K0teUJjEBl5ui7i9 +WqVvbbTy3I34ae2tCdN98iwHVpkfm/VYuvqtKcgzv99FcasvAWLPr+z9fG5iOx54 +WthG2UCXf4k75W8Ddd5TD8n/3JaVZX8UUq7EiURRD2fFtqMce4PCDYia2MZybjio +qJOvxMGOr981JMI5uN+2gVKe+A2p9s9ittvHtnHQxVWd1O+CGFQg87+js+0BB4hi +WcYGdDPh6GhpYx38In3tBHxzIfCitvKMOvovFpV1j1kYaMCENrlaO2C2DWHALCX7 +unpvrSb3gNnCFzB86+PJkwOSRcWxERdGY8soZacTDoTqUrwCraR4/KgZk6JK8jKH +t3w/a9igvwmzZuUrolAiv41zywDupl/wYOA6uUmvi8GxWCGZ5sHRuLGxm+Tk2QyA +QA6seNaun7OE4gvrTtuA/2AYAy/NVqdVdjHN4oOIFPnsoRfW+ltvWsQ2fBsyG0mW +A0JT7aicKCa8aZZ6ZQtP4zbKMYxJW4n042hiYcgrdCdumLQpTWF0dCBCdXRjaGVy +IDxtYXR0LmJ1dGNoZXJAbWljcm9zb2Z0LmNvbT6JAjUEEwEIACkFAmBLvGsJEG4q +I9gGtubdAhsDBQkeEzgABAsHCQMFFQgKAgMEFgABAgAAWkAP/0KjQDI1HyFIT5GG +j0yufkcmRZrsXSy57eUpfL1RY1OGqTnB/dS4DL6OJX1GaXOlfj3lwjiDl2Y1pHAk +oncv6n5AAXWfvWxkDJzxqyo8A6FhS+fOgoXaKBPAH5/1CgilNzABNIlRmHwJ4uAw +TFP8v20Ug6gqaW9lSH2PXtZKKf+gH6lBB4YwNnzehnIteX30PWhhZ1SUib0jJCoc +6H156wo7G6INzZepg+hqI1ly/XYg/XzL7qRvIREtALOs/7qU04+x1ny4Ys6G1ZAP +hI0sxfcy+qbSqzb5+7oYg/UwrbwIhs81HaTyQLa4FOYKGPyg1GkeJpzo9EENRgoy +u1Dmd/7S/Zbszj4kakF7INMByolvbHvl3FMLAILj6DwFxakI5kd1V9XemYPSRoLA +wzeUlzYHrK5tD1Q+EdmTGBpmVghFuN0ov/jja9tInF/ZXra4GdeCdksatbkUHP5p +xb8BCGmJQtJJ0ncxdn3zwJSl+5qFtdaTmMrc9p20QYiwKuMupHL6+hkdhwncbRux +S8x0dUm4Fn5EnEcejRiLu6Xs6cmUURZyWXEkcUW2i3+cvj+1dkp/HPkStWrBceyb +VarypHX5BhBGThdWiDT/Gl6W7uycFGm8kEUF9bGgSvly1clwRskj0cc6IZnSXmNq +/+efhKkDyQC3krStcwT2/HzvtLgDtCRNYXR0IEJ1dGNoZXIgPG1hYnV0Y2hAbWlj +cm9zb2Z0LmNvbT6JAjUEEwEIACkFAmBLvGsJEG4qI9gGtubdAhsDBQkeEzgABAsH +CQMFFQgKAgMEFgABAgAA3TIP+wSoWwwicctBVV0Mu3zX+9TOC/QT3pf95la5PgIV +fu6S97h7ePphk0ORRFe4qW5f7IM0iXWTN455h1ngnZGXn5tG3JtkUY616AnmK1fJ +MHRZRCJmeD8u5SzCCZGBlL+n3Hp6gOR7q14hhgkeg4oPiFKSF75LJos4JYEeCIYN +WyUa2yjz/glnzrA/zMeRQ+acRXj/Aa1MlwiDukxpIaHzB8U0xm+V6AgWdNzP7T8P +Daxidjgkjk3GGAK741z37avP9MFYUTd/Pq6Z2uB5xFuaB2xD5gJcvVYMBJQtYmtt +AmbzEZwYsROmkfCmS9jmlUFaMbKdAl2do/0feX7Hw29fhVT23tYD2d9Zm39CFXOm +tIb4SDcteyqeIOhQkLZgKLwJiwXkaLsHPVZlQljzvkQlW4qRGvzxyCWWr4PZovQG +ZSyFcO3XJk2hswijbhM3rQOxtOL9GJ9U+khnghLfmet5otSl0Gm1yW+ub7AynXi9 +JT+kMv2QZfPP+jZjIeBLC3yItI6K/+0qI53JMswKDvQ8qnmeVj++dquSSnSozXpa +npqxrjxAhZ905UrPKqzxd9lJUegfB4khUBC/IuE7HTkFnZz/I+r6IfJ031YZK/lr +eeCQm6DMvoehR+4vgo+APdvclMmmCWd4TBTFBhtOZvLX5HfMU++YZC13AeDUmzOp +edRWuQINBGBLvGsBEADtGQcj2nLThgu9QBKN7Q4TCwywd/RTyJCZm2aq6NVs2iYP +NGd49RmHdzYbiSgOaSSIYODevDB0KFK0/D3YMjEE5oBpf94MxGDOfq/tVEVOjiOR +rwW7YaKGpxoD0q9QB+CI4+w3Dhu5Yiaiun+carXPfhxaOvoYq26heLipZ/cztgRK +16bqoAn/Kl2/yY3kfN2YRBgHFaLwkKFAKD39QxbxrCTB6YuGLhGOI+BLv47WlECi +TnSM//k80jQVEjuvoXZaFQO0/A8O7vIXF2TarVKO2I2HPlCt4q09ub6rmmqn2MGj +2gwYR1lv1vQZMVevJOe+4gwGKPCicIbp+JX2CN8n9lorS/PlYkUSNZehNhEaBKUK +yl5WFY00oGtjYKwRwStN9m3JwNPAQES9EYipGi4YGdsrTa+MtsIZQdnbaMVA9wlU +sNMyoTBjaGr79Gu4cPLISy3mNy6LRivlEeE3pxcziUj3k/6dLEUFgTfgmH3dGJ2o +c1fqF7RPJ0hvzqh6pG9lx5nkUtpG+s8FC7hDDnuqVXCS+4rPe13sEFRlM6l1YAiC +hXeApBhvpqB71ydiVR/yHua1H9b49+1eVeWzfF6XPtUSSCkwH7W1ZWx+8yUBi6zz +GUgmGNJ4m0GglCDPXsP3w7WNJoPAU15LNsi5z59bjGou3OkI5czPTKF7Q73znwAR +AQABiQI1BBgBCAApBQJgS7xrCRBuKiPYBrbm3QIbDAUJHhM4AAQLBwkDBRUICgID +BBYAAQIAACVcEACY7aIw03LMedYRsWogFn6IkpdbqRVEYP5Zjglky8MFIOQv81j7 +Zg99BB4V0lyvSMSlFmom4BE+Sq6EO3uuqC7WR+7GL3p92AyIF9EJIOAg9FFH8eRn +jk1jA12Zdx40V6okWpy3C/OY6D6du17G6AJ1NExfSWtbxXknFAbsv2azQpJ0ATdK +xEPun0PGlOhsg+Bu33k7tQ2P6/4dJT8c2e8QBy/kedj3mGhrb9Ymy0VdOn12P7kA +oVl9TvvQV64f9YSToQzDjHTSP8dxiEV7a8SMD4cm/7sTLF1a7LW8lD405jxqll8a +dtj4+yY/rfSN/rDVoTDBkc6habYL0G97j70o02nZYJtukkIQvSYdYARE0OUdwb+y +SZWuTxT340LDJHUwmDpFyk6L6MTaCwlFPoi4+0FDpjdOngEMjMHe92vWT1gGhk6B +uOKbA/wFozjv87y8T6bCJ+dA1/TqhUT7UJBKJozXpOpcYapI59ZmTVu5V7WwFJvK +JlWm8DSDpOI75JRRy3DTX4UmYg/nRX5pfLPsxq2JQW/QnjPLPJ/y+5Y++b92wWrP +AirPev6SluPhLJ2mswaK3THlhOZulKO/VIEJ6g50m5Vj3hdYf6sR603yK9rP+3iu +IagTQt2SGfW3Ap0RO3Yt+w29BpZ1CZ5Ml4gAYkXz0hiiMnVRhlcLIOHoFw== +=h3+3 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/Makefile b/Makefile index 2dfbd2c3c98..d897bc939cf 100644 --- a/Makefile +++ b/Makefile @@ -1,88 +1,101 @@ -DOCKER_REGISTRY ?= gcr.io -IMAGE_PREFIX ?= kubernetes-helm -SHORT_NAME ?= tiller -SHORT_NAME_RUDDER ?= rudder -TARGETS ?= darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le windows/amd64 -DIST_DIRS = find * -type d -exec -APP = helm +BINDIR := $(CURDIR)/bin +INSTALL_PATH ?= /usr/local/bin +DIST_DIRS := find * -type d -exec +TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x windows/amd64 +TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum +BINNAME ?= helm + +GOBIN = $(shell go env GOBIN) +ifeq ($(GOBIN),) +GOBIN = $(shell go env GOPATH)/bin +endif +GOX = $(GOBIN)/gox +GOIMPORTS = $(GOBIN)/goimports +ARCH = $(shell uname -p) + +ACCEPTANCE_DIR:=../acceptance-testing +# To specify the subset of acceptance tests to run. '.' means all tests +ACCEPTANCE_RUN_TESTS=. # go option -GO ?= go -PKG := $(shell glide novendor) -TAGS := -TESTS := . -TESTFLAGS := -LDFLAGS := -w -s -GOFLAGS := -BINDIR := $(CURDIR)/bin -BINARIES := helm tiller +PKG := ./... +TAGS := +TESTS := . +TESTFLAGS := +LDFLAGS := -w -s +GOFLAGS := + +# Rebuild the binary if any of these files change +SRC := $(shell find . -type f -name '*.go' -print) go.mod go.sum # Required for globs to work correctly -SHELL=/bin/bash +SHELL = /usr/bin/env bash + +GIT_COMMIT = $(shell git rev-parse HEAD) +GIT_SHA = $(shell git rev-parse --short HEAD) +GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null) +GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean") + +ifdef VERSION + BINARY_VERSION = $(VERSION) +endif +BINARY_VERSION ?= ${GIT_TAG} + +# Only set Version if building a tag or VERSION is set +ifneq ($(BINARY_VERSION),) + LDFLAGS += -X helm.sh/helm/v3/internal/version.version=${BINARY_VERSION} +endif + +VERSION_METADATA = unreleased +# Clear the "unreleased" string in BuildMetadata +ifneq ($(GIT_TAG),) + VERSION_METADATA = +endif + +LDFLAGS += -X helm.sh/helm/v3/internal/version.metadata=${VERSION_METADATA} +LDFLAGS += -X helm.sh/helm/v3/internal/version.gitCommit=${GIT_COMMIT} +LDFLAGS += -X helm.sh/helm/v3/internal/version.gitTreeState=${GIT_DIRTY} +LDFLAGS += $(EXT_LDFLAGS) + +# Define constants based on the client-go version +K8S_MODULES_VER=$(subst ., ,$(subst v,,$(shell go list -f '{{.Version}}' -m k8s.io/client-go))) +K8S_MODULES_MAJOR_VER=$(shell echo $$(($(firstword $(K8S_MODULES_VER)) + 1))) +K8S_MODULES_MINOR_VER=$(word 2,$(K8S_MODULES_VER)) + +LDFLAGS += -X helm.sh/helm/v3/pkg/lint/rules.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) +LDFLAGS += -X helm.sh/helm/v3/pkg/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) +LDFLAGS += -X helm.sh/helm/v3/pkg/chartutil.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) +LDFLAGS += -X helm.sh/helm/v3/pkg/chartutil.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) .PHONY: all all: build -.PHONY: build -build: - GOBIN=$(BINDIR) $(GO) install $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/... +# ------------------------------------------------------------------------------ +# build -# usage: make clean build-cross dist APP=helm|tiller VERSION=v2.0.0-alpha.3 -.PHONY: build-cross -build-cross: LDFLAGS += -extldflags "-static" -build-cross: - CGO_ENABLED=0 gox -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/$(APP) +.PHONY: build +build: $(BINDIR)/$(BINNAME) -.PHONY: dist -dist: - ( \ - cd _dist && \ - $(DIST_DIRS) cp ../LICENSE {} \; && \ - $(DIST_DIRS) cp ../README.md {} \; && \ - $(DIST_DIRS) tar -zcf helm-${VERSION}-{}.tar.gz {} \; && \ - $(DIST_DIRS) zip -r helm-${VERSION}-{}.zip {} \; \ - ) +$(BINDIR)/$(BINNAME): $(SRC) + GO111MODULE=on go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm -.PHONY: checksum -checksum: - for f in _dist/*.{gz,zip} ; do \ - shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \ - done +# ------------------------------------------------------------------------------ +# install -.PHONY: check-docker -check-docker: - @if [ -z $$(which docker) ]; then \ - echo "Missing \`docker\` client which is required for development"; \ - exit 2; \ - fi +.PHONY: install +install: build + @install "$(BINDIR)/$(BINNAME)" "$(INSTALL_PATH)/$(BINNAME)" -.PHONY: docker-binary -docker-binary: BINDIR = ./rootfs -docker-binary: GOFLAGS += -a -installsuffix cgo -docker-binary: - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GO) build -o $(BINDIR)/tiller $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/tiller - -.PHONY: docker-build -docker-build: check-docker docker-binary - docker build --rm -t ${IMAGE} rootfs - docker tag ${IMAGE} ${MUTABLE_IMAGE} - -.PHONY: docker-binary-rudder -docker-binary-rudder: BINDIR = ./rootfs -docker-binary-rudder: GOFLAGS += -a -installsuffix cgo -docker-binary-rudder: - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GO) build -o $(BINDIR)/rudder $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' k8s.io/helm/cmd/rudder - -.PHONY: docker-build-experimental -docker-build-experimental: check-docker docker-binary docker-binary-rudder - docker build --rm -t ${IMAGE} rootfs -f rootfs/Dockerfile.experimental - docker tag ${IMAGE} ${MUTABLE_IMAGE} - docker build --rm -t ${IMAGE_RUDDER} rootfs -f rootfs/Dockerfile.rudder - docker tag ${IMAGE_RUDDER} ${MUTABLE_IMAGE_RUDDER} +# ------------------------------------------------------------------------------ +# test .PHONY: test test: build +ifeq ($(ARCH),s390x) +test: TESTFLAGS += -v +else test: TESTFLAGS += -race -v +endif test: test-style test: test-unit @@ -90,50 +103,132 @@ test: test-unit test-unit: @echo @echo "==> Running unit tests <==" - HELM_HOME=/no/such/dir $(GO) test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) + GO111MODULE=on go test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) + +.PHONY: test-coverage +test-coverage: + @echo + @echo "==> Running unit tests with coverage <==" + @ ./scripts/coverage.sh .PHONY: test-style test-style: - @scripts/validate-go.sh + GO111MODULE=on golangci-lint run --timeout 5m0s @scripts/validate-license.sh -.PHONY: protoc -protoc: - $(MAKE) -C _proto/ all +.PHONY: test-acceptance +test-acceptance: TARGETS = linux/amd64 +test-acceptance: build build-cross + @if [ -d "${ACCEPTANCE_DIR}" ]; then \ + cd ${ACCEPTANCE_DIR} && \ + ROBOT_RUN_TESTS=$(ACCEPTANCE_RUN_TESTS) ROBOT_HELM_PATH='$(BINDIR)' make acceptance; \ + else \ + echo "You must clone the acceptance_testing repo under $(ACCEPTANCE_DIR)"; \ + echo "You can find the acceptance_testing repo at https://github.com/helm/acceptance-testing"; \ + fi + +.PHONY: test-acceptance-completion +test-acceptance-completion: ACCEPTANCE_RUN_TESTS = shells.robot +test-acceptance-completion: test-acceptance + +.PHONY: coverage +coverage: + @scripts/coverage.sh + +.PHONY: format +format: $(GOIMPORTS) + GO111MODULE=on go list -f '{{.Dir}}' ./... | xargs $(GOIMPORTS) -w -local helm.sh/helm + +# Generate golden files used in unit tests +.PHONY: gen-test-golden +gen-test-golden: +gen-test-golden: PKG = ./cmd/helm ./pkg/action +gen-test-golden: TESTFLAGS = -update +gen-test-golden: test-unit + +# ------------------------------------------------------------------------------ +# dependencies + +# If go get is run from inside the project directory it will add the dependencies +# to the go.mod file. To avoid that we change to a directory without a go.mod file +# when downloading the following dependencies + +$(GOX): + (cd /; GO111MODULE=on go get -u github.com/mitchellh/gox) + +$(GOIMPORTS): + (cd /; GO111MODULE=on go get -u golang.org/x/tools/cmd/goimports) + +# ------------------------------------------------------------------------------ +# release + +.PHONY: build-cross +build-cross: LDFLAGS += -extldflags "-static" +build-cross: $(GOX) + GOFLAGS="-trimpath" GO111MODULE=on CGO_ENABLED=0 $(GOX) -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/$(BINNAME)" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/helm + +.PHONY: dist +dist: + ( \ + cd _dist && \ + $(DIST_DIRS) cp ../LICENSE {} \; && \ + $(DIST_DIRS) cp ../README.md {} \; && \ + $(DIST_DIRS) tar -zcf helm-${VERSION}-{}.tar.gz {} \; && \ + $(DIST_DIRS) zip -r helm-${VERSION}-{}.zip {} \; \ + ) + +.PHONY: fetch-dist +fetch-dist: + mkdir -p _dist + cd _dist && \ + for obj in ${TARGET_OBJS} ; do \ + curl -sSL -o helm-${VERSION}-$${obj} https://get.helm.sh/helm-${VERSION}-$${obj} ; \ + done + +.PHONY: sign +sign: + for f in $$(ls _dist/*.{gz,zip,sha256,sha256sum} 2>/dev/null) ; do \ + gpg --armor --detach-sign $${f} ; \ + done -.PHONY: docs -docs: build - @scripts/update-docs.sh +# The contents of the .sha256sum file are compatible with tools like +# shasum. For example, using the following command will verify +# the file helm-3.1.0-rc.1-darwin-amd64.tar.gz: +# shasum -a 256 -c helm-3.1.0-rc.1-darwin-amd64.tar.gz.sha256sum +# The .sha256 files hold only the hash and are not compatible with +# verification tools like shasum or sha256sum. This method and file can be +# removed in Helm v4. +.PHONY: checksum +checksum: + for f in $$(ls _dist/*.{gz,zip} 2>/dev/null) ; do \ + shasum -a 256 "$${f}" | sed 's/_dist\///' > "$${f}.sha256sum" ; \ + shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \ + done -.PHONY: verify-docs -verify-docs: build - @scripts/verify-docs.sh +# ------------------------------------------------------------------------------ .PHONY: clean clean: - @rm -rf $(BINDIR) ./rootfs/tiller ./_dist + @rm -rf '$(BINDIR)' ./_dist -.PHONY: coverage -coverage: - @scripts/coverage.sh +.PHONY: release-notes +release-notes: + @if [ ! -d "./_dist" ]; then \ + echo "please run 'make fetch-dist' first" && \ + exit 1; \ + fi + @if [ -z "${PREVIOUS_RELEASE}" ]; then \ + echo "please set PREVIOUS_RELEASE environment variable" \ + && exit 1; \ + fi -HAS_GLIDE := $(shell command -v glide;) -HAS_GOX := $(shell command -v gox;) -HAS_GIT := $(shell command -v git;) + @./scripts/release-notes.sh ${PREVIOUS_RELEASE} ${VERSION} -.PHONY: bootstrap -bootstrap: -ifndef HAS_GLIDE - go get -u github.com/Masterminds/glide -endif -ifndef HAS_GOX - go get -u github.com/mitchellh/gox -endif -ifndef HAS_GIT - $(error You must install Git) -endif - glide install --strip-vendor - go build -o bin/protoc-gen-go ./vendor/github.com/golang/protobuf/protoc-gen-go -include versioning.mk +.PHONY: info +info: + @echo "Version: ${VERSION}" + @echo "Git Tag: ${GIT_TAG}" + @echo "Git Commit: ${GIT_COMMIT}" + @echo "Git Tree State: ${GIT_DIRTY}" diff --git a/OWNERS b/OWNERS index 32b26efa26b..e7c95307790 100644 --- a/OWNERS +++ b/OWNERS @@ -1,30 +1,21 @@ maintainers: - adamreese - bacongobbler - - jascott1 + - fibonacci1729 + - hickeyma + - jdolitsky + - marckhouzam - mattfarina - - michelleN - - nebril - prydonius - SlickNik - technosophos - thomastaylor312 - viglesiasce -reviewers: - - adamreese - - bacongobbler - - fibonacci1729 +emeritus: - jascott1 - - mattfarina - michelleN - migmartri - nebril - - prydonius - - SlickNik - - technosophos - - thomastaylor312 - - viglesiasce -emeritus: - - migmartri - seh - vaikas-google + - rimusz diff --git a/README.md b/README.md index fb2e16bce71..5ae42118338 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# Kubernetes Helm +# Helm -[![CircleCI](https://circleci.com/gh/kubernetes/helm.svg?style=svg)](https://circleci.com/gh/kubernetes/helm) -[![Go Report Card](https://goreportcard.com/badge/github.com/kubernetes/helm)](https://goreportcard.com/report/github.com/kubernetes/helm) -[![GoDoc](https://godoc.org/github.com/kubernetes/helm?status.svg)](https://godoc.org/github.com/kubernetes/helm) +[![CircleCI](https://circleci.com/gh/helm/helm.svg?style=shield)](https://circleci.com/gh/helm/helm) +[![Go Report Card](https://goreportcard.com/badge/github.com/helm/helm)](https://goreportcard.com/report/github.com/helm/helm) +[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/helm.sh/helm/v3) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3131/badge)](https://bestpractices.coreinfrastructure.org/projects/3131) -Helm is a tool for managing Kubernetes charts. Charts are packages of -pre-configured Kubernetes resources. +Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources. Use Helm to: -- Find and use [popular software packaged as Kubernetes charts](https://github.com/kubernetes/charts) -- Share your own applications as Kubernetes charts +- Find and use [popular software packaged as Helm Charts](https://artifacthub.io/packages/search?kind=0) to run in Kubernetes +- Share your own applications as Helm Charts - Create reproducible builds of your Kubernetes applications - Intelligently manage your Kubernetes manifest files - Manage releases of Helm packages @@ -20,9 +20,7 @@ Use Helm to: Helm is a tool that streamlines installing and managing Kubernetes applications. Think of it like apt/yum/homebrew for Kubernetes. -- Helm has two parts: a client (`helm`) and a server (`tiller`) -- Tiller runs inside of your Kubernetes cluster, and manages releases (installations) - of your charts. +- Helm renders your templates and communicates with the Kubernetes API - Helm runs on your laptop, CI/CD, or wherever you want it to run. - Charts are Helm packages that contain at least two things: - A description of the package (`Chart.yaml`) @@ -32,46 +30,44 @@ Think of it like apt/yum/homebrew for Kubernetes. ## Install -Binary downloads of the Helm client can be found at the following links: -- [OSX](https://kubernetes-helm.storage.googleapis.com/helm-v2.8.2-darwin-amd64.tar.gz) -- [Linux](https://kubernetes-helm.storage.googleapis.com/helm-v2.8.2-linux-amd64.tar.gz) -- [Linux 32-bit](https://kubernetes-helm.storage.googleapis.com/helm-v2.8.2-linux-386.tar.gz) -- [Windows](https://kubernetes-helm.storage.googleapis.com/helm-v2.8.2-windows-amd64.tar.gz) +Binary downloads of the Helm client can be found on [the Releases page](https://github.com/helm/helm/releases/latest). Unpack the `helm` binary and add it to your PATH and you are good to go! If you want to use a package manager: -- macOS/[homebrew](https://brew.sh/) users can use `brew install kubernetes-helm`. -- Windows/[chocolatey](https://chocolatey.org/) users can use `choco install kubernetes-helm`. +- [Homebrew](https://brew.sh/) users can use `brew install helm`. +- [Chocolatey](https://chocolatey.org/) users can use `choco install kubernetes-helm`. +- [Scoop](https://scoop.sh/) users can use `scoop install helm`. +- [GoFish](https://gofi.sh/) users can use `gofish install helm`. +- [Snapcraft](https://snapcraft.io/) users can use `snap install helm --classic` -To rapidly get Helm up and running, start with the [Quick Start Guide](https://docs.helm.sh/using_helm/#quickstart-guide). +To rapidly get Helm up and running, start with the [Quick Start Guide](https://helm.sh/docs/intro/quickstart/). -See the [installation guide](https://docs.helm.sh/using_helm/#installing-helm) for more options, +See the [installation guide](https://helm.sh/docs/intro/install/) for more options, including installing pre-releases. ## Docs -Get started with the [Quick Start guide](https://docs.helm.sh/using_helm/#quickstart-guide) or plunge into the [complete documentation](https://docs.helm.sh) +Get started with the [Quick Start guide](https://helm.sh/docs/intro/quickstart/) or plunge into the [complete documentation](https://helm.sh/docs) ## Roadmap -The [Helm roadmap uses Github milestones](https://github.com/kubernetes/helm/milestones) to track the progress of the project. +The [Helm roadmap uses GitHub milestones](https://github.com/helm/helm/milestones) to track the progress of the project. ## Community, discussion, contribution, and support You can reach the Helm community and developers via the following channels: -- [Kubernetes Slack](http://slack.k8s.io): - - #helm-users - - #helm-dev - - #charts -- Mailing Lists: - - [Helm Mailing List](https://lists.cncf.io/g/cncf-kubernetes-helm) - - [Kubernetes SIG Apps Mailing List](https://groups.google.com/forum/#!forum/kubernetes-sig-apps) -- Developer Call: Thursdays at 9:30-10:00 Pacific. [https://zoom.us/j/4526666954](https://zoom.us/j/4526666954) +- [Kubernetes Slack](https://kubernetes.slack.com): + - [#helm-users](https://kubernetes.slack.com/messages/helm-users) + - [#helm-dev](https://kubernetes.slack.com/messages/helm-dev) + - [#charts](https://kubernetes.slack.com/messages/charts) +- Mailing List: + - [Helm Mailing List](https://lists.cncf.io/g/cncf-helm) +- Developer Call: Thursdays at 9:30-10:00 Pacific ([meeting details](https://github.com/helm/community/blob/master/communication.md#meetings)) ### Code of conduct -Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). +Participation in the Helm community is governed by the [Code of Conduct](code-of-conduct.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..c84a6f86631 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Helm Security Reporting and Policy + +The Helm project has [a common process and policy that can be found here](https://github.com/helm/community/blob/master/SECURITY.md). \ No newline at end of file diff --git a/_proto/Makefile b/_proto/Makefile deleted file mode 100644 index 39f72d441ec..00000000000 --- a/_proto/Makefile +++ /dev/null @@ -1,58 +0,0 @@ -space := $(empty) $(empty) -comma := , -empty := - -import_path = k8s.io/helm/pkg/proto/hapi - -dst = ../pkg/proto -target = go -plugins = grpc - -chart_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(chart_pkg),$(addprefix M,$(chart_pbs)))) -chart_pbs = $(sort $(wildcard hapi/chart/*.proto)) -chart_pkg = chart - -release_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(release_pkg),$(addprefix M,$(release_pbs)))) -release_pbs = $(sort $(wildcard hapi/release/*.proto)) -release_pkg = release - -services_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(services_pkg),$(addprefix M,$(services_pbs)))) -services_pbs = $(sort $(wildcard hapi/services/*.proto)) -services_pkg = services - -rudder_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(rudder_pkg),$(addprefix M,$(rudder_pbs)))) -rudder_pbs = $(sort $(wildcard hapi/rudder/*.proto)) -rudder_pkg = rudder - -version_ias = $(subst $(space),$(comma),$(addsuffix =$(import_path)/$(version_pkg),$(addprefix M,$(version_pbs)))) -version_pbs = $(sort $(wildcard hapi/version/*.proto)) -version_pkg = version - -google_deps = Mgoogle/protobuf/timestamp.proto=github.com/golang/protobuf/ptypes/timestamp,Mgoogle/protobuf/any.proto=github.com/golang/protobuf/ptypes/any - -.PHONY: all -all: chart release services rudder version - -.PHONY: chart -chart: - PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias):$(dst) $(chart_pbs) - -.PHONY: release -release: - PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias),$(version_ias):$(dst) $(release_pbs) - -.PHONY: services -services: - PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias),$(version_ias),$(release_ias):$(dst) $(services_pbs) - -.PHONY: rudder -rudder: - PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps),$(chart_ias),$(version_ias),$(release_ias):$(dst) $(rudder_pbs) - -.PHONY: version -version: - PATH=../bin:$(PATH) protoc --$(target)_out=plugins=$(plugins),$(google_deps):$(dst) $(version_pbs) - -.PHONY: clean -clean: - @rm -rf $(dst)/hapi 2>/dev/null diff --git a/_proto/hapi/chart/chart.proto b/_proto/hapi/chart/chart.proto deleted file mode 100644 index 9b838fd1a95..00000000000 --- a/_proto/hapi/chart/chart.proto +++ /dev/null @@ -1,44 +0,0 @@ -// 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.chart; - -import "hapi/chart/config.proto"; -import "hapi/chart/metadata.proto"; -import "hapi/chart/template.proto"; -import "google/protobuf/any.proto"; - -option go_package = "chart"; - -// Chart is a helm package that contains metadata, a default config, zero or more -// optionally parameterizable templates, and zero or more charts (dependencies). -message Chart { - // Contents of the Chartfile. - hapi.chart.Metadata metadata = 1; - - // Templates for this chart. - repeated hapi.chart.Template templates = 2; - - // Charts that this chart depends on. - repeated Chart dependencies = 3; - - // Default config for this template. - hapi.chart.Config values = 4; - - // Miscellaneous files in a chart archive, - // e.g. README, LICENSE, etc. - repeated google.protobuf.Any files = 5; -} diff --git a/_proto/hapi/chart/config.proto b/_proto/hapi/chart/config.proto deleted file mode 100644 index a1404476bd9..00000000000 --- a/_proto/hapi/chart/config.proto +++ /dev/null @@ -1,31 +0,0 @@ -// 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.chart; - -option go_package = "chart"; - -// Config supplies values to the parametrizable templates of a chart. -message Config { - string raw = 1; - - map values = 2; -} - -// Value describes a configuration value as a string. -message Value { - string value = 1; -} diff --git a/_proto/hapi/chart/metadata.proto b/_proto/hapi/chart/metadata.proto deleted file mode 100644 index 49d6a217a6b..00000000000 --- a/_proto/hapi/chart/metadata.proto +++ /dev/null @@ -1,93 +0,0 @@ -// 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.chart; - -option go_package = "chart"; - -// Maintainer describes a Chart maintainer. -message Maintainer { - // Name is a user name or organization name - string name = 1; - - // Email is an optional email address to contact the named maintainer - string email = 2; - - // Url is an optional URL to an address for the named maintainer - string url = 3; -} - -// Metadata for a Chart file. This models the structure of a Chart.yaml file. -// -// Spec: https://k8s.io/helm/blob/master/docs/design/chart_format.md#the-chart-file -message Metadata { - enum Engine { - UNKNOWN = 0; - GOTPL = 1; - } - // The name of the chart - string name = 1; - - // The URL to a relevant project page, git repo, or contact person - string home = 2; - - // Source is the URL to the source code of this chart - repeated string sources = 3; - - // A SemVer 2 conformant version string of the chart - string version = 4; - - // A one-sentence description of the chart - string description = 5; - - // A list of string keywords - repeated string keywords = 6; - - // A list of name and URL/email address combinations for the maintainer(s) - repeated Maintainer maintainers = 7; - - // The name of the template engine to use. Defaults to 'gotpl'. - string engine = 8; - - // The URL to an icon file. - string icon = 9; - - // The API Version of this chart. - string apiVersion = 10; - - // The condition to check to enable chart - string condition = 11; - - // The tags to check to enable chart - string tags = 12; - - // The version of the application enclosed inside of this chart. - string appVersion = 13; - - // Whether or not this chart is deprecated - bool deprecated = 14; - - // TillerVersion is a SemVer constraints on what version of Tiller is required. - // See SemVer ranges here: https://github.com/Masterminds/semver#basic-comparisons - string tillerVersion = 15; - - // Annotations are additional mappings uninterpreted by Tiller, - // made available for inspection by other applications. - map annotations = 16; - - // KubeVersion is a SemVer constraint specifying the version of Kubernetes required. - string kubeVersion = 17; -} diff --git a/_proto/hapi/chart/template.proto b/_proto/hapi/chart/template.proto deleted file mode 100644 index d13543ea973..00000000000 --- a/_proto/hapi/chart/template.proto +++ /dev/null @@ -1,31 +0,0 @@ -// 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.chart; - -option go_package = "chart"; - -// Template represents a template as a name/value pair. -// -// By convention, name is a relative path within the scope of the chart's -// base directory. -message Template { - // Name is the path-like name of the template. - string name = 1; - - // Data is the template as byte data. - bytes data = 2; -} diff --git a/_proto/hapi/release/hook.proto b/_proto/hapi/release/hook.proto deleted file mode 100644 index f0332ecb851..00000000000 --- a/_proto/hapi/release/hook.proto +++ /dev/null @@ -1,58 +0,0 @@ -// 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; - PRE_ROLLBACK = 7; - POST_ROLLBACK = 8; - RELEASE_TEST_SUCCESS = 9; - RELEASE_TEST_FAILURE = 10; - } - enum DeletePolicy { - SUCCEEDED = 0; - FAILED = 1; - BEFORE_HOOK_CREATION = 2; - } - 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; - // 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/_proto/hapi/release/info.proto b/_proto/hapi/release/info.proto deleted file mode 100644 index e23175d3d80..00000000000 --- a/_proto/hapi/release/info.proto +++ /dev/null @@ -1,37 +0,0 @@ -// 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"; -import "hapi/release/status.proto"; - -option go_package = "release"; - -// Info describes release information. -message Info { - Status status = 1; - - google.protobuf.Timestamp first_deployed = 2; - - google.protobuf.Timestamp last_deployed = 3; - - // Deleted tracks when this object was deleted. - google.protobuf.Timestamp deleted = 4; - - // Description is human-friendly "log entry" about this release. - string Description = 5; -} diff --git a/_proto/hapi/release/release.proto b/_proto/hapi/release/release.proto deleted file mode 100644 index 4a6afa0fe2e..00000000000 --- a/_proto/hapi/release/release.proto +++ /dev/null @@ -1,53 +0,0 @@ -// 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 "hapi/release/hook.proto"; -import "hapi/release/info.proto"; -import "hapi/chart/config.proto"; -import "hapi/chart/chart.proto"; - -option go_package = "release"; - -// Release describes a deployment of a chart, together with the chart -// and the variables used to deploy that chart. -message Release { - // Name is the name of the release - string name = 1; - - // Info provides information about a release - hapi.release.Info info = 2; - - // Chart is the chart that was released. - hapi.chart.Chart chart = 3; - - // Config is the set of extra Values added to the chart. - // These values override the default values inside of the chart. - hapi.chart.Config config = 4; - - // Manifest is the string representation of the rendered template. - string manifest = 5; - - // Hooks are all of the hooks declared for this release. - repeated hapi.release.Hook hooks = 6; - - // Version is an int32 which represents the version of the release. - int32 version = 7; - - // Namespace is the kubernetes namespace of the release. - string namespace = 8; -} diff --git a/_proto/hapi/release/status.proto b/_proto/hapi/release/status.proto deleted file mode 100644 index ef28a233c15..00000000000 --- a/_proto/hapi/release/status.proto +++ /dev/null @@ -1,61 +0,0 @@ -// 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 "hapi/release/test_suite.proto"; - -import "google/protobuf/any.proto"; - -option go_package = "release"; - -// Status defines the status of a release. -message Status { - enum Code { - // Status_UNKNOWN indicates that a release is in an uncertain state. - UNKNOWN = 0; - // Status_DEPLOYED indicates that the release has been pushed to Kubernetes. - DEPLOYED = 1; - // Status_DELETED indicates that a release has been deleted from Kubermetes. - DELETED = 2; - // Status_SUPERSEDED indicates that this release object is outdated and a newer one exists. - SUPERSEDED = 3; - // Status_FAILED indicates that the release was not successfully deployed. - FAILED = 4; - // Status_DELETING indicates that a delete operation is underway. - DELETING = 5; - // Status_PENDING_INSTALL indicates that an install operation is underway. - PENDING_INSTALL = 6; - // Status_PENDING_UPGRADE indicates that an upgrade operation is underway. - PENDING_UPGRADE = 7; - // Status_PENDING_ROLLBACK indicates that an rollback operation is underway. - PENDING_ROLLBACK = 8; - } - - Code code = 1; - - // Deprecated - // google.protobuf.Any details = 2; - - // Cluster resources as kubectl would print them. - string resources = 3; - - // Contains the rendered templates/NOTES.txt if available - string notes = 4; - - // LastTestSuiteRun provides results on the last test run on a release - hapi.release.TestSuite last_test_suite_run = 5; -} diff --git a/_proto/hapi/release/test_run.proto b/_proto/hapi/release/test_run.proto deleted file mode 100644 index 60734ae0384..00000000000 --- a/_proto/hapi/release/test_run.proto +++ /dev/null @@ -1,37 +0,0 @@ - -// 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"; - -message TestRun { - enum Status { - UNKNOWN = 0; - SUCCESS = 1; - FAILURE = 2; - RUNNING = 3; - } - - string name = 1; - Status status = 2; - string info = 3; - google.protobuf.Timestamp started_at = 4; - google.protobuf.Timestamp completed_at = 5; -} diff --git a/_proto/hapi/release/test_suite.proto b/_proto/hapi/release/test_suite.proto deleted file mode 100644 index 2f6feb08c49..00000000000 --- a/_proto/hapi/release/test_suite.proto +++ /dev/null @@ -1,34 +0,0 @@ -// 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"; -import "hapi/release/test_run.proto"; - -option go_package = "release"; - -// TestSuite comprises of the last run of the pre-defined test suite of a release version -message TestSuite { - // StartedAt indicates the date/time this test suite was kicked off - google.protobuf.Timestamp started_at = 1; - - // CompletedAt indicates the date/time this test suite was completed - google.protobuf.Timestamp completed_at = 2; - - // Results are the results of each segment of the test - repeated hapi.release.TestRun results = 3; -} diff --git a/_proto/hapi/rudder/rudder.proto b/_proto/hapi/rudder/rudder.proto deleted file mode 100644 index 3a37c9e99d2..00000000000 --- a/_proto/hapi/rudder/rudder.proto +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2017 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.services.rudder; - -import "hapi/release/info.proto"; -import "hapi/release/release.proto"; - -option go_package = "rudder"; - -service ReleaseModuleService { - rpc Version(VersionReleaseRequest) returns (VersionReleaseResponse) { - } - - // InstallRelease requests installation of a chart as a new release. - rpc InstallRelease(InstallReleaseRequest) returns (InstallReleaseResponse) { - } - - // DeleteRelease requests deletion of a named release. - rpc DeleteRelease(DeleteReleaseRequest) returns (DeleteReleaseResponse) { - } - - // RollbackRelease rolls back a release to a previous version. - rpc RollbackRelease(RollbackReleaseRequest) returns (RollbackReleaseResponse) { - } - - // UpgradeRelease updates release content. - rpc UpgradeRelease(UpgradeReleaseRequest) returns (UpgradeReleaseResponse) { - } - - // ReleaseStatus retrieves release status. - rpc ReleaseStatus(ReleaseStatusRequest) returns (ReleaseStatusResponse) { - } -} - -message Result { - enum Status { - // No status set - UNKNOWN = 0; - // Operation was successful - SUCCESS = 1; - // Operation had no results (e.g. upgrade identical, rollback to same, delete non-existent) - UNCHANGED = 2; - // Operation failed - ERROR = 3; - } - string info = 1; - repeated string log = 2; -} - -message VersionReleaseRequest { -} - -message VersionReleaseResponse { - string name = 1; // The canonical name of the release module - string version = 2; // The version of the release module -} - -message InstallReleaseRequest { - hapi.release.Release release = 1; -} -message InstallReleaseResponse { - hapi.release.Release release = 1; - Result result = 2; -} - -message DeleteReleaseRequest { - hapi.release.Release release = 1; -} -message DeleteReleaseResponse { - hapi.release.Release release = 1; - Result result = 2; -} - -message UpgradeReleaseRequest{ - hapi.release.Release current = 1; - hapi.release.Release target = 2; - int64 Timeout = 3; - bool Wait = 4; - bool Recreate = 5; - bool Force = 6; -} -message UpgradeReleaseResponse{ - hapi.release.Release release = 1; - Result result = 2; -} - -message RollbackReleaseRequest{ - hapi.release.Release current = 1; - hapi.release.Release target = 2; - int64 Timeout = 3; - bool Wait = 4; - bool Recreate = 5; - bool Force = 6; -} -message RollbackReleaseResponse{ - hapi.release.Release release = 1; - Result result = 2; -} - -message ReleaseStatusRequest{ - hapi.release.Release release = 1; -} -message ReleaseStatusResponse{ - hapi.release.Release release = 1; - hapi.release.Info info = 2; -} diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto deleted file mode 100644 index 5897676aba7..00000000000 --- a/_proto/hapi/services/tiller.proto +++ /dev/null @@ -1,337 +0,0 @@ -// 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.services.tiller; - -import "hapi/chart/chart.proto"; -import "hapi/chart/config.proto"; -import "hapi/release/release.proto"; -import "hapi/release/info.proto"; -import "hapi/release/test_run.proto"; -import "hapi/release/status.proto"; -import "hapi/version/version.proto"; - -option go_package = "services"; - -// ReleaseService is the service that a helm application uses to mutate, -// query, and manage releases. -// -// Release: A named installation composed of a chart and -// config. At any given time a release has one -// chart and one config. -// -// Config: A config is a YAML file that supplies values -// to the parametrizable templates of a chart. -// -// Chart: A chart is a helm package that contains -// metadata, a default config, zero or more -// optionally parameterizable templates, and -// zero or more charts (dependencies). -service ReleaseService { - // ListReleases retrieves release history. - // TODO: Allow filtering the set of releases by - // release status. By default, ListAllReleases returns the releases who - // current status is "Active". - rpc ListReleases(ListReleasesRequest) returns (stream ListReleasesResponse) { - } - - // GetReleasesStatus retrieves status information for the specified release. - rpc GetReleaseStatus(GetReleaseStatusRequest) returns (GetReleaseStatusResponse) { - } - - // GetReleaseContent retrieves the release content (chart + value) for the specified release. - rpc GetReleaseContent(GetReleaseContentRequest) returns (GetReleaseContentResponse) { - } - - // UpdateRelease updates release content. - rpc UpdateRelease(UpdateReleaseRequest) returns (UpdateReleaseResponse) { - } - - // InstallRelease requests installation of a chart as a new release. - rpc InstallRelease(InstallReleaseRequest) returns (InstallReleaseResponse) { - } - - // UninstallRelease requests deletion of a named release. - rpc UninstallRelease(UninstallReleaseRequest) returns (UninstallReleaseResponse) { - } - - // GetVersion returns the current version of the server. - rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) { - } - - // RollbackRelease rolls back a release to a previous version. - rpc RollbackRelease(RollbackReleaseRequest) returns (RollbackReleaseResponse) { - } - - // ReleaseHistory retrieves a releasse's history. - rpc GetHistory(GetHistoryRequest) returns (GetHistoryResponse) { - } - - // RunReleaseTest executes the tests defined of a named release - rpc RunReleaseTest(TestReleaseRequest) returns (stream TestReleaseResponse) { - } -} - -// ListReleasesRequest requests a list of releases. -// -// Releases can be retrieved in chunks by setting limit and offset. -// -// Releases can be sorted according to a few pre-determined sort stategies. -message ListReleasesRequest { - // Limit is the maximum number of releases to be returned. - int64 limit = 1; - - // Offset is the last release name that was seen. The next listing - // operation will start with the name after this one. - // Example: If list one returns albert, bernie, carl, and sets 'next: dennis'. - // dennis is the offset. Supplying 'dennis' for the next request should - // cause the next batch to return a set of results starting with 'dennis'. - string offset = 2; - - // SortBy is the sort field that the ListReleases server should sort data before returning. - ListSort.SortBy sort_by = 3; - - // Filter is a regular expression used to filter which releases should be listed. - // - // Anything that matches the regexp will be included in the results. - string filter = 4; - - // SortOrder is the ordering directive used for sorting. - ListSort.SortOrder sort_order = 5; - - repeated hapi.release.Status.Code status_codes = 6; - // Namespace is the filter to select releases only from a specific namespace. - string namespace = 7; -} - -// ListSort defines sorting fields on a release list. -message ListSort{ - // SortBy defines sort operations. - enum SortBy { - UNKNOWN = 0; - NAME = 1; - LAST_RELEASED = 2; - } - - // SortOrder defines sort orders to augment sorting operations. - enum SortOrder { - ASC = 0; - DESC = 1; - } -} - -// ListReleasesResponse is a list of releases. -message ListReleasesResponse { - // Count is the expected total number of releases to be returned. - int64 count = 1; - - // Next is the name of the next release. If this is other than an empty - // string, it means there are more results. - string next = 2; - - // Total is the total number of queryable releases. - int64 total = 3; - - // Releases is the list of found release objects. - repeated hapi.release.Release releases = 4; -} - -// GetReleaseStatusRequest is a request to get the status of a release. -message GetReleaseStatusRequest { - // Name is the name of the release - string name = 1; - // Version is the version of the release - int32 version = 2; -} - -// GetReleaseStatusResponse is the response indicating the status of the named release. -message GetReleaseStatusResponse { - // Name is the name of the release. - string name = 1; - - // Info contains information about the release. - hapi.release.Info info = 2; - - // Namespace the release was released into - string namespace = 3; -} - -// GetReleaseContentRequest is a request to get the contents of a release. -message GetReleaseContentRequest { - // The name of the release - string name = 1; - // Version is the version of the release - int32 version = 2; -} - -// GetReleaseContentResponse is a response containing the contents of a release. -message GetReleaseContentResponse { - // The release content - hapi.release.Release release = 1; -} - -// UpdateReleaseRequest updates a release. -message UpdateReleaseRequest { - // The name of the release - string name = 1; - // Chart is the protobuf representation of a chart. - hapi.chart.Chart chart = 2; - // Values is a string containing (unparsed) YAML values. - hapi.chart.Config values = 3; - // dry_run, if true, will run through the release logic, but neither create - bool dry_run = 4; - // DisableHooks causes the server to skip running any hooks for the upgrade. - bool disable_hooks = 5; - // Performs pods restart for resources if applicable - bool recreate = 6; - // timeout specifies the max amount of time any kubernetes client command can run. - int64 timeout = 7; - // ResetValues will cause Tiller to ignore stored values, resetting to default values. - bool reset_values = 8; - // wait, if true, will wait until all Pods, PVCs, and Services are in a ready state - // before marking the release as successful. It will wait for as long as timeout - bool wait = 9; - // ReuseValues will cause Tiller to reuse the values from the last release. - // This is ignored if reset_values is set. - bool reuse_values = 10; - // Force resource update through delete/recreate if needed. - bool force = 11; -} - -// UpdateReleaseResponse is the response to an update request. -message UpdateReleaseResponse { - hapi.release.Release release = 1; -} - -message RollbackReleaseRequest { - // The name of the release - string name = 1; - // dry_run, if true, will run through the release logic but no create - bool dry_run = 2; - // DisableHooks causes the server to skip running any hooks for the rollback - bool disable_hooks = 3; - // Version is the version of the release to deploy. - int32 version = 4; - // Performs pods restart for resources if applicable - bool recreate = 5; - // timeout specifies the max amount of time any kubernetes client command can run. - int64 timeout = 6; - // wait, if true, will wait until all Pods, PVCs, and Services are in a ready state - // before marking the release as successful. It will wait for as long as timeout - bool wait = 7; - // Force resource update through delete/recreate if needed. - bool force = 8; -} - -// RollbackReleaseResponse is the response to an update request. -message RollbackReleaseResponse { - hapi.release.Release release = 1; -} - -// InstallReleaseRequest is the request for an installation of a chart. -message InstallReleaseRequest { - // Chart is the protobuf representation of a chart. - hapi.chart.Chart chart = 1; - // Values is a string containing (unparsed) YAML values. - hapi.chart.Config values = 2; - // DryRun, if true, will run through the release logic, but neither create - // a release object nor deploy to Kubernetes. The release object returned - // in the response will be fake. - bool dry_run = 3; - - // Name is the candidate release name. This must be unique to the - // namespace, otherwise the server will return an error. If it is not - // supplied, the server will autogenerate one. - string name = 4; - - // DisableHooks causes the server to skip running any hooks for the install. - bool disable_hooks = 5; - - // Namepace is the kubernetes namespace of the release. - string namespace = 6; - - // ReuseName requests that Tiller re-uses a name, instead of erroring out. - bool reuse_name = 7; - - // timeout specifies the max amount of time any kubernetes client command can run. - int64 timeout = 8; - // wait, if true, will wait until all Pods, PVCs, and Services are in a ready state - // before marking the release as successful. It will wait for as long as timeout - bool wait = 9; -} - -// InstallReleaseResponse is the response from a release installation. -message InstallReleaseResponse { - hapi.release.Release release = 1; -} - -// UninstallReleaseRequest represents a request to uninstall a named release. -message UninstallReleaseRequest { - // Name is the name of the release to delete. - string name = 1; - // DisableHooks causes the server to skip running any hooks for the uninstall. - bool disable_hooks = 2; - // Purge removes the release from the store and make its name free for later use. - bool purge = 3; - // timeout specifies the max amount of time any kubernetes client command can run. - int64 timeout = 4; -} - -// UninstallReleaseResponse represents a successful response to an uninstall request. -message UninstallReleaseResponse { - // Release is the release that was marked deleted. - hapi.release.Release release = 1; - // Info is an uninstall message - string info = 2; -} - -// GetVersionRequest requests for version information. -message GetVersionRequest { -} - -message GetVersionResponse { - hapi.version.Version Version = 1; -} - -// GetHistoryRequest requests a release's history. -message GetHistoryRequest { - // The name of the release. - string name = 1; - // The maximum number of releases to include. - int32 max = 2; -} - -// GetHistoryResponse is received in response to a GetHistory rpc. -message GetHistoryResponse { - repeated hapi.release.Release releases = 1; -} - -// TestReleaseRequest is a request to get the status of a release. -message TestReleaseRequest { - // Name is the name of the release - string name = 1; - // timeout specifies the max amount of time any kubernetes client command can run. - int64 timeout = 2; - // cleanup specifies whether or not to attempt pod deletion after test completes - bool cleanup = 3; -} - -// TestReleaseResponse represents a message from executing a test -message TestReleaseResponse { - string msg = 1; - hapi.release.TestRun.Status status = 2; - -} diff --git a/_proto/hapi/version/version.proto b/_proto/hapi/version/version.proto deleted file mode 100644 index 0ae0985b786..00000000000 --- a/_proto/hapi/version/version.proto +++ /dev/null @@ -1,26 +0,0 @@ -// 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.version; - -option go_package = "version"; - -message Version { - // Sem ver string for the version - string sem_ver = 1; - string git_commit = 2; - string git_tree_state = 3; -} diff --git a/cmd/helm/chart.go b/cmd/helm/chart.go new file mode 100644 index 00000000000..adc874cfe3f --- /dev/null +++ b/cmd/helm/chart.go @@ -0,0 +1,49 @@ +/* +Copyright The Helm Authors. +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" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/pkg/action" +) + +const chartHelp = ` +This command consists of multiple subcommands to work with the chart cache. + +The subcommands can be used to push, pull, tag, list, or remove Helm charts. +` + +func newChartCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "chart", + Short: "push, pull, tag, or remove Helm charts", + Long: chartHelp, + Hidden: !FeatureGateOCI.IsEnabled(), + PersistentPreRunE: checkOCIFeatureGate(), + } + cmd.AddCommand( + newChartListCmd(cfg, out), + newChartExportCmd(cfg, out), + newChartPullCmd(cfg, out), + newChartPushCmd(cfg, out), + newChartRemoveCmd(cfg, out), + newChartSaveCmd(cfg, out), + ) + return cmd +} diff --git a/cmd/helm/chart_export.go b/cmd/helm/chart_export.go new file mode 100644 index 00000000000..67caf08d702 --- /dev/null +++ b/cmd/helm/chart_export.go @@ -0,0 +1,55 @@ +/* +Copyright The Helm Authors. + +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" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +const chartExportDesc = ` +Export a chart stored in local registry cache. + +This will create a new directory with the name of +the chart, in a format that developers can modify +and check into source control if desired. +` + +func newChartExportCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewChartExport(cfg) + + cmd := &cobra.Command{ + Use: "export [ref]", + Short: "export a chart to directory", + Long: chartExportDesc, + Args: require.MinimumNArgs(1), + Hidden: !FeatureGateOCI.IsEnabled(), + RunE: func(cmd *cobra.Command, args []string) error { + ref := args[0] + return client.Run(out, ref) + }, + } + + f := cmd.Flags() + f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.") + + return cmd +} diff --git a/cmd/helm/chart_list.go b/cmd/helm/chart_list.go new file mode 100644 index 00000000000..a9d01c9bde6 --- /dev/null +++ b/cmd/helm/chart_list.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. + +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" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/pkg/action" +) + +const chartListDesc = ` +List all charts in the local registry cache. + +Charts are sorted by ref name, alphabetically. +` + +func newChartListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "list all saved charts", + Long: chartListDesc, + Hidden: !FeatureGateOCI.IsEnabled(), + RunE: func(cmd *cobra.Command, args []string) error { + return action.NewChartList(cfg).Run(out) + }, + } +} diff --git a/cmd/helm/chart_pull.go b/cmd/helm/chart_pull.go new file mode 100644 index 00000000000..760ff3e2cdf --- /dev/null +++ b/cmd/helm/chart_pull.go @@ -0,0 +1,46 @@ +/* +Copyright The Helm Authors. + +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" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +const chartPullDesc = ` +Download a chart from a remote registry. + +This will store the chart in the local registry cache to be used later. +` + +func newChartPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "pull [ref]", + Short: "pull a chart from remote", + Long: chartPullDesc, + Args: require.MinimumNArgs(1), + Hidden: !FeatureGateOCI.IsEnabled(), + RunE: func(cmd *cobra.Command, args []string) error { + ref := args[0] + return action.NewChartPull(cfg).Run(out, ref) + }, + } +} diff --git a/cmd/helm/chart_push.go b/cmd/helm/chart_push.go new file mode 100644 index 00000000000..ff34632b145 --- /dev/null +++ b/cmd/helm/chart_push.go @@ -0,0 +1,48 @@ +/* +Copyright The Helm Authors. + +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" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +const chartPushDesc = ` +Upload a chart to a remote registry. + +Note: the ref must already exist in the local registry cache. + +Must first run "helm chart save" or "helm chart pull". +` + +func newChartPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "push [ref]", + Short: "push a chart to remote", + Long: chartPushDesc, + Args: require.MinimumNArgs(1), + Hidden: !FeatureGateOCI.IsEnabled(), + RunE: func(cmd *cobra.Command, args []string) error { + ref := args[0] + return action.NewChartPush(cfg).Run(out, ref) + }, + } +} diff --git a/cmd/helm/chart_remove.go b/cmd/helm/chart_remove.go new file mode 100644 index 00000000000..d952951fb6a --- /dev/null +++ b/cmd/helm/chart_remove.go @@ -0,0 +1,50 @@ +/* +Copyright The Helm Authors. + +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" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +const chartRemoveDesc = ` +Remove a chart from the local registry cache. + +Note: the chart content will still exist in the cache, +but it will no longer appear in "helm chart list". + +To remove all unlinked content, please run "helm chart prune". (TODO) +` + +func newChartRemoveCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "remove [ref]", + Aliases: []string{"rm"}, + Short: "remove a chart", + Long: chartRemoveDesc, + Args: require.MinimumNArgs(1), + Hidden: !FeatureGateOCI.IsEnabled(), + RunE: func(cmd *cobra.Command, args []string) error { + ref := args[0] + return action.NewChartRemove(cfg).Run(out, ref) + }, + } +} diff --git a/cmd/helm/chart_save.go b/cmd/helm/chart_save.go new file mode 100644 index 00000000000..35b72cd0739 --- /dev/null +++ b/cmd/helm/chart_save.go @@ -0,0 +1,61 @@ +/* +Copyright The Helm Authors. + +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" + "path/filepath" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart/loader" +) + +const chartSaveDesc = ` +Store a copy of chart in local registry cache. + +Note: modifying the chart after this operation will +not change the item as it exists in the cache. +` + +func newChartSaveCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "save [path] [ref]", + Short: "save a chart directory", + Long: chartSaveDesc, + Args: require.MinimumNArgs(2), + Hidden: !FeatureGateOCI.IsEnabled(), + RunE: func(cmd *cobra.Command, args []string) error { + path := args[0] + ref := args[1] + + path, err := filepath.Abs(path) + if err != nil { + return err + } + + ch, err := loader.Load(path) + if err != nil { + return err + } + + return action.NewChartSave(cfg).Run(out, ch, ref) + }, + } +} diff --git a/cmd/helm/completion.go b/cmd/helm/completion.go index b1cd04140ca..5aea0592e4f 100644 --- a/cmd/helm/completion.go +++ b/cmd/helm/completion.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,214 +16,163 @@ limitations under the License. package main import ( - "bytes" "fmt" "io" + "os" + "path/filepath" "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" ) const completionDesc = ` -Generate autocompletions script for Helm for the specified shell (bash or zsh). +Generate autocompletion scripts for Helm for the specified shell. +` +const bashCompDesc = ` +Generate the autocompletion script for Helm for the bash shell. + +To load completions in your current shell session: +$ source <(helm completion bash) -This command can generate shell autocompletions. e.g. +To load completions for every new session, execute once: +Linux: + $ helm completion bash > /etc/bash_completion.d/helm +MacOS: + $ helm completion bash > /usr/local/etc/bash_completion.d/helm +` - $ helm completion bash +const zshCompDesc = ` +Generate the autocompletion script for Helm for the zsh shell. -Can be sourced as such +To load completions in your current shell session: +$ source <(helm completion zsh) - $ source <(helm completion bash) +To load completions for every new session, execute once: +$ helm completion zsh > "${fpath[1]}/_helm" ` -var ( - completionShells = map[string]func(out io.Writer, cmd *cobra.Command) error{ - "bash": runCompletionBash, - "zsh": runCompletionZsh, - } +const fishCompDesc = ` +Generate the autocompletion script for Helm for the fish shell. + +To load completions in your current shell session: +$ helm completion fish | source + +To load completions for every new session, execute once: +$ helm completion fish > ~/.config/fish/completions/helm.fish + +You will need to start a new shell for this setup to take effect. +` + +const ( + noDescFlagName = "no-descriptions" + noDescFlagText = "disable completion descriptions" ) -func newCompletionCmd(out io.Writer) *cobra.Command { - shells := []string{} - for s := range completionShells { - shells = append(shells, s) - } +var disableCompDescriptions bool +func newCompletionCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "completion SHELL", - Short: "Generate autocompletions script for the specified shell (bash or zsh)", + Use: "completion", + Short: "generate autocompletion scripts for the specified shell", Long: completionDesc, + Args: require.NoArgs, + } + + bash := &cobra.Command{ + Use: "bash", + Short: "generate autocompletion script for bash", + Long: bashCompDesc, + Args: require.NoArgs, + DisableFlagsInUseLine: true, + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { - return runCompletion(out, cmd, args) + return runCompletionBash(out, cmd) }, - ValidArgs: shells, } - return cmd -} - -func runCompletion(out io.Writer, cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return fmt.Errorf("shell not specified") - } - if len(args) > 1 { - return fmt.Errorf("too many arguments, expected only the shell type") + zsh := &cobra.Command{ + Use: "zsh", + Short: "generate autocompletion script for zsh", + Long: zshCompDesc, + Args: require.NoArgs, + ValidArgsFunction: noCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return runCompletionZsh(out, cmd) + }, } - run, found := completionShells[args[0]] - if !found { - return fmt.Errorf("unsupported shell type %q", args[0]) + zsh.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) + + fish := &cobra.Command{ + Use: "fish", + Short: "generate autocompletion script for fish", + Long: fishCompDesc, + Args: require.NoArgs, + ValidArgsFunction: noCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return runCompletionFish(out, cmd) + }, } + fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) - return run(out, cmd) + cmd.AddCommand(bash, zsh, fish) + + return cmd } func runCompletionBash(out io.Writer, cmd *cobra.Command) error { - return cmd.Root().GenBashCompletion(out) + err := cmd.Root().GenBashCompletion(out) + + // In case the user renamed the helm binary (e.g., to be able to run + // both helm2 and helm3), we hook the new binary name to the completion function + if binary := filepath.Base(os.Args[0]); binary != "helm" { + renamedBinaryHook := ` +# Hook the command used to generate the completion script +# to the helm completion function to handle the case where +# the user renamed the helm binary +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_helm %[1]s +else + complete -o default -o nospace -F __start_helm %[1]s +fi +` + fmt.Fprintf(out, renamedBinaryHook, binary) + } + + return err } func runCompletionZsh(out io.Writer, cmd *cobra.Command) error { - zshInitialization := ` -__helm_bash_source() { - alias shopt=':' - alias _expand=_bash_expand - alias _complete=_bash_comp - emulate -L sh - setopt kshglob noshglob braceexpand - source "$@" -} -__helm_type() { - # -t is not supported by zsh - if [ "$1" == "-t" ]; then - shift - # fake Bash 4 to disable "complete -o nospace". Instead - # "compopt +-o nospace" is used in the code to toggle trailing - # spaces. We don't support that, but leave trailing spaces on - # all the time - if [ "$1" = "__helm_compopt" ]; then - echo builtin - return 0 - fi - fi - type "$@" -} -__helm_compgen() { - local completions w - completions=( $(compgen "$@") ) || return $? - # filter by given word as prefix - while [[ "$1" = -* && "$1" != -- ]]; do - shift - shift - done - if [[ "$1" == -- ]]; then - shift - fi - for w in "${completions[@]}"; do - if [[ "${w}" = "$1"* ]]; then - echo "${w}" - fi - done -} -__helm_compopt() { - true # don't do anything. Not supported by bashcompinit in zsh -} -__helm_declare() { - if [ "$1" == "-F" ]; then - whence -w "$@" - else - builtin declare "$@" - fi -} -__helm_ltrim_colon_completions() -{ - if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then - # Remove colon-word prefix from COMPREPLY items - local colon_word=${1%${1##*:}} - local i=${#COMPREPLY[*]} - while [[ $((--i)) -ge 0 ]]; do - COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"} - done - fi -} -__helm_get_comp_words_by_ref() { - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[${COMP_CWORD}-1]}" - words=("${COMP_WORDS[@]}") - cword=("${COMP_CWORD[@]}") -} -__helm_filedir() { - local RET OLD_IFS w qw - __debug "_filedir $@ cur=$cur" - if [[ "$1" = \~* ]]; then - # somehow does not work. Maybe, zsh does not call this at all - eval echo "$1" - return 0 - fi - OLD_IFS="$IFS" - IFS=$'\n' - if [ "$1" = "-d" ]; then - shift - RET=( $(compgen -d) ) - else - RET=( $(compgen -f) ) - fi - IFS="$OLD_IFS" - IFS="," __debug "RET=${RET[@]} len=${#RET[@]}" - for w in ${RET[@]}; do - if [[ ! "${w}" = "${cur}"* ]]; then - continue - fi - if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then - qw="$(__helm_quote "${w}")" - if [ -d "${w}" ]; then - COMPREPLY+=("${qw}/") - else - COMPREPLY+=("${qw}") - fi - fi - done -} -__helm_quote() { - if [[ $1 == \'* || $1 == \"* ]]; then - # Leave out first character - printf %q "${1:1}" - else - printf %q "$1" - fi -} -autoload -U +X bashcompinit && bashcompinit -# use word boundary patterns for BSD or GNU sed -LWORD='[[:<:]]' -RWORD='[[:>:]]' -if sed --help 2>&1 | grep -q GNU; then - LWORD='\<' - RWORD='\>' -fi -__helm_convert_bash_to_zsh() { - sed \ - -e 's/declare -F/whence -w/' \ - -e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \ - -e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \ - -e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \ - -e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \ - -e "s/${LWORD}_filedir${RWORD}/__helm_filedir/g" \ - -e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__helm_get_comp_words_by_ref/g" \ - -e "s/${LWORD}__ltrim_colon_completions${RWORD}/__helm_ltrim_colon_completions/g" \ - -e "s/${LWORD}compgen${RWORD}/__helm_compgen/g" \ - -e "s/${LWORD}compopt${RWORD}/__helm_compopt/g" \ - -e "s/${LWORD}declare${RWORD}/__helm_declare/g" \ - -e "s/\\\$(type${RWORD}/\$(__helm_type/g" \ - <<'BASH_COMPLETION_EOF' + var err error + if disableCompDescriptions { + err = cmd.Root().GenZshCompletionNoDesc(out) + } else { + err = cmd.Root().GenZshCompletion(out) + } + + // In case the user renamed the helm binary (e.g., to be able to run + // both helm2 and helm3), we hook the new binary name to the completion function + if binary := filepath.Base(os.Args[0]); binary != "helm" { + renamedBinaryHook := ` +# Hook the command used to generate the completion script +# to the helm completion function to handle the case where +# the user renamed the helm binary +compdef _helm %[1]s ` - out.Write([]byte(zshInitialization)) + fmt.Fprintf(out, renamedBinaryHook, binary) + } - buf := new(bytes.Buffer) - cmd.Root().GenBashCompletion(buf) - out.Write(buf.Bytes()) + // Cobra doesn't source zsh completion file, explicitly doing it here + fmt.Fprintf(out, "compdef _helm helm") - zshTail := ` -BASH_COMPLETION_EOF + return err } -__helm_bash_source <(__helm_convert_bash_to_zsh) -` - out.Write([]byte(zshTail)) - return nil + +func runCompletionFish(out io.Writer, cmd *cobra.Command) error { + return cmd.Root().GenFishCompletion(out, !disableCompDescriptions) +} + +// Function to disable file completion +func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/helm/completion_test.go b/cmd/helm/completion_test.go new file mode 100644 index 00000000000..6d53616d01e --- /dev/null +++ b/cmd/helm/completion_test.go @@ -0,0 +1,93 @@ +/* +Copyright The Helm Authors. + +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" + "strings" + "testing" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/release" +) + +// Check if file completion should be performed according to parameter 'shouldBePerformed' +func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) { + storage := storageFixture() + storage.Create(&release.Release{ + Name: "myrelease", + Info: &release.Info{Status: release.StatusDeployed}, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "Myrelease-Chart", + Version: "1.2.3", + }, + }, + Version: 1, + }) + + testcmd := fmt.Sprintf("__complete %s ''", cmdName) + _, out, err := executeActionCommandC(storage, testcmd) + if err != nil { + t.Errorf("unexpected error, %s", err) + } + if !strings.Contains(out, "ShellCompDirectiveNoFileComp") != shouldBePerformed { + if shouldBePerformed { + t.Error(fmt.Sprintf("Unexpected directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName)) + } else { + + t.Error(fmt.Sprintf("Did not receive directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName)) + } + t.Log(out) + } +} + +func TestCompletionFileCompletion(t *testing.T) { + checkFileCompletion(t, "completion", false) + checkFileCompletion(t, "completion bash", false) + checkFileCompletion(t, "completion zsh", false) + checkFileCompletion(t, "completion fish", false) +} + +func checkReleaseCompletion(t *testing.T, cmdName string, multiReleasesAllowed bool) { + multiReleaseTestGolden := "output/empty_nofile_comp.txt" + if multiReleasesAllowed { + multiReleaseTestGolden = "output/release_list_repeat_comp.txt" + } + tests := []cmdTestCase{{ + name: "completion for uninstall", + cmd: fmt.Sprintf("__complete %s ''", cmdName), + golden: "output/release_list_comp.txt", + rels: []*release.Release{ + release.Mock(&release.MockReleaseOptions{Name: "athos"}), + release.Mock(&release.MockReleaseOptions{Name: "porthos"}), + release.Mock(&release.MockReleaseOptions{Name: "aramis"}), + }, + }, { + name: "completion for uninstall repetition", + cmd: fmt.Sprintf("__complete %s porthos ''", cmdName), + golden: multiReleaseTestGolden, + rels: []*release.Release{ + release.Mock(&release.MockReleaseOptions{Name: "athos"}), + release.Mock(&release.MockReleaseOptions{Name: "porthos"}), + release.Mock(&release.MockReleaseOptions{Name: "aramis"}), + }, + }} + for _, test := range tests { + runTestCmd(t, []cmdTestCase{test}) + } +} diff --git a/cmd/helm/create.go b/cmd/helm/create.go index 556ff171d71..fe5cc540a66 100644 --- a/cmd/helm/create.go +++ b/cmd/helm/create.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,16 +17,16 @@ limitations under the License. package main import ( - "errors" "fmt" "io" "path/filepath" "github.com/spf13/cobra" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/proto/hapi/chart" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/helmpath" ) const createDesc = ` @@ -36,17 +36,13 @@ directories used in a chart. For example, 'helm create foo' will create a directory structure that looks something like this: - foo/ - | - |- .helmignore # Contains patterns to ignore when packaging Helm charts. - | - |- Chart.yaml # Information about your chart - | - |- values.yaml # The default values for your templates - | - |- charts/ # Charts that this chart depends on - | - |- templates/ # The template files + foo/ + ├── .helmignore # Contains patterns to ignore when packaging Helm charts. + ├── Chart.yaml # Information about your chart + ├── values.yaml # The default values for your templates + ├── charts/ # Charts that this chart depends on + └── templates/ # The template files + └── tests/ # The test files 'helm create' takes a path for an argument. If directories in the given path do not exist, Helm will attempt to create them as it goes. If the given @@ -54,52 +50,64 @@ destination exists and there are files in that directory, conflicting files will be overwritten, but other files will be left alone. ` -type createCmd struct { - home helmpath.Home - name string - out io.Writer - starter string +type createOptions struct { + starter string // --starter + name string + starterDir string } func newCreateCmd(out io.Writer) *cobra.Command { - cc := &createCmd{out: out} + o := &createOptions{} 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 { - cc.home = settings.Home + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { - return errors.New("the name of the new chart is required") + // Allow file completion when completing the argument for the name + // which could be a path + return nil, cobra.ShellCompDirectiveDefault } - cc.name = args[0] - return cc.run() + // No more completions, so disable file completion + return nil, cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(cmd *cobra.Command, args []string) error { + o.name = args[0] + o.starterDir = helmpath.DataPath("starters") + return o.run(out) }, } - cmd.Flags().StringVarP(&cc.starter, "starter", "p", "", "the named Helm starter scaffold") + cmd.Flags().StringVarP(&o.starter, "starter", "p", "", "the name or absolute path to Helm starter scaffold") return cmd } -func (c *createCmd) run() error { - fmt.Fprintf(c.out, "Creating %s\n", c.name) +func (o *createOptions) run(out io.Writer) error { + fmt.Fprintf(out, "Creating %s\n", o.name) - chartname := filepath.Base(c.name) + chartname := filepath.Base(o.name) cfile := &chart.Metadata{ Name: chartname, Description: "A Helm chart for Kubernetes", + Type: "application", Version: "0.1.0", - AppVersion: "1.0", - ApiVersion: chartutil.ApiVersionV1, + AppVersion: "0.1.0", + APIVersion: chart.APIVersionV2, } - if c.starter != "" { + if o.starter != "" { // Create from the starter - lstarter := filepath.Join(c.home.Starters(), c.starter) - return chartutil.CreateFrom(cfile, filepath.Dir(c.name), lstarter) + lstarter := filepath.Join(o.starterDir, o.starter) + // If path is absolute, we don't want to prefix it with helm starters folder + if filepath.IsAbs(o.starter) { + lstarter = o.starter + } + return chartutil.CreateFrom(cfile, filepath.Dir(o.name), lstarter) } - _, err := chartutil.Create(cfile, filepath.Dir(c.name)) + chartutil.Stderr = out + _, err := chartutil.Create(chartname, filepath.Dir(o.name)) return err } diff --git a/cmd/helm/create_test.go b/cmd/helm/create_test.go index 84ccebecebc..1db6bed52ab 100644 --- a/cmd/helm/create_test.go +++ b/cmd/helm/create_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,37 +17,72 @@ limitations under the License. package main import ( + "fmt" "io/ioutil" "os" "path/filepath" "testing" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/proto/hapi/chart" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/helmpath" ) func TestCreateCmd(t *testing.T) { + defer ensure.HelmHome(t)() cname := "testchart" - // Make a temp dir - tdir, err := ioutil.TempDir("", "helm-create-") - if err != nil { - t.Fatal(err) + dir := ensure.TempDir(t) + defer testChdir(t, dir)() + + // Run a create + if _, _, err := executeActionCommand("create " + cname); err != nil { + t.Fatalf("Failed to run create: %s", err) + } + + // Test that the chart is there + if fi, err := os.Stat(cname); err != nil { + t.Fatalf("no chart directory: %s", err) + } else if !fi.IsDir() { + t.Fatalf("chart is not directory") } - defer os.RemoveAll(tdir) - // CD into it - pwd, err := os.Getwd() + c, err := loader.LoadDir(cname) if err != nil { t.Fatal(err) } - if err := os.Chdir(tdir); err != nil { - t.Fatal(err) + + if c.Name() != cname { + t.Errorf("Expected %q name, got %q", cname, c.Name()) + } + if c.Metadata.APIVersion != chart.APIVersionV2 { + t.Errorf("Wrong API version: %q", c.Metadata.APIVersion) + } +} + +func TestCreateStarterCmd(t *testing.T) { + defer ensure.HelmHome(t)() + cname := "testchart" + defer resetEnv()() + os.MkdirAll(helmpath.CachePath(), 0755) + defer testChdir(t, helmpath.CachePath())() + + // Create a starter. + starterchart := helmpath.DataPath("starters") + os.MkdirAll(starterchart, 0755) + if dest, err := chartutil.Create("starterchart", starterchart); err != nil { + t.Fatalf("Could not create chart: %s", err) + } else { + t.Logf("Created %s", dest) + } + tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl") + if err := ioutil.WriteFile(tplpath, []byte("test"), 0644); err != nil { + t.Fatalf("Could not write template: %s", err) } - defer os.Chdir(pwd) // Run a create - cmd := newCreateCmd(ioutil.Discard) - if err := cmd.RunE(cmd, []string{cname}); err != nil { + if _, _, err := executeActionCommand(fmt.Sprintf("create --starter=starterchart %s", cname)); err != nil { t.Errorf("Failed to run create: %s", err) return } @@ -59,67 +94,63 @@ func TestCreateCmd(t *testing.T) { t.Fatalf("chart is not directory") } - c, err := chartutil.LoadDir(cname) + c, err := loader.LoadDir(cname) if err != nil { t.Fatal(err) } - if c.Metadata.Name != cname { - t.Errorf("Expected %q name, got %q", cname, c.Metadata.Name) + if c.Name() != cname { + t.Errorf("Expected %q name, got %q", cname, c.Name()) } - if c.Metadata.ApiVersion != chartutil.ApiVersionV1 { - t.Errorf("Wrong API version: %q", c.Metadata.ApiVersion) + if c.Metadata.APIVersion != chart.APIVersionV2 { + t.Errorf("Wrong API version: %q", c.Metadata.APIVersion) } -} -func TestCreateStarterCmd(t *testing.T) { - cname := "testchart" - // Make a temp dir - tdir, err := ioutil.TempDir("", "helm-create-") - if err != nil { - t.Fatal(err) + expectedNumberOfTemplates := 9 + if l := len(c.Templates); l != expectedNumberOfTemplates { + t.Errorf("Expected %d templates, got %d", expectedNumberOfTemplates, l) } - defer os.RemoveAll(tdir) - thome, err := tempHelmHome(t) - if err != nil { - t.Fatal(err) + found := false + for _, tpl := range c.Templates { + if tpl.Name == "templates/foo.tpl" { + found = true + if data := string(tpl.Data); data != "test" { + t.Errorf("Expected template 'test', got %q", data) + } + } + } + if !found { + t.Error("Did not find foo.tpl") } - cleanup := resetEnv() - defer func() { - os.RemoveAll(thome.String()) - cleanup() - }() - settings.Home = thome +} + +func TestCreateStarterAbsoluteCmd(t *testing.T) { + defer resetEnv()() + defer ensure.HelmHome(t)() + cname := "testchart" // Create a starter. - starterchart := filepath.Join(thome.String(), "starters") - os.Mkdir(starterchart, 0755) - if dest, err := chartutil.Create(&chart.Metadata{Name: "starterchart"}, starterchart); err != nil { + starterchart := helmpath.DataPath("starters") + os.MkdirAll(starterchart, 0755) + if dest, err := chartutil.Create("starterchart", starterchart); err != nil { t.Fatalf("Could not create chart: %s", err) } else { t.Logf("Created %s", dest) } tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl") - if err := ioutil.WriteFile(tplpath, []byte("test"), 0755); err != nil { + if err := ioutil.WriteFile(tplpath, []byte("test"), 0644); err != nil { t.Fatalf("Could not write template: %s", err) } - // CD into it - pwd, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - if err := os.Chdir(tdir); err != nil { - t.Fatal(err) - } - defer os.Chdir(pwd) + os.MkdirAll(helmpath.CachePath(), 0755) + defer testChdir(t, helmpath.CachePath())() + + starterChartPath := filepath.Join(starterchart, "starterchart") // Run a create - cmd := newCreateCmd(ioutil.Discard) - cmd.ParseFlags([]string{"--starter", "starterchart"}) - if err := cmd.RunE(cmd, []string{cname}); err != nil { + if _, _, err := executeActionCommand(fmt.Sprintf("create --starter=%s %s", starterChartPath, cname)); err != nil { t.Errorf("Failed to run create: %s", err) return } @@ -131,34 +162,38 @@ func TestCreateStarterCmd(t *testing.T) { t.Fatalf("chart is not directory") } - c, err := chartutil.LoadDir(cname) + c, err := loader.LoadDir(cname) if err != nil { t.Fatal(err) } - if c.Metadata.Name != cname { - t.Errorf("Expected %q name, got %q", cname, c.Metadata.Name) + if c.Name() != cname { + t.Errorf("Expected %q name, got %q", cname, c.Name()) } - if c.Metadata.ApiVersion != chartutil.ApiVersionV1 { - t.Errorf("Wrong API version: %q", c.Metadata.ApiVersion) + if c.Metadata.APIVersion != chart.APIVersionV2 { + t.Errorf("Wrong API version: %q", c.Metadata.APIVersion) } - if l := len(c.Templates); l != 6 { - t.Errorf("Expected 5 templates, got %d", l) + expectedNumberOfTemplates := 9 + if l := len(c.Templates); l != expectedNumberOfTemplates { + t.Errorf("Expected %d templates, got %d", expectedNumberOfTemplates, l) } found := false for _, tpl := range c.Templates { if tpl.Name == "templates/foo.tpl" { found = true - data := tpl.Data - if string(data) != "test" { - t.Errorf("Expected template 'test', got %q", string(data)) + if data := string(tpl.Data); data != "test" { + t.Errorf("Expected template 'test', got %q", data) } } } if !found { t.Error("Did not find foo.tpl") } +} +func TestCreateFileCompletion(t *testing.T) { + checkFileCompletion(t, "create", true) + checkFileCompletion(t, "create myname", false) } diff --git a/cmd/helm/delete.go b/cmd/helm/delete.go deleted file mode 100755 index e0ac8c4f6cd..00000000000 --- a/cmd/helm/delete.go +++ /dev/null @@ -1,101 +0,0 @@ -/* -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" - - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" -) - -const deleteDesc = ` -This command takes a release name, and then deletes the release from Kubernetes. -It removes all of the resources associated with the last release of the chart. - -Use the '--dry-run' flag to see which releases will be deleted without actually -deleting them. -` - -type deleteCmd struct { - name string - dryRun bool - disableHooks bool - purge bool - timeout int64 - - out io.Writer - client helm.Interface -} - -func newDeleteCmd(c helm.Interface, out io.Writer) *cobra.Command { - del := &deleteCmd{ - out: out, - client: c, - } - - cmd := &cobra.Command{ - Use: "delete [flags] RELEASE_NAME [...]", - Aliases: []string{"del"}, - SuggestFor: []string{"remove", "rm"}, - Short: "given a release name, delete the release from Kubernetes", - Long: deleteDesc, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errors.New("command 'delete' requires a release name") - } - del.client = ensureHelmClient(del.client) - - for i := 0; i < len(args); i++ { - del.name = args[i] - if err := del.run(); err != nil { - return err - } - - fmt.Fprintf(out, "release \"%s\" deleted\n", del.name) - } - return nil - }, - } - - f := cmd.Flags() - f.BoolVar(&del.dryRun, "dry-run", false, "simulate a delete") - f.BoolVar(&del.disableHooks, "no-hooks", false, "prevent hooks from running during deletion") - f.BoolVar(&del.purge, "purge", false, "remove the release from the store and make its name free for later use") - f.Int64Var(&del.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") - - return cmd -} - -func (d *deleteCmd) run() error { - opts := []helm.DeleteOption{ - helm.DeleteDryRun(d.dryRun), - helm.DeleteDisableHooks(d.disableHooks), - helm.DeletePurge(d.purge), - helm.DeleteTimeout(d.timeout), - } - res, err := d.client.DeleteRelease(d.name, opts...) - if res != nil && res.Info != "" { - fmt.Fprintln(d.out, res.Info) - } - - return prettyError(err) -} diff --git a/cmd/helm/delete_test.go b/cmd/helm/delete_test.go deleted file mode 100644 index 829d179069b..00000000000 --- a/cmd/helm/delete_test.go +++ /dev/null @@ -1,73 +0,0 @@ -/* -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" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" -) - -func TestDelete(t *testing.T) { - - tests := []releaseCase{ - { - name: "basic delete", - args: []string{"aeneas"}, - flags: []string{}, - expected: "", // Output of a delete is an empty string and exit 0. - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, - }, - { - name: "delete with timeout", - args: []string{"aeneas"}, - flags: []string{"--timeout", "120"}, - expected: "", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, - }, - { - name: "delete without hooks", - args: []string{"aeneas"}, - flags: []string{"--no-hooks"}, - expected: "", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, - }, - { - name: "purge", - args: []string{"aeneas"}, - flags: []string{"--purge"}, - expected: "", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, - }, - { - name: "delete without release", - args: []string{}, - err: true, - }, - } - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newDeleteCmd(c, out) - }) -} diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go index 231659691a7..6bb82e217d5 100644 --- a/cmd/helm/dependency.go +++ b/cmd/helm/dependency.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,34 +16,29 @@ limitations under the License. package main import ( - "fmt" "io" - "os" "path/filepath" - "github.com/Masterminds/semver" - "github.com/gosuri/uitable" "github.com/spf13/cobra" - "k8s.io/helm/pkg/chartutil" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" ) const dependencyDesc = ` Manage the dependencies of a chart. Helm charts store their dependencies in 'charts/'. For chart developers, it is -often easier to manage a single dependency file ('requirements.yaml') -which declares all dependencies. +often easier to manage dependencies in 'Chart.yaml' which declares all +dependencies. The dependency commands operate on that file, making it easy to synchronize between the desired dependencies and the actual dependencies stored in the 'charts/' directory. -A 'requirements.yaml' file is a YAML file in which developers can declare chart -dependencies, along with the location of the chart and the desired version. -For example, this requirements file declares two dependencies: +For example, this Chart.yaml declares two dependencies: - # requirements.yaml + # Chart.yaml dependencies: - name: nginx version: "1.2.3" @@ -52,6 +47,7 @@ For example, this requirements file declares two dependencies: version: "3.2.1" repository: "https://another.example.com/charts" + The 'name' should be the name of a chart, where that name must match the name in that chart's 'Chart.yaml' file. @@ -66,7 +62,7 @@ Starting from 2.2.0, repository can be defined as the path to the directory of the dependency charts stored locally. The path should start with a prefix of "file://". For example, - # requirements.yaml + # Chart.yaml dependencies: - name: nginx version: "1.2.3" @@ -83,194 +79,41 @@ List all of the dependencies declared in a chart. This can take chart archives and chart directories as input. It will not alter the contents of a chart. -This will produce an error if the chart cannot be loaded. It will emit a warning -if it cannot find a requirements.yaml. +This will produce an error if the chart cannot be loaded. ` -func newDependencyCmd(out io.Writer) *cobra.Command { +func newDependencyCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "dependency update|build|list", Aliases: []string{"dep", "dependencies"}, Short: "manage a chart's dependencies", Long: dependencyDesc, + Args: require.NoArgs, } cmd.AddCommand(newDependencyListCmd(out)) - cmd.AddCommand(newDependencyUpdateCmd(out)) - cmd.AddCommand(newDependencyBuildCmd(out)) + cmd.AddCommand(newDependencyUpdateCmd(cfg, out)) + cmd.AddCommand(newDependencyBuildCmd(cfg, out)) return cmd } -type dependencyListCmd struct { - out io.Writer - chartpath string -} - func newDependencyListCmd(out io.Writer) *cobra.Command { - dlc := &dependencyListCmd{out: out} + client := action.NewDependency() cmd := &cobra.Command{ - Use: "list [flags] CHART", + Use: "list CHART", Aliases: []string{"ls"}, Short: "list the dependencies for the given chart", Long: dependencyListDesc, + Args: require.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cp := "." + chartpath := "." if len(args) > 0 { - cp = args[0] + chartpath = filepath.Clean(args[0]) } - - var err error - dlc.chartpath, err = filepath.Abs(cp) - if err != nil { - return err - } - return dlc.run() + return client.List(chartpath, out) }, } return cmd } - -func (l *dependencyListCmd) run() error { - c, err := chartutil.Load(l.chartpath) - if err != nil { - return err - } - - r, err := chartutil.LoadRequirements(c) - if err != nil { - if err == chartutil.ErrRequirementsNotFound { - fmt.Fprintf(l.out, "WARNING: no requirements at %s/charts\n", l.chartpath) - return nil - } - return err - } - - l.printRequirements(r, l.out) - fmt.Fprintln(l.out) - l.printMissing(r) - return nil -} - -func (l *dependencyListCmd) dependencyStatus(dep *chartutil.Dependency) string { - filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") - archives, err := filepath.Glob(filepath.Join(l.chartpath, "charts", filename)) - if err != nil { - return "bad pattern" - } else if len(archives) > 1 { - return "too many matches" - } else if len(archives) == 1 { - archive := archives[0] - if _, err := os.Stat(archive); err == nil { - c, err := chartutil.Load(archive) - if err != nil { - return "corrupt" - } - if c.Metadata.Name != dep.Name { - return "misnamed" - } - - if c.Metadata.Version != dep.Version { - constraint, err := semver.NewConstraint(dep.Version) - if err != nil { - return "invalid version" - } - - v, err := semver.NewVersion(c.Metadata.Version) - if err != nil { - return "invalid version" - } - - if constraint.Check(v) { - return "ok" - } - return "wrong version" - } - return "ok" - } - } - - folder := filepath.Join(l.chartpath, "charts", dep.Name) - if fi, err := os.Stat(folder); err != nil { - return "missing" - } else if !fi.IsDir() { - return "mispackaged" - } - - c, err := chartutil.Load(folder) - if err != nil { - return "corrupt" - } - - if c.Metadata.Name != dep.Name { - return "misnamed" - } - - if c.Metadata.Version != dep.Version { - constraint, err := semver.NewConstraint(dep.Version) - if err != nil { - return "invalid version" - } - - v, err := semver.NewVersion(c.Metadata.Version) - if err != nil { - return "invalid version" - } - - if constraint.Check(v) { - return "unpacked" - } - return "wrong version" - } - - return "unpacked" -} - -// printRequirements prints all of the requirements in the yaml file. -func (l *dependencyListCmd) printRequirements(reqs *chartutil.Requirements, out io.Writer) { - table := uitable.New() - table.MaxColWidth = 80 - table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") - for _, row := range reqs.Dependencies { - table.AddRow(row.Name, row.Version, row.Repository, l.dependencyStatus(row)) - } - fmt.Fprintln(out, table) -} - -// printMissing prints warnings about charts that are present on disk, but are not in the requirements. -func (l *dependencyListCmd) printMissing(reqs *chartutil.Requirements) { - folder := filepath.Join(l.chartpath, "charts/*") - files, err := filepath.Glob(folder) - if err != nil { - fmt.Fprintln(l.out, err) - return - } - - for _, f := range files { - fi, err := os.Stat(f) - if err != nil { - fmt.Fprintf(l.out, "Warning: %s\n", err) - } - // Skip anything that is not a directory and not a tgz file. - if !fi.IsDir() && filepath.Ext(f) != ".tgz" { - continue - } - c, err := chartutil.Load(f) - if err != nil { - fmt.Fprintf(l.out, "WARNING: %q is not a chart.\n", f) - continue - } - found := false - for _, d := range reqs.Dependencies { - if d.Name == c.Metadata.Name { - found = true - break - } - } - if !found { - fmt.Fprintf(l.out, "WARNING: %q is not in requirements.yaml.\n", f) - } - } - -} diff --git a/cmd/helm/dependency_build.go b/cmd/helm/dependency_build.go index ec5fd14b75d..1ee46d3d2d2 100644 --- a/cmd/helm/dependency_build.go +++ b/cmd/helm/dependency_build.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,17 +16,22 @@ limitations under the License. package main import ( + "fmt" "io" + "os" + "path/filepath" "github.com/spf13/cobra" + "k8s.io/client-go/util/homedir" - "k8s.io/helm/pkg/downloader" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" ) const dependencyBuildDesc = ` -Build out the charts/ directory from the requirements.lock file. +Build out the charts/ directory from the Chart.lock file. Build is used to reconstruct a chart's dependencies to the state specified in the lock file. This will not re-negotiate dependencies, as 'helm dependency update' @@ -36,50 +41,53 @@ If no lock file is found, 'helm dependency build' will mirror the behavior of 'helm dependency update'. ` -type dependencyBuildCmd struct { - out io.Writer - chartpath string - verify bool - keyring string - helmhome helmpath.Home -} - -func newDependencyBuildCmd(out io.Writer) *cobra.Command { - dbc := &dependencyBuildCmd{out: out} +func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewDependency() cmd := &cobra.Command{ - Use: "build [flags] CHART", - Short: "rebuild the charts/ directory based on the requirements.lock file", + Use: "build CHART", + Short: "rebuild the charts/ directory based on the Chart.lock file", Long: dependencyBuildDesc, + Args: require.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - dbc.helmhome = settings.Home - dbc.chartpath = "." - + chartpath := "." if len(args) > 0 { - dbc.chartpath = args[0] + chartpath = filepath.Clean(args[0]) + } + man := &downloader.Manager{ + Out: out, + ChartPath: chartpath, + Keyring: client.Keyring, + SkipUpdate: client.SkipRefresh, + Getters: getter.All(settings), + RegistryClient: cfg.RegistryClient, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + Debug: settings.Debug, } - return dbc.run() + if client.Verify { + man.Verify = downloader.VerifyIfPossible + } + err := man.Build() + if e, ok := err.(downloader.ErrRepoNotFound); ok { + return fmt.Errorf("%s. Please add the missing repos via 'helm repo add'", e.Error()) + } + return err }, } f := cmd.Flags() - f.BoolVar(&dbc.verify, "verify", false, "verify the packages against signatures") - f.StringVar(&dbc.keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") return cmd } -func (d *dependencyBuildCmd) run() error { - man := &downloader.Manager{ - Out: d.out, - ChartPath: d.chartpath, - HelmHome: d.helmhome, - Keyring: d.keyring, - Getters: getter.All(settings), +// defaultKeyring returns the expanded path to the default keyring. +func defaultKeyring() string { + if v, ok := os.LookupEnv("GNUPGHOME"); ok { + return filepath.Join(v, "pubring.gpg") } - if d.verify { - man.Verify = downloader.VerifyIfPossible - } - - return man.Build() + return filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg") } diff --git a/cmd/helm/dependency_build_test.go b/cmd/helm/dependency_build_test.go index 2d7c88654c1..33198a9dd04 100644 --- a/cmd/helm/dependency_build_test.go +++ b/cmd/helm/dependency_build_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,69 +16,75 @@ limitations under the License. package main import ( - "bytes" + "fmt" "os" "path/filepath" "strings" "testing" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/provenance" - "k8s.io/helm/pkg/repo" - "k8s.io/helm/pkg/repo/repotest" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/provenance" + "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v3/pkg/repo/repotest" ) func TestDependencyBuildCmd(t *testing.T) { - hh, err := tempHelmHome(t) + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") + defer srv.Stop() if err != nil { t.Fatal(err) } - cleanup := resetEnv() - defer func() { - os.RemoveAll(hh.String()) - cleanup() - }() - settings.Home = hh + rootDir := srv.Root() + srv.LinkIndices() - srv := repotest.NewServer(hh.String()) - defer srv.Stop() - _, err = srv.CopyCharts("testdata/testcharts/*.tgz") + ociSrv, err := repotest.NewOCIServer(t, srv.Root()) if err != nil { t.Fatal(err) } - chartname := "depbuild" - if err := createTestingChart(hh.String(), chartname, srv.URL()); err != nil { + ociChartName := "oci-depending-chart" + c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL) + if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil { t.Fatal(err) } + ociSrv.Run(t, repotest.WithDependingChart(c)) + + err = os.Setenv("HELM_EXPERIMENTAL_OCI", "1") + if err != nil { + t.Fatal("failed to set environment variable enabling OCI support") + } + + dir := func(p ...string) string { + return filepath.Join(append([]string{srv.Root()}, p...)...) + } - out := bytes.NewBuffer(nil) - dbc := &dependencyBuildCmd{out: out} - dbc.helmhome = helmpath.Home(hh) - dbc.chartpath = filepath.Join(hh.String(), chartname) + chartname := "depbuild" + createTestingChart(t, rootDir, chartname, srv.URL()) + repoFile := filepath.Join(rootDir, "repositories.yaml") + + cmd := fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s", filepath.Join(rootDir, chartname), repoFile, rootDir) + _, out, err := executeActionCommand(cmd) // In the first pass, we basically want the same results as an update. - if err := dbc.run(); err != nil { - output := out.String() - t.Logf("Output: %s", output) + if err != nil { + t.Logf("Output: %s", out) t.Fatal(err) } - output := out.String() - if !strings.Contains(output, `update from the "test" chart repository`) { - t.Errorf("Repo did not get updated\n%s", output) + if !strings.Contains(out, `update from the "test" chart repository`) { + t.Errorf("Repo did not get updated\n%s", out) } // Make sure the actual file got downloaded. - expect := filepath.Join(hh.String(), chartname, "charts/reqtest-0.1.0.tgz") + expect := filepath.Join(rootDir, chartname, "charts/reqtest-0.1.0.tgz") if _, err := os.Stat(expect); err != nil { t.Fatal(err) } // In the second pass, we want to remove the chart's request dependency, // then see if it restores from the lock. - lockfile := filepath.Join(hh.String(), chartname, "requirements.lock") + lockfile := filepath.Join(rootDir, chartname, "Chart.lock") if _, err := os.Stat(lockfile); err != nil { t.Fatal(err) } @@ -86,14 +92,13 @@ func TestDependencyBuildCmd(t *testing.T) { t.Fatal(err) } - if err := dbc.run(); err != nil { - output := out.String() - t.Logf("Output: %s", output) + _, out, err = executeActionCommand(cmd) + if err != nil { + t.Logf("Output: %s", out) t.Fatal(err) } // Now repeat the test that the dependency exists. - expect = filepath.Join(hh.String(), chartname, "charts/reqtest-0.1.0.tgz") if _, err := os.Stat(expect); err != nil { t.Fatal(err) } @@ -104,7 +109,7 @@ func TestDependencyBuildCmd(t *testing.T) { t.Fatal(err) } - i, err := repo.LoadIndexFile(dbc.helmhome.CacheIndex("test")) + i, err := repo.LoadIndexFile(filepath.Join(rootDir, "index.yaml")) if err != nil { t.Fatal(err) } @@ -117,4 +122,45 @@ func TestDependencyBuildCmd(t *testing.T) { t.Errorf("mismatched versions. Expected %q, got %q", "0.1.0", v) } + skipRefreshCmd := fmt.Sprintf("dependency build '%s' --skip-refresh --repository-config %s --repository-cache %s", filepath.Join(rootDir, chartname), repoFile, rootDir) + _, out, err = executeActionCommand(skipRefreshCmd) + + // In this pass, we check --skip-refresh option becomes effective. + if err != nil { + t.Logf("Output: %s", out) + t.Fatal(err) + } + + if strings.Contains(out, `update from the "test" chart repository`) { + t.Errorf("Repo did get updated\n%s", out) + } + + // OCI dependencies + cmd = fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json", + dir(ociChartName), + dir("repositories.yaml"), + dir(), + dir()) + _, out, err = executeActionCommand(cmd) + if err != nil { + t.Logf("Output: %s", out) + t.Fatal(err) + } + expect = dir(ociChartName, "charts/oci-dependent-chart-0.1.0.tgz") + if _, err := os.Stat(expect); err != nil { + t.Fatal(err) + } +} + +func TestDependencyBuildCmdWithHelmV2Hash(t *testing.T) { + chartName := "testdata/testcharts/issue-7233" + + cmd := fmt.Sprintf("dependency build '%s'", chartName) + _, out, err := executeActionCommand(cmd) + + // Want to make sure the build can verify Helm v2 hash + if err != nil { + t.Logf("Output: %s", out) + t.Fatal(err) + } } diff --git a/cmd/helm/dependency_test.go b/cmd/helm/dependency_test.go index cc451914711..34c6a25e1de 100644 --- a/cmd/helm/dependency_test.go +++ b/cmd/helm/dependency_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,43 +16,42 @@ limitations under the License. package main import ( - "io" + "runtime" "testing" - - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" ) func TestDependencyListCmd(t *testing.T) { + noSuchChart := cmdTestCase{ + name: "No such chart", + cmd: "dependency list /no/such/chart", + golden: "output/dependency-list-no-chart-linux.txt", + wantError: true, + } - tests := []releaseCase{ - { - name: "No such chart", - args: []string{"/no/such/chart"}, - err: true, - }, - { - name: "No requirements.yaml", - args: []string{"testdata/testcharts/alpine"}, - expected: "WARNING: no requirements at ", - }, - { - name: "Requirements in chart dir", - args: []string{"testdata/testcharts/reqtest"}, - expected: "NAME \tVERSION\tREPOSITORY \tSTATUS \n" + - "reqsubchart \t0.1.0 \thttps://example.com/charts\tunpacked\n" + - "reqsubchart2\t0.2.0 \thttps://example.com/charts\tunpacked\n" + - "reqsubchart3\t>=0.1.0\thttps://example.com/charts\tok \n\n", - }, - { - name: "Requirements in chart archive", - args: []string{"testdata/testcharts/reqtest-0.1.0.tgz"}, - expected: "NAME \tVERSION\tREPOSITORY \tSTATUS \nreqsubchart \t0.1.0 \thttps://example.com/charts\tmissing\nreqsubchart2\t0.2.0 \thttps://example.com/charts\tmissing\n", - }, + noDependencies := cmdTestCase{ + name: "No dependencies", + cmd: "dependency list testdata/testcharts/alpine", + golden: "output/dependency-list-no-requirements-linux.txt", } - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newDependencyListCmd(out) - }) + if runtime.GOOS == "windows" { + noSuchChart.golden = "output/dependency-list-no-chart-windows.txt" + noDependencies.golden = "output/dependency-list-no-requirements-windows.txt" + } + + tests := []cmdTestCase{noSuchChart, + noDependencies, { + name: "Dependencies in chart dir", + cmd: "dependency list testdata/testcharts/reqtest", + golden: "output/dependency-list.txt", + }, { + name: "Dependencies in chart archive", + cmd: "dependency list testdata/testcharts/reqtest-0.1.0.tgz", + golden: "output/dependency-list-archive.txt", + }} + runTestCmd(t, tests) +} + +func TestDependencyFileCompletion(t *testing.T) { + checkFileCompletion(t, "dependency", false) } diff --git a/cmd/helm/dependency_update.go b/cmd/helm/dependency_update.go index d6a9986398b..ad0188f1778 100644 --- a/cmd/helm/dependency_update.go +++ b/cmd/helm/dependency_update.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -20,86 +20,65 @@ import ( "path/filepath" "github.com/spf13/cobra" - "k8s.io/helm/pkg/downloader" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" ) const dependencyUpDesc = ` -Update the on-disk dependencies to mirror the requirements.yaml file. +Update the on-disk dependencies to mirror Chart.yaml. -This command verifies that the required charts, as expressed in 'requirements.yaml', +This command verifies that the required charts, as expressed in 'Chart.yaml', are present in 'charts/' and are at an acceptable version. It will pull down the latest charts that satisfy the dependencies, and clean up old dependencies. On successful update, this will generate a lock file that can be used to -rebuild the requirements to an exact version. +rebuild the dependencies to an exact version. -Dependencies are not required to be represented in 'requirements.yaml'. For that +Dependencies are not required to be represented in 'Chart.yaml'. For that reason, an update command will not remove charts unless they are (a) present -in the requirements.yaml file, but (b) at the wrong version. +in the Chart.yaml file, but (b) at the wrong version. ` -// dependencyUpdateCmd describes a 'helm dependency update' -type dependencyUpdateCmd struct { - out io.Writer - chartpath string - helmhome helmpath.Home - verify bool - keyring string - skipRefresh bool -} - // newDependencyUpdateCmd creates a new dependency update command. -func newDependencyUpdateCmd(out io.Writer) *cobra.Command { - duc := &dependencyUpdateCmd{out: out} +func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewDependency() cmd := &cobra.Command{ - Use: "update [flags] CHART", + Use: "update CHART", Aliases: []string{"up"}, - Short: "update charts/ based on the contents of requirements.yaml", + Short: "update charts/ based on the contents of Chart.yaml", Long: dependencyUpDesc, + Args: require.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cp := "." + chartpath := "." if len(args) > 0 { - cp = args[0] + chartpath = filepath.Clean(args[0]) } - - var err error - duc.chartpath, err = filepath.Abs(cp) - if err != nil { - return err + man := &downloader.Manager{ + Out: out, + ChartPath: chartpath, + Keyring: client.Keyring, + SkipUpdate: client.SkipRefresh, + Getters: getter.All(settings), + RegistryClient: cfg.RegistryClient, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + Debug: settings.Debug, } - - duc.helmhome = settings.Home - - return duc.run() + if client.Verify { + man.Verify = downloader.VerifyAlways + } + return man.Update() }, } f := cmd.Flags() - f.BoolVar(&duc.verify, "verify", false, "verify the packages against signatures") - f.StringVar(&duc.keyring, "keyring", defaultKeyring(), "keyring containing public keys") - f.BoolVar(&duc.skipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") return cmd } - -// run runs the full dependency update process. -func (d *dependencyUpdateCmd) run() error { - man := &downloader.Manager{ - Out: d.out, - ChartPath: d.chartpath, - HelmHome: d.helmhome, - Keyring: d.keyring, - SkipUpdate: d.skipRefresh, - Getters: getter.All(settings), - } - if d.verify { - man.Verify = downloader.VerifyAlways - } - if settings.Debug { - man.Debug = true - } - return man.Update() -} diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go index e29cb35de7f..9f7b0f3033b 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,69 +16,77 @@ limitations under the License. package main import ( - "bytes" + "fmt" "io/ioutil" "os" "path/filepath" "strings" "testing" - "github.com/ghodss/yaml" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/provenance" - "k8s.io/helm/pkg/repo" - "k8s.io/helm/pkg/repo/repotest" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/provenance" + "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v3/pkg/repo/repotest" ) func TestDependencyUpdateCmd(t *testing.T) { - hh, err := tempHelmHome(t) + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") if err != nil { t.Fatal(err) } - cleanup := resetEnv() - defer func() { - os.RemoveAll(hh.String()) - cleanup() - }() + defer srv.Stop() + t.Logf("Listening on directory %s", srv.Root()) - settings.Home = hh + ociSrv, err := repotest.NewOCIServer(t, srv.Root()) + if err != nil { + t.Fatal(err) + } - srv := repotest.NewServer(hh.String()) - defer srv.Stop() - copied, err := srv.CopyCharts("testdata/testcharts/*.tgz") + ociChartName := "oci-depending-chart" + c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL) + if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil { + t.Fatal(err) + } + ociSrv.Run(t, repotest.WithDependingChart(c)) + + err = os.Setenv("HELM_EXPERIMENTAL_OCI", "1") if err != nil { + t.Fatal("failed to set environment variable enabling OCI support") + } + + if err := srv.LinkIndices(); err != nil { t.Fatal(err) } - t.Logf("Copied charts:\n%s", strings.Join(copied, "\n")) - t.Logf("Listening on directory %s", srv.Root()) + + dir := func(p ...string) string { + return filepath.Join(append([]string{srv.Root()}, p...)...) + } chartname := "depup" - if err := createTestingChart(hh.String(), chartname, srv.URL()); err != nil { + ch := createTestingMetadata(chartname, srv.URL()) + md := ch.Metadata + if err := chartutil.SaveDir(ch, dir()); err != nil { t.Fatal(err) } - out := bytes.NewBuffer(nil) - duc := &dependencyUpdateCmd{out: out} - duc.helmhome = helmpath.Home(hh) - duc.chartpath = filepath.Join(hh.String(), chartname) - - if err := duc.run(); err != nil { - output := out.String() - t.Logf("Output: %s", output) + _, out, err := executeActionCommand( + fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir()), + ) + if err != nil { + t.Logf("Output: %s", out) t.Fatal(err) } - output := out.String() // This is written directly to stdout, so we have to capture as is. - if !strings.Contains(output, `update from the "test" chart repository`) { - t.Errorf("Repo did not get updated\n%s", output) + if !strings.Contains(out, `update from the "test" chart repository`) { + t.Errorf("Repo did not get updated\n%s", out) } // Make sure the actual file got downloaded. - expect := filepath.Join(hh.String(), chartname, "charts/reqtest-0.1.0.tgz") + expect := dir(chartname, "charts/reqtest-0.1.0.tgz") if _, err := os.Stat(expect); err != nil { t.Fatal(err) } @@ -88,7 +96,7 @@ func TestDependencyUpdateCmd(t *testing.T) { t.Fatal(err) } - i, err := repo.LoadIndexFile(duc.helmhome.CacheIndex("test")) + i, err := repo.LoadIndexFile(dir(helmpath.CacheIndexFile("test"))) if err != nil { t.Fatal(err) } @@ -100,112 +108,72 @@ func TestDependencyUpdateCmd(t *testing.T) { // Now change the dependencies and update. This verifies that on update, // old dependencies are cleansed and new dependencies are added. - reqfile := &chartutil.Requirements{ - Dependencies: []*chartutil.Dependency{ - {Name: "reqtest", Version: "0.1.0", Repository: srv.URL()}, - {Name: "compressedchart", Version: "0.3.0", Repository: srv.URL()}, - }, + md.Dependencies = []*chart.Dependency{ + {Name: "reqtest", Version: "0.1.0", Repository: srv.URL()}, + {Name: "compressedchart", Version: "0.3.0", Repository: srv.URL()}, } - dir := filepath.Join(hh.String(), chartname) - if err := writeRequirements(dir, reqfile); err != nil { + if err := chartutil.SaveChartfile(dir(chartname, "Chart.yaml"), md); err != nil { t.Fatal(err) } - if err := duc.run(); err != nil { - output := out.String() - t.Logf("Output: %s", output) + + _, out, err = executeActionCommand(fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) + if err != nil { + t.Logf("Output: %s", out) t.Fatal(err) } // In this second run, we should see compressedchart-0.3.0.tgz, and not // the 0.1.0 version. - expect = filepath.Join(hh.String(), chartname, "charts/compressedchart-0.3.0.tgz") + expect = dir(chartname, "charts/compressedchart-0.3.0.tgz") if _, err := os.Stat(expect); err != nil { t.Fatalf("Expected %q: %s", expect, err) } - dontExpect := filepath.Join(hh.String(), chartname, "charts/compressedchart-0.1.0.tgz") - if _, err := os.Stat(dontExpect); err == nil { - t.Fatalf("Unexpected %q", dontExpect) + unexpected := dir(chartname, "charts/compressedchart-0.1.0.tgz") + if _, err := os.Stat(unexpected); err == nil { + t.Fatalf("Unexpected %q", unexpected) } -} -func TestDependencyUpdateCmd_SkipRefresh(t *testing.T) { - hh, err := tempHelmHome(t) + // test for OCI charts + cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json", + dir(ociChartName), + dir("repositories.yaml"), + dir(), + dir()) + _, out, err = executeActionCommand(cmd) if err != nil { + t.Logf("Output: %s", out) t.Fatal(err) } - cleanup := resetEnv() - defer func() { - os.RemoveAll(hh.String()) - cleanup() - }() - - settings.Home = hh - - srv := repotest.NewServer(hh.String()) - defer srv.Stop() - copied, err := srv.CopyCharts("testdata/testcharts/*.tgz") - if err != nil { - t.Fatal(err) - } - t.Logf("Copied charts:\n%s", strings.Join(copied, "\n")) - t.Logf("Listening on directory %s", srv.Root()) - - chartname := "depup" - if err := createTestingChart(hh.String(), chartname, srv.URL()); err != nil { + expect = dir(ociChartName, "charts/oci-dependent-chart-0.1.0.tgz") + if _, err := os.Stat(expect); err != nil { t.Fatal(err) } - - out := bytes.NewBuffer(nil) - duc := &dependencyUpdateCmd{out: out} - duc.helmhome = helmpath.Home(hh) - duc.chartpath = filepath.Join(hh.String(), chartname) - duc.skipRefresh = true - - if err := duc.run(); err == nil { - t.Fatal("Expected failure to find the repo with skipRefresh") - } - - output := out.String() - // This is written directly to stdout, so we have to capture as is. - if strings.Contains(output, `update from the "test" chart repository`) { - t.Errorf("Repo was unexpectedly updated\n%s", output) - } } -func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) { - hh, err := tempHelmHome(t) - if err != nil { - t.Fatal(err) - } - cleanup := resetEnv() - defer func() { - os.RemoveAll(hh.String()) - cleanup() - }() - - settings.Home = hh +func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { + defer resetEnv()() + defer ensure.HelmHome(t)() - srv := repotest.NewServer(hh.String()) - defer srv.Stop() - copied, err := srv.CopyCharts("testdata/testcharts/*.tgz") + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") if err != nil { t.Fatal(err) } - t.Logf("Copied charts:\n%s", strings.Join(copied, "\n")) + defer srv.Stop() t.Logf("Listening on directory %s", srv.Root()) - chartname := "depupdelete" - if err := createTestingChart(hh.String(), chartname, srv.URL()); err != nil { + if err := srv.LinkIndices(); err != nil { t.Fatal(err) } - out := bytes.NewBuffer(nil) - duc := &dependencyUpdateCmd{out: out} - duc.helmhome = helmpath.Home(hh) - duc.chartpath = filepath.Join(hh.String(), chartname) + chartname := "depupdelete" - if err := duc.run(); err != nil { - output := out.String() + dir := func(p ...string) string { + return filepath.Join(append([]string{srv.Root()}, p...)...) + } + createTestingChart(t, dir(), chartname, srv.URL()) + + _, output, err := executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) + if err != nil { t.Logf("Output: %s", output) t.Fatal(err) } @@ -213,14 +181,14 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) { // Chart repo is down srv.Stop() - if err := duc.run(); err == nil { - output := out.String() + _, output, err = executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) + if err == nil { t.Logf("Output: %s", output) t.Fatal("Expected error, got nil") } // Make sure charts dir still has dependencies - files, err := ioutil.ReadDir(filepath.Join(duc.chartpath, "charts")) + files, err := ioutil.ReadDir(filepath.Join(dir(chartname), "charts")) if err != nil { t.Fatal(err) } @@ -236,38 +204,48 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) { } // Make sure tmpcharts is deleted - if _, err := os.Stat(filepath.Join(duc.chartpath, "tmpcharts")); !os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(dir(chartname), "tmpcharts")); !os.IsNotExist(err) { t.Fatalf("tmpcharts dir still exists") } } -// createTestingChart creates a basic chart that depends on reqtest-0.1.0 +// createTestingMetadata creates a basic chart that depends on reqtest-0.1.0 // // The baseURL can be used to point to a particular repository server. -func createTestingChart(dest, name, baseURL string) error { - cfile := &chart.Metadata{ - Name: name, - Version: "1.2.3", - } - dir := filepath.Join(dest, name) - _, err := chartutil.Create(cfile, dest) - if err != nil { - return err - } - req := &chartutil.Requirements{ - Dependencies: []*chartutil.Dependency{ - {Name: "reqtest", Version: "0.1.0", Repository: baseURL}, - {Name: "compressedchart", Version: "0.1.0", Repository: baseURL}, +func createTestingMetadata(name, baseURL string) *chart.Chart { + return &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV2, + Name: name, + Version: "1.2.3", + Dependencies: []*chart.Dependency{ + {Name: "reqtest", Version: "0.1.0", Repository: baseURL}, + {Name: "compressedchart", Version: "0.1.0", Repository: baseURL}, + }, }, } - return writeRequirements(dir, req) } -func writeRequirements(dir string, req *chartutil.Requirements) error { - data, err := yaml.Marshal(req) - if err != nil { - return err +func createTestingMetadataForOCI(name, registryURL string) *chart.Chart { + return &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV2, + Name: name, + Version: "1.2.3", + Dependencies: []*chart.Dependency{ + {Name: "oci-dependent-chart", Version: "0.1.0", Repository: fmt.Sprintf("oci://%s/u/ocitestuser", registryURL)}, + }, + }, } +} - return ioutil.WriteFile(filepath.Join(dir, "requirements.yaml"), data, 0655) +// createTestingChart creates a basic chart that depends on reqtest-0.1.0 +// +// The baseURL can be used to point to a particular repository server. +func createTestingChart(t *testing.T, dest, name, baseURL string) { + t.Helper() + cfile := createTestingMetadata(name, baseURL) + if err := chartutil.SaveDir(cfile, dest); err != nil { + t.Fatal(err) + } } diff --git a/cmd/helm/docs.go b/cmd/helm/docs.go index e5b9f752107..1a28a47ec42 100644 --- a/cmd/helm/docs.go +++ b/cmd/helm/docs.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -18,10 +18,15 @@ package main import ( "fmt" "io" + "path" "path/filepath" + "strings" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" + + "helm.sh/helm/v3/cmd/helm/require" ) const docsDesc = ` @@ -33,48 +38,72 @@ This command can generate documentation for Helm in the following formats: - Man pages It can also generate bash autocompletions. - - $ helm docs markdown -dir mydocs/ ` -type docsCmd struct { - out io.Writer - dest string - docTypeString string - topCmd *cobra.Command +type docsOptions struct { + dest string + docTypeString string + topCmd *cobra.Command + generateHeaders bool } func newDocsCmd(out io.Writer) *cobra.Command { - dc := &docsCmd{out: out} + o := &docsOptions{} cmd := &cobra.Command{ - Use: "docs", - Short: "Generate documentation as markdown or man pages", - Long: docsDesc, - Hidden: true, + Use: "docs", + Short: "generate documentation as markdown or man pages", + Long: docsDesc, + Hidden: true, + Args: require.NoArgs, + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { - dc.topCmd = cmd.Root() - return dc.run() + o.topCmd = cmd.Root() + return o.run(out) }, } f := cmd.Flags() - f.StringVar(&dc.dest, "dir", "./", "directory to which documentation is written") - f.StringVar(&dc.docTypeString, "type", "markdown", "the type of documentation to generate (markdown, man, bash)") + f.StringVar(&o.dest, "dir", "./", "directory to which documentation is written") + f.StringVar(&o.docTypeString, "type", "markdown", "the type of documentation to generate (markdown, man, bash)") + f.BoolVar(&o.generateHeaders, "generate-headers", false, "generate standard headers for markdown files") + + cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + types := []string{"bash", "man", "markdown"} + var comps []string + for _, t := range types { + if strings.HasPrefix(t, toComplete) { + comps = append(comps, t) + } + } + return comps, cobra.ShellCompDirectiveNoFileComp + }) return cmd } -func (d *docsCmd) run() error { - switch d.docTypeString { +func (o *docsOptions) run(out io.Writer) error { + switch o.docTypeString { case "markdown", "mdown", "md": - return doc.GenMarkdownTree(d.topCmd, d.dest) + if o.generateHeaders { + standardLinks := func(s string) string { return s } + + hdrFunc := func(filename string) string { + base := filepath.Base(filename) + name := strings.TrimSuffix(base, path.Ext(base)) + title := strings.Title(strings.Replace(name, "_", " ", -1)) + return fmt.Sprintf("---\ntitle: \"%s\"\n---\n\n", title) + } + + return doc.GenMarkdownTreeCustom(o.topCmd, o.dest, hdrFunc, standardLinks) + } + return doc.GenMarkdownTree(o.topCmd, o.dest) case "man": manHdr := &doc.GenManHeader{Title: "HELM", Section: "1"} - return doc.GenManTree(d.topCmd, manHdr, d.dest) + return doc.GenManTree(o.topCmd, manHdr, o.dest) case "bash": - return d.topCmd.GenBashCompletionFile(filepath.Join(d.dest, "completions.bash")) + return o.topCmd.GenBashCompletionFile(filepath.Join(o.dest, "completions.bash")) default: - return fmt.Errorf("unknown doc type %q. Try 'markdown' or 'man'", d.docTypeString) + return errors.Errorf("unknown doc type %q. Try 'markdown' or 'man'", o.docTypeString) } } diff --git a/cmd/helm/docs_test.go b/cmd/helm/docs_test.go new file mode 100644 index 00000000000..f0082578a76 --- /dev/null +++ b/cmd/helm/docs_test.go @@ -0,0 +1,38 @@ +/* +Copyright The Helm Authors. + +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" +) + +func TestDocsTypeFlagCompletion(t *testing.T) { + tests := []cmdTestCase{{ + name: "completion for docs --type", + cmd: "__complete docs --type ''", + golden: "output/docs-type-comp.txt", + }, { + name: "completion for docs --type", + cmd: "__complete docs --type mar", + golden: "output/docs-type-filtered-comp.txt", + }} + runTestCmd(t, tests) +} + +func TestDocsFileCompletion(t *testing.T) { + checkFileCompletion(t, "docs", false) +} diff --git a/cmd/helm/env.go b/cmd/helm/env.go new file mode 100644 index 00000000000..3754b748d89 --- /dev/null +++ b/cmd/helm/env.go @@ -0,0 +1,76 @@ +/* +Copyright The Helm Authors. + +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" + "sort" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" +) + +var envHelp = ` +Env prints out all the environment information in use by Helm. +` + +func newEnvCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "env", + Short: "helm client environment information", + Long: envHelp, + Args: require.MaximumNArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + keys := getSortedEnvVarKeys() + return keys, cobra.ShellCompDirectiveNoFileComp + } + + return nil, cobra.ShellCompDirectiveNoFileComp + }, + Run: func(cmd *cobra.Command, args []string) { + envVars := settings.EnvVars() + + if len(args) == 0 { + // Sort the variables by alphabetical order. + // This allows for a constant output across calls to 'helm env'. + keys := getSortedEnvVarKeys() + + for _, k := range keys { + fmt.Fprintf(out, "%s=\"%s\"\n", k, envVars[k]) + } + } else { + fmt.Fprintf(out, "%s\n", envVars[args[0]]) + } + }, + } + return cmd +} + +func getSortedEnvVarKeys() []string { + envVars := settings.EnvVars() + + var keys []string + for k := range envVars { + keys = append(keys, k) + } + sort.Strings(keys) + + return keys +} diff --git a/cmd/helm/env_test.go b/cmd/helm/env_test.go new file mode 100644 index 00000000000..01ef2593309 --- /dev/null +++ b/cmd/helm/env_test.go @@ -0,0 +1,35 @@ +/* +Copyright The Helm Authors. + +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" +) + +func TestEnv(t *testing.T) { + tests := []cmdTestCase{{ + name: "completion for env", + cmd: "__complete env ''", + golden: "output/env-comp.txt", + }} + runTestCmd(t, tests) +} + +func TestEnvFileCompletion(t *testing.T) { + checkFileCompletion(t, "env", false) + checkFileCompletion(t, "env HELM_BIN", false) +} diff --git a/cmd/helm/fetch.go b/cmd/helm/fetch.go deleted file mode 100644 index 069f57effe6..00000000000 --- a/cmd/helm/fetch.go +++ /dev/null @@ -1,186 +0,0 @@ -/* -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" - "io/ioutil" - "os" - "path/filepath" - - "github.com/spf13/cobra" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/downloader" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/repo" -) - -const fetchDesc = ` -Retrieve a package from a package repository, and download it locally. - -This is useful for fetching packages to inspect, modify, or repackage. It can -also be used to perform cryptographic verification of a chart without installing -the chart. - -There are options for unpacking the chart after download. This will create a -directory for the chart and uncompress into that directory. - -If the --verify flag is specified, the requested chart MUST have a provenance -file, and MUST pass the verification process. Failure in any part of this will -result in an error, and the chart will not be saved locally. -` - -type fetchCmd struct { - untar bool - untardir string - chartRef string - destdir string - version string - repoURL string - username string - password string - - verify bool - verifyLater bool - keyring string - - certFile string - keyFile string - caFile string - - devel bool - - out io.Writer -} - -func newFetchCmd(out io.Writer) *cobra.Command { - fch := &fetchCmd{out: out} - - cmd := &cobra.Command{ - Use: "fetch [flags] [chart URL | repo/chartname] [...]", - Short: "download a chart from a repository and (optionally) unpack it in local directory", - Long: fetchDesc, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return fmt.Errorf("need at least one argument, url or repo/name of the chart") - } - - if fch.version == "" && fch.devel { - debug("setting version to >0.0.0-0") - fch.version = ">0.0.0-0" - } - - for i := 0; i < len(args); i++ { - fch.chartRef = args[i] - if err := fch.run(); err != nil { - return err - } - } - return nil - }, - } - - f := cmd.Flags() - f.BoolVar(&fch.untar, "untar", false, "if set to true, will untar the chart after downloading it") - f.StringVar(&fch.untardir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded") - f.BoolVar(&fch.verify, "verify", false, "verify the package against its signature") - f.BoolVar(&fch.verifyLater, "prov", false, "fetch the provenance file, but don't perform verification") - f.StringVar(&fch.version, "version", "", "specific version of a chart. Without this, the latest version is fetched") - f.StringVar(&fch.keyring, "keyring", defaultKeyring(), "keyring containing public keys") - f.StringVarP(&fch.destdir, "destination", "d", ".", "location to write the chart. If this and tardir are specified, tardir is appended to this") - f.StringVar(&fch.repoURL, "repo", "", "chart repository url where to locate the requested chart") - f.StringVar(&fch.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") - f.StringVar(&fch.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") - f.StringVar(&fch.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") - f.BoolVar(&fch.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") - f.StringVar(&fch.username, "username", "", "chart repository username") - f.StringVar(&fch.password, "password", "", "chart repository password") - - return cmd -} - -func (f *fetchCmd) run() error { - c := downloader.ChartDownloader{ - HelmHome: settings.Home, - Out: f.out, - Keyring: f.keyring, - Verify: downloader.VerifyNever, - Getters: getter.All(settings), - Username: f.username, - Password: f.password, - } - - if f.verify { - c.Verify = downloader.VerifyAlways - } else if f.verifyLater { - c.Verify = downloader.VerifyLater - } - - // If untar is set, we fetch to a tempdir, then untar and copy after - // verification. - dest := f.destdir - if f.untar { - var err error - dest, err = ioutil.TempDir("", "helm-") - if err != nil { - return fmt.Errorf("Failed to untar: %s", err) - } - defer os.RemoveAll(dest) - } - - if f.repoURL != "" { - chartURL, err := repo.FindChartInAuthRepoURL(f.repoURL, f.username, f.password, f.chartRef, f.version, f.certFile, f.keyFile, f.caFile, getter.All(settings)) - if err != nil { - return err - } - f.chartRef = chartURL - } - - saved, v, err := c.DownloadTo(f.chartRef, f.version, dest) - if err != nil { - return err - } - - if f.verify { - fmt.Fprintf(f.out, "Verification: %v\n", v) - } - - // After verification, untar the chart into the requested directory. - if f.untar { - ud := f.untardir - if !filepath.IsAbs(ud) { - ud = filepath.Join(f.destdir, ud) - } - if fi, err := os.Stat(ud); err != nil { - if err := os.MkdirAll(ud, 0755); err != nil { - return fmt.Errorf("Failed to untar (mkdir): %s", err) - } - - } else if !fi.IsDir() { - return fmt.Errorf("Failed to untar: %s is not a directory", ud) - } - - return chartutil.ExpandFile(ud, saved) - } - return nil -} - -// defaultKeyring returns the expanded path to the default keyring. -func defaultKeyring() string { - return os.ExpandEnv("$HOME/.gnupg/pubring.gpg") -} diff --git a/cmd/helm/fetch_test.go b/cmd/helm/fetch_test.go deleted file mode 100644 index 13247ee9972..00000000000 --- a/cmd/helm/fetch_test.go +++ /dev/null @@ -1,179 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "regexp" - "testing" - - "k8s.io/helm/pkg/repo/repotest" -) - -func TestFetchCmd(t *testing.T) { - hh, err := tempHelmHome(t) - if err != nil { - t.Fatal(err) - } - cleanup := resetEnv() - defer func() { - os.RemoveAll(hh.String()) - cleanup() - }() - srv := repotest.NewServer(hh.String()) - defer srv.Stop() - - settings.Home = hh - - // all flags will get "--home=TMDIR -d outdir" appended. - tests := []struct { - name string - chart string - flags []string - fail bool - failExpect string - expectFile string - expectDir bool - expectVerify bool - }{ - { - name: "Basic chart fetch", - chart: "test/signtest", - expectFile: "./signtest-0.1.0.tgz", - }, - { - name: "Chart fetch with version", - chart: "test/signtest", - flags: []string{"--version", "0.1.0"}, - expectFile: "./signtest-0.1.0.tgz", - }, - { - name: "Fail chart fetch with non-existent version", - chart: "test/signtest", - flags: []string{"--version", "99.1.0"}, - fail: true, - failExpect: "no such chart", - }, - { - name: "Fail fetching non-existent chart", - chart: "test/nosuchthing", - failExpect: "Failed to fetch", - fail: true, - }, - { - name: "Fetch and verify", - chart: "test/signtest", - flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, - expectFile: "./signtest-0.1.0.tgz", - expectVerify: true, - }, - { - name: "Fetch and fail verify", - chart: "test/reqtest", - flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub"}, - failExpect: "Failed to fetch provenance", - fail: true, - }, - { - name: "Fetch and untar", - chart: "test/signtest", - flags: []string{"--untar", "--untardir", "signtest"}, - expectFile: "./signtest", - expectDir: true, - }, - { - name: "Fetch, verify, untar", - chart: "test/signtest", - flags: []string{"--verify", "--keyring", "testdata/helm-test-key.pub", "--untar", "--untardir", "signtest"}, - expectFile: "./signtest", - expectDir: true, - expectVerify: true, - }, - { - name: "Chart fetch using repo URL", - chart: "signtest", - expectFile: "./signtest-0.1.0.tgz", - flags: []string{"--repo", srv.URL()}, - }, - { - name: "Fail fetching non-existent chart on repo URL", - chart: "someChart", - flags: []string{"--repo", srv.URL()}, - failExpect: "Failed to fetch chart", - fail: true, - }, - { - name: "Specific version chart fetch using repo URL", - chart: "signtest", - expectFile: "./signtest-0.1.0.tgz", - flags: []string{"--repo", srv.URL(), "--version", "0.1.0"}, - }, - { - name: "Specific version chart fetch using repo URL", - chart: "signtest", - flags: []string{"--repo", srv.URL(), "--version", "0.2.0"}, - failExpect: "Failed to fetch chart version", - fail: true, - }, - } - - if _, err := srv.CopyCharts("testdata/testcharts/*.tgz*"); err != nil { - t.Fatal(err) - } - if err := srv.LinkIndices(); err != nil { - t.Fatal(err) - } - - for _, tt := range tests { - outdir := filepath.Join(hh.String(), "testout") - os.RemoveAll(outdir) - os.Mkdir(outdir, 0755) - - buf := bytes.NewBuffer(nil) - cmd := newFetchCmd(buf) - tt.flags = append(tt.flags, "-d", outdir) - cmd.ParseFlags(tt.flags) - if err := cmd.RunE(cmd, []string{tt.chart}); err != nil { - if tt.fail { - continue - } - t.Errorf("%q reported error: %s", tt.name, err) - continue - } - if tt.expectVerify { - pointerAddressPattern := "0[xX][A-Fa-f0-9]+" - sha256Pattern := "[A-Fa-f0-9]{64}" - verificationRegex := regexp.MustCompile( - fmt.Sprintf("Verification: &{%s sha256:%s signtest-0.1.0.tgz}\n", pointerAddressPattern, sha256Pattern)) - if !verificationRegex.MatchString(buf.String()) { - t.Errorf("%q: expected match for regex %s, got %s", tt.name, verificationRegex, buf.String()) - } - } - - ef := filepath.Join(outdir, tt.expectFile) - fi, err := os.Stat(ef) - if err != nil { - t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err) - } - if fi.IsDir() != tt.expectDir { - t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir) - } - } -} diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go new file mode 100644 index 00000000000..fe653625dc5 --- /dev/null +++ b/cmd/helm/flags.go @@ -0,0 +1,198 @@ +/* +Copyright The Helm Authors. + +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 ( + "flag" + "fmt" + "log" + "path/filepath" + "sort" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "k8s.io/klog/v2" + + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/postrender" + "helm.sh/helm/v3/pkg/repo" +) + +const outputFlag = "output" +const postRenderFlag = "post-renderer" + +func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { + f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)") + f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") +} + +func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { + f.StringVar(&c.Version, "version", "", "specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used") + f.BoolVar(&c.Verify, "verify", false, "verify the package before using it") + f.StringVar(&c.Keyring, "keyring", defaultKeyring(), "location of public keys used for verification") + f.StringVar(&c.RepoURL, "repo", "", "chart repository url where to locate the requested chart") + f.StringVar(&c.Username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&c.Password, "password", "", "chart repository password where to locate the requested chart") + f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") + f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") +} + +// bindOutputFlag will add the output flag to the given command and bind the +// value to the given format pointer +func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) { + cmd.Flags().VarP(newOutputValue(output.Table, varRef), outputFlag, "o", + fmt.Sprintf("prints the output in the specified format. Allowed values: %s", strings.Join(output.Formats(), ", "))) + + err := cmd.RegisterFlagCompletionFunc(outputFlag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var formatNames []string + for format, desc := range output.FormatsWithDesc() { + if strings.HasPrefix(format, toComplete) { + formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc)) + } + } + + // Sort the results to get a deterministic order for the tests + sort.Strings(formatNames) + return formatNames, cobra.ShellCompDirectiveNoFileComp + }) + + if err != nil { + log.Fatal(err) + } +} + +type outputValue output.Format + +func newOutputValue(defaultValue output.Format, p *output.Format) *outputValue { + *p = defaultValue + return (*outputValue)(p) +} + +func (o *outputValue) String() string { + // It is much cleaner looking (and technically less allocations) to just + // convert to a string rather than type asserting to the underlying + // output.Format + return string(*o) +} + +func (o *outputValue) Type() string { + return "format" +} + +func (o *outputValue) Set(s string) error { + outfmt, err := output.ParseFormat(s) + if err != nil { + return err + } + *o = outputValue(outfmt) + return nil +} + +func bindPostRenderFlag(cmd *cobra.Command, varRef *postrender.PostRenderer) { + cmd.Flags().Var(&postRenderer{varRef}, postRenderFlag, "the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path") +} + +type postRenderer struct { + renderer *postrender.PostRenderer +} + +func (p postRenderer) String() string { + return "exec" +} + +func (p postRenderer) Type() string { + return "postrenderer" +} + +func (p postRenderer) Set(s string) error { + if s == "" { + return nil + } + pr, err := postrender.NewExec(s) + if err != nil { + return err + } + *p.renderer = pr + return nil +} + +func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellCompDirective) { + chartInfo := strings.Split(chartRef, "/") + if len(chartInfo) != 2 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + repoName := chartInfo[0] + chartName := chartInfo[1] + + path := filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName)) + + var versions []string + if indexFile, err := repo.LoadIndexFile(path); err == nil { + for _, details := range indexFile.Entries[chartName] { + version := details.Metadata.Version + if strings.HasPrefix(version, toComplete) { + appVersion := details.Metadata.AppVersion + appVersionDesc := "" + if appVersion != "" { + appVersionDesc = fmt.Sprintf("App: %s, ", appVersion) + } + created := details.Created.Format("January 2, 2006") + createdDesc := "" + if created != "" { + createdDesc = fmt.Sprintf("Created: %s ", created) + } + deprecated := "" + if details.Metadata.Deprecated { + deprecated = "(deprecated)" + } + versions = append(versions, fmt.Sprintf("%s\t%s%s%s", version, appVersionDesc, createdDesc, deprecated)) + } + } + } + + return versions, cobra.ShellCompDirectiveNoFileComp +} + +// addKlogFlags adds flags from k8s.io/klog +// marks the flags as hidden to avoid polluting the help text +func addKlogFlags(fs *pflag.FlagSet) { + local := flag.NewFlagSet("klog", flag.ExitOnError) + klog.InitFlags(local) + local.VisitAll(func(fl *flag.Flag) { + fl.Name = normalize(fl.Name) + if fs.Lookup(fl.Name) != nil { + return + } + newflag := pflag.PFlagFromGoFlag(fl) + newflag.Hidden = true + fs.AddFlag(newflag) + }) +} + +// normalize replaces underscores with hyphens +func normalize(s string) string { + return strings.ReplaceAll(s, "_", "-") +} diff --git a/cmd/helm/flags_test.go b/cmd/helm/flags_test.go new file mode 100644 index 00000000000..d5576fe9f3b --- /dev/null +++ b/cmd/helm/flags_test.go @@ -0,0 +1,88 @@ +/* +Copyright The Helm Authors. + +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" + "testing" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" +) + +func outputFlagCompletionTest(t *testing.T, cmdName string) { + releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { + info.LastDeployed = helmtime.Unix(1452902400, 0).UTC() + return []*release.Release{{ + Name: "athos", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "porthos", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "aramis", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "dartagnan", + Namespace: "gascony", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }} + } + + tests := []cmdTestCase{{ + name: "completion for output flag long and before arg", + cmd: fmt.Sprintf("__complete %s --output ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }, { + name: "completion for output flag long and after arg", + cmd: fmt.Sprintf("__complete %s aramis --output ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }, { + name: "completion for output flag short and before arg", + cmd: fmt.Sprintf("__complete %s -o ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }, { + name: "completion for output flag short and after arg", + cmd: fmt.Sprintf("__complete %s aramis -o ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }} + runTestCmd(t, tests) +} diff --git a/cmd/helm/get.go b/cmd/helm/get.go index a2eb1d137fd..7c4854b59e8 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,73 +17,37 @@ limitations under the License. package main import ( - "errors" "io" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" ) var getHelp = ` -This command shows the details of a named release. +This command consists of multiple subcommands which can be used to +get extended information about the release, including: -It can be used to get extended information about the release, including: - - - The values used to generate the release - - The chart used to generate the release - - The generated manifest file - -By default, this prints a human readable collection of information about the -chart, the supplied values, and the generated manifest file. +- The values used to generate the release +- The generated manifest file +- The notes provided by the chart of the release +- The hooks associated with the release ` -var errReleaseRequired = errors.New("release name is required") - -type getCmd struct { - release string - out io.Writer - client helm.Interface - version int32 -} - -func newGetCmd(client helm.Interface, out io.Writer) *cobra.Command { - get := &getCmd{ - out: out, - client: client, - } - +func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "get [flags] RELEASE_NAME", - Short: "download a named release", - Long: getHelp, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired - } - get.release = args[0] - if get.client == nil { - get.client = newClient() - } - return get.run() - }, + Use: "get", + Short: "download extended information of a named release", + Long: getHelp, + Args: require.NoArgs, } - cmd.Flags().Int32Var(&get.version, "revision", 0, "get the named release with revision") - - cmd.AddCommand(addFlagsTLS(newGetValuesCmd(nil, out))) - cmd.AddCommand(addFlagsTLS(newGetManifestCmd(nil, out))) - cmd.AddCommand(addFlagsTLS(newGetHooksCmd(nil, out))) + cmd.AddCommand(newGetAllCmd(cfg, out)) + cmd.AddCommand(newGetValuesCmd(cfg, out)) + cmd.AddCommand(newGetManifestCmd(cfg, out)) + cmd.AddCommand(newGetHooksCmd(cfg, out)) + cmd.AddCommand(newGetNotesCmd(cfg, out)) return cmd } - -// getCmd is the command that implements 'helm get' -func (g *getCmd) run() error { - res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) - if err != nil { - return prettyError(err) - } - return printRelease(g.out, res.Release) -} diff --git a/cmd/helm/get_all.go b/cmd/helm/get_all.go new file mode 100644 index 00000000000..bf367da7f7e --- /dev/null +++ b/cmd/helm/get_all.go @@ -0,0 +1,82 @@ +/* +Copyright The Helm Authors. + +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" + "log" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" +) + +var getAllHelp = ` +This command prints a human readable collection of information about the +notes, hooks, supplied values, and generated manifest file of the given release. +` + +func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + var template string + client := action.NewGet(cfg) + + cmd := &cobra.Command{ + Use: "all RELEASE_NAME", + Short: "download all information for a named release", + Long: getAllHelp, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compListReleases(toComplete, args, cfg) + }, + RunE: func(cmd *cobra.Command, args []string) error { + res, err := client.Run(args[0]) + if err != nil { + return err + } + if template != "" { + data := map[string]interface{}{ + "Release": res, + } + return tpl(template, data, out) + } + + return output.Table.Write(out, &statusPrinter{res, true, false}) + }, + } + + f := cmd.Flags() + f.IntVar(&client.Version, "revision", 0, "get the named release with revision") + err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 1 { + return compListRevisions(toComplete, cfg, args[0]) + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) + + if err != nil { + log.Fatal(err) + } + + f.StringVar(&template, "template", "", "go template for formatting the output, eg: {{.Release.Name}}") + + return cmd +} diff --git a/cmd/helm/get_all_test.go b/cmd/helm/get_all_test.go new file mode 100644 index 00000000000..948f0aa7130 --- /dev/null +++ b/cmd/helm/get_all_test.go @@ -0,0 +1,56 @@ +/* +Copyright The Helm Authors. + +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" + + "helm.sh/helm/v3/pkg/release" +) + +func TestGetCmd(t *testing.T) { + tests := []cmdTestCase{{ + name: "get all with a release", + cmd: "get all thomas-guide", + golden: "output/get-release.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }, { + name: "get all with a formatted release", + cmd: "get all elevated-turkey --template {{.Release.Chart.Metadata.Version}}", + golden: "output/get-release-template.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "elevated-turkey"})}, + }, { + name: "get all requires release name arg", + cmd: "get all", + golden: "output/get-all-no-args.txt", + wantError: true, + }} + runTestCmd(t, tests) +} + +func TestGetAllCompletion(t *testing.T) { + checkReleaseCompletion(t, "get all", false) +} + +func TestGetAllRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get all") +} + +func TestGetAllFileCompletion(t *testing.T) { + checkFileCompletion(t, "get all", false) + checkFileCompletion(t, "get all myrelease", false) +} diff --git a/cmd/helm/get_hooks.go b/cmd/helm/get_hooks.go index 1b6f2f8fef5..913e2c58aab 100644 --- a/cmd/helm/get_hooks.go +++ b/cmd/helm/get_hooks.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,10 +19,12 @@ package main import ( "fmt" "io" + "log" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" ) const getHooksHelp = ` @@ -31,45 +33,43 @@ 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 - version int32 -} +func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewGet(cfg) -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, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + Use: "hooks RELEASE_NAME", + Short: "download all hooks for a named release", + Long: getHooksHelp, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compListReleases(toComplete, args, cfg) + }, RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired + res, err := client.Run(args[0]) + if err != nil { + return err + } + for _, hook := range res.Hooks { + fmt.Fprintf(out, "---\n# Source: %s\n%s\n", hook.Path, hook.Manifest) } - ghc.release = args[0] - ghc.client = ensureHelmClient(ghc.client) - return ghc.run() + return nil }, } - cmd.Flags().Int32Var(&ghc.version, "revision", 0, "get the named release with revision") - return cmd -} -func (g *getHooksCmd) run() error { - res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) + cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision") + err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 1 { + return compListRevisions(toComplete, cfg, args[0]) + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) + if err != nil { - fmt.Fprintln(g.out, g.release) - return prettyError(err) + log.Fatal(err) } - for _, hook := range res.Release.Hooks { - fmt.Fprintf(g.out, "---\n# %s\n%s", hook.Name, hook.Manifest) - } - return nil + return cmd } diff --git a/cmd/helm/get_hooks_test.go b/cmd/helm/get_hooks_test.go index e578c2533ed..251d5c73188 100644 --- a/cmd/helm/get_hooks_test.go +++ b/cmd/helm/get_hooks_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,31 +17,35 @@ limitations under the License. package main import ( - "io" "testing" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" + "helm.sh/helm/v3/pkg/release" ) func TestGetHooks(t *testing.T) { - tests := []releaseCase{ - { - name: "get hooks with release", - args: []string{"aeneas"}, - expected: helm.MockHookTemplate, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"})}, - }, - { - name: "get hooks without args", - args: []string{}, - err: true, - }, - } - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newGetHooksCmd(c, out) - }) + tests := []cmdTestCase{{ + name: "get hooks with release", + cmd: "get hooks aeneas", + golden: "output/get-hooks.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, + }, { + name: "get hooks without args", + cmd: "get hooks", + golden: "output/get-hooks-no-args.txt", + wantError: true, + }} + runTestCmd(t, tests) +} + +func TestGetHooksCompletion(t *testing.T) { + checkReleaseCompletion(t, "get hooks", false) +} + +func TestGetHooksRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get hooks") +} + +func TestGetHooksFileCompletion(t *testing.T) { + checkFileCompletion(t, "get hooks", false) + checkFileCompletion(t, "get hooks myrelease", false) } diff --git a/cmd/helm/get_manifest.go b/cmd/helm/get_manifest.go index c01febfb45a..baeaf8d72a6 100644 --- a/cmd/helm/get_manifest.go +++ b/cmd/helm/get_manifest.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,10 +19,12 @@ package main import ( "fmt" "io" + "log" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" ) var getManifestHelp = ` @@ -33,43 +35,41 @@ 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 - version int32 -} +func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewGet(cfg) -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, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + Use: "manifest RELEASE_NAME", + Short: "download the manifest for a named release", + Long: getManifestHelp, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compListReleases(toComplete, args, cfg) + }, RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired + res, err := client.Run(args[0]) + if err != nil { + return err } - get.release = args[0] - get.client = ensureHelmClient(get.client) - return get.run() + fmt.Fprintln(out, res.Manifest) + return nil }, } - cmd.Flags().Int32Var(&get.version, "revision", 0, "get the named release with revision") - return cmd -} + cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision") + err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 1 { + return compListRevisions(toComplete, cfg, args[0]) + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) -// getManifest implements 'helm get manifest' -func (g *getManifestCmd) run() error { - res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) if err != nil { - return prettyError(err) + log.Fatal(err) } - fmt.Fprintln(g.out, res.Release.Manifest) - return nil + + return cmd } diff --git a/cmd/helm/get_manifest_test.go b/cmd/helm/get_manifest_test.go index 286b5628aa6..2f27476b62b 100644 --- a/cmd/helm/get_manifest_test.go +++ b/cmd/helm/get_manifest_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,31 +17,35 @@ limitations under the License. package main import ( - "io" "testing" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" + "helm.sh/helm/v3/pkg/release" ) func TestGetManifest(t *testing.T) { - tests := []releaseCase{ - { - name: "get manifest with release", - args: []string{"juno"}, - expected: helm.MockManifest, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "juno"}), - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "juno"})}, - }, - { - name: "get manifest without args", - args: []string{}, - err: true, - }, - } - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newGetManifestCmd(c, out) - }) + tests := []cmdTestCase{{ + name: "get manifest with release", + cmd: "get manifest juno", + golden: "output/get-manifest.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "juno"})}, + }, { + name: "get manifest without args", + cmd: "get manifest", + golden: "output/get-manifest-no-args.txt", + wantError: true, + }} + runTestCmd(t, tests) +} + +func TestGetManifestCompletion(t *testing.T) { + checkReleaseCompletion(t, "get manifest", false) +} + +func TestGetManifestRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get manifest") +} + +func TestGetManifestFileCompletion(t *testing.T) { + checkFileCompletion(t, "get manifest", false) + checkFileCompletion(t, "get manifest myrelease", false) } diff --git a/cmd/helm/get_notes.go b/cmd/helm/get_notes.go new file mode 100644 index 00000000000..b71bcbdf621 --- /dev/null +++ b/cmd/helm/get_notes.go @@ -0,0 +1,74 @@ +/* +Copyright The Helm Authors. + +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" + "log" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +var getNotesHelp = ` +This command shows notes provided by the chart of a named release. +` + +func newGetNotesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewGet(cfg) + + cmd := &cobra.Command{ + Use: "notes RELEASE_NAME", + Short: "download the notes for a named release", + Long: getNotesHelp, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compListReleases(toComplete, args, cfg) + }, + RunE: func(cmd *cobra.Command, args []string) error { + res, err := client.Run(args[0]) + if err != nil { + return err + } + if len(res.Info.Notes) > 0 { + fmt.Fprintf(out, "NOTES:\n%s\n", res.Info.Notes) + } + return nil + }, + } + + f := cmd.Flags() + f.IntVar(&client.Version, "revision", 0, "get the named release with revision") + err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 1 { + return compListRevisions(toComplete, cfg, args[0]) + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) + + if err != nil { + log.Fatal(err) + } + + return cmd +} diff --git a/cmd/helm/get_notes_test.go b/cmd/helm/get_notes_test.go new file mode 100644 index 00000000000..8be9a3f7c1f --- /dev/null +++ b/cmd/helm/get_notes_test.go @@ -0,0 +1,51 @@ +/* +Copyright The Helm Authors. + +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" + + "helm.sh/helm/v3/pkg/release" +) + +func TestGetNotesCmd(t *testing.T) { + tests := []cmdTestCase{{ + name: "get notes of a deployed release", + cmd: "get notes the-limerick", + golden: "output/get-notes.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "the-limerick"})}, + }, { + name: "get notes without args", + cmd: "get notes", + golden: "output/get-notes-no-args.txt", + wantError: true, + }} + runTestCmd(t, tests) +} + +func TestGetNotesCompletion(t *testing.T) { + checkReleaseCompletion(t, "get notes", false) +} + +func TestGetNotesRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get notes") +} + +func TestGetNotesFileCompletion(t *testing.T) { + checkFileCompletion(t, "get notes", false) + checkFileCompletion(t, "get notes myrelease", false) +} diff --git a/cmd/helm/get_test.go b/cmd/helm/get_test.go index a6e72987ad4..79f914beaf5 100644 --- a/cmd/helm/get_test.go +++ b/cmd/helm/get_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,32 +17,9 @@ limitations under the License. package main import ( - "io" "testing" - - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" ) -func TestGetCmd(t *testing.T) { - tests := []releaseCase{ - { - name: "get with a release", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}), - args: []string{"thomas-guide"}, - expected: "REVISION: 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" + helm.MockHookTemplate + "\nMANIFEST:", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"})}, - }, - { - name: "get requires release name arg", - err: true, - }, - } - - cmd := func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newGetCmd(c, out) - } - runReleaseCases(t, tests, cmd) +func TestGetFileCompletion(t *testing.T) { + checkFileCompletion(t, "get", false) } diff --git a/cmd/helm/get_values.go b/cmd/helm/get_values.go index b6ce648e519..6124e1b3302 100644 --- a/cmd/helm/get_values.go +++ b/cmd/helm/get_values.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,71 +19,80 @@ package main import ( "fmt" "io" + "log" "github.com/spf13/cobra" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/helm" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" ) var getValuesHelp = ` This command downloads a values file for a given release. ` -type getValuesCmd struct { - release string +type valuesWriter struct { + vals map[string]interface{} allValues bool - out io.Writer - client helm.Interface - version int32 } -func newGetValuesCmd(client helm.Interface, out io.Writer) *cobra.Command { - get := &getValuesCmd{ - out: out, - client: client, - } +func newGetValuesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + var outfmt output.Format + client := action.NewGetValues(cfg) + cmd := &cobra.Command{ - Use: "values [flags] RELEASE_NAME", - Short: "download the values file for a named release", - Long: getValuesHelp, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + Use: "values RELEASE_NAME", + Short: "download the values file for a named release", + Long: getValuesHelp, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compListReleases(toComplete, args, cfg) + }, RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired + vals, err := client.Run(args[0]) + if err != nil { + return err } - get.release = args[0] - get.client = ensureHelmClient(get.client) - return get.run() + return outfmt.Write(out, &valuesWriter{vals, client.AllValues}) }, } - cmd.Flags().Int32Var(&get.version, "revision", 0, "get the named release with revision") - cmd.Flags().BoolVarP(&get.allValues, "all", "a", false, "dump all (computed) values") - return cmd -} + f := cmd.Flags() + f.IntVar(&client.Version, "revision", 0, "get the named release with revision") + err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 1 { + return compListRevisions(toComplete, cfg, args[0]) + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) -// getValues implements 'helm get values' -func (g *getValuesCmd) run() error { - res, err := g.client.ReleaseContent(g.release, helm.ContentReleaseVersion(g.version)) if err != nil { - return prettyError(err) + log.Fatal(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) - if err != nil { - return err - } - cfgStr, err := cfg.YAML() - if err != nil { - return err - } - fmt.Fprintln(g.out, cfgStr) - return nil + f.BoolVarP(&client.AllValues, "all", "a", false, "dump all (computed) values") + bindOutputFlag(cmd, &outfmt) + + return cmd +} + +func (v valuesWriter) WriteTable(out io.Writer) error { + if v.allValues { + fmt.Fprintln(out, "COMPUTED VALUES:") + } else { + fmt.Fprintln(out, "USER-SUPPLIED VALUES:") } + return output.EncodeYAML(out, v.vals) +} + +func (v valuesWriter) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, v.vals) +} - fmt.Fprintln(g.out, res.Release.Config.Raw) - return nil +func (v valuesWriter) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, v.vals) } diff --git a/cmd/helm/get_values_test.go b/cmd/helm/get_values_test.go index 30b2ba4b86d..423c32859e4 100644 --- a/cmd/helm/get_values_test.go +++ b/cmd/helm/get_values_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,31 +17,55 @@ limitations under the License. package main import ( - "io" "testing" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" + "helm.sh/helm/v3/pkg/release" ) func TestGetValuesCmd(t *testing.T) { - tests := []releaseCase{ - { - name: "get values with a release", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}), - args: []string{"thomas-guide"}, - expected: "name: \"value\"", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"})}, - }, - { - name: "get values requires release name arg", - err: true, - }, - } - cmd := func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newGetValuesCmd(c, out) - } - runReleaseCases(t, tests, cmd) + tests := []cmdTestCase{{ + name: "get values with a release", + cmd: "get values thomas-guide", + golden: "output/get-values.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }, { + name: "get values requires release name arg", + cmd: "get values", + golden: "output/get-values-args.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + wantError: true, + }, { + name: "get values thomas-guide (all)", + cmd: "get values thomas-guide --all", + golden: "output/get-values-all.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }, { + name: "get values to json", + cmd: "get values thomas-guide --output json", + golden: "output/values.json", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }, { + name: "get values to yaml", + cmd: "get values thomas-guide --output yaml", + golden: "output/values.yaml", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }} + runTestCmd(t, tests) +} + +func TestGetValuesCompletion(t *testing.T) { + checkReleaseCompletion(t, "get values", false) +} + +func TestGetValuesRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get values") +} + +func TestGetValuesOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "get values") +} + +func TestGetValuesFileCompletion(t *testing.T) { + checkFileCompletion(t, "get values", false) + checkFileCompletion(t, "get values myrelease", false) } diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 4c7ca92909c..d0aab5c184f 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main // import "k8s.io/helm/cmd/helm" +package main // import "helm.sh/helm/v3/cmd/helm" import ( "fmt" @@ -24,287 +24,113 @@ import ( "strings" "github.com/spf13/cobra" - "google.golang.org/grpc/grpclog" - "google.golang.org/grpc/status" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "sigs.k8s.io/yaml" // Import to initialize client auth plugins. _ "k8s.io/client-go/plugin/pkg/client/auth" - "k8s.io/helm/pkg/helm" - helm_env "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/helm/portforwarder" - "k8s.io/helm/pkg/kube" - "k8s.io/helm/pkg/tlsutil" -) - -var ( - tlsCaCertFile string // path to TLS CA certificate file - tlsCertFile string // path to TLS certificate file - tlsKeyFile string // path to TLS key file - tlsVerify bool // enable TLS and verify remote certificates - tlsEnable bool // enable TLS - - tlsCaCertDefault = "$HELM_HOME/ca.pem" - tlsCertDefault = "$HELM_HOME/cert.pem" - tlsKeyDefault = "$HELM_HOME/key.pem" - tillerTunnel *kube.Tunnel - settings helm_env.EnvSettings + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/gates" + kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage/driver" ) -var globalUsage = `The Kubernetes package manager - -To begin working with Helm, run the 'helm init' command: - - $ helm init +// FeatureGateOCI is the feature gate for checking if `helm chart` and `helm registry` commands should work +const FeatureGateOCI = gates.Gate("HELM_EXPERIMENTAL_OCI") -This will install Tiller to your running Kubernetes cluster. -It will also set up any necessary local configuration. +var settings = cli.New() -Common actions from this point include: - -- helm search: search for charts -- helm fetch: download a chart to your local directory to view -- helm install: upload the chart to Kubernetes -- helm list: list releases of charts - -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-system") - $KUBECONFIG set an alternative Kubernetes configuration file (default "~/.kube/config") -` +func init() { + log.SetFlags(log.Lshortfile) +} -func newRootCmd(args []string) *cobra.Command { - cmd := &cobra.Command{ - Use: "helm", - Short: "The Helm package manager for Kubernetes.", - Long: globalUsage, - SilenceUsage: true, - PersistentPreRun: func(*cobra.Command, []string) { - tlsCaCertFile = os.ExpandEnv(tlsCaCertFile) - tlsCertFile = os.ExpandEnv(tlsCertFile) - tlsKeyFile = os.ExpandEnv(tlsKeyFile) - }, - PersistentPostRun: func(*cobra.Command, []string) { - teardown() - }, +func debug(format string, v ...interface{}) { + if settings.Debug { + format = fmt.Sprintf("[debug] %s\n", format) + log.Output(2, fmt.Sprintf(format, v...)) } - flags := cmd.PersistentFlags() - - settings.AddFlags(flags) - - out := cmd.OutOrStdout() - - cmd.AddCommand( - // chart commands - newCreateCmd(out), - newDependencyCmd(out), - newFetchCmd(out), - newInspectCmd(out), - newLintCmd(out), - newPackageCmd(out), - newRepoCmd(out), - newSearchCmd(out), - newServeCmd(out), - newVerifyCmd(out), - - // release commands - addFlagsTLS(newDeleteCmd(nil, out)), - addFlagsTLS(newGetCmd(nil, out)), - addFlagsTLS(newHistoryCmd(nil, out)), - addFlagsTLS(newInstallCmd(nil, out)), - addFlagsTLS(newListCmd(nil, out)), - addFlagsTLS(newRollbackCmd(nil, out)), - addFlagsTLS(newStatusCmd(nil, out)), - addFlagsTLS(newUpgradeCmd(nil, out)), - - addFlagsTLS(newReleaseTestCmd(nil, out)), - addFlagsTLS(newResetCmd(nil, out)), - addFlagsTLS(newVersionCmd(nil, out)), - - newCompletionCmd(out), - newHomeCmd(out), - newInitCmd(out), - newPluginCmd(out), - newTemplateCmd(out), - - // Hidden documentation generator command: 'helm docs' - newDocsCmd(out), - - // Deprecated - markDeprecated(newRepoUpdateCmd(out), "use 'helm repo update'\n"), - ) - - flags.Parse(args) - - // set defaults from environment - settings.Init(flags) - - // Find and add plugins - loadPlugins(cmd, out) - - return cmd } -func init() { - // Tell gRPC not to log to console. - grpclog.SetLogger(log.New(ioutil.Discard, "", log.LstdFlags)) +func warning(format string, v ...interface{}) { + format = fmt.Sprintf("WARNING: %s\n", format) + fmt.Fprintf(os.Stderr, format, v...) } func main() { - cmd := newRootCmd(os.Args[1:]) - if err := cmd.Execute(); err != nil { + actionConfig := new(action.Configuration) + cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:]) + if err != nil { + warning("%+v", err) os.Exit(1) } -} -func markDeprecated(cmd *cobra.Command, notice string) *cobra.Command { - cmd.Deprecated = notice - return cmd -} - -func setupConnection() error { - if settings.TillerHost == "" { - config, client, err := getKubeClient(settings.KubeContext) - if err != nil { - return err + // run when each command's execute method is called + cobra.OnInitialize(func() { + helmDriver := os.Getenv("HELM_DRIVER") + if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil { + log.Fatal(err) } - - tunnel, err := portforwarder.New(settings.TillerNamespace, client, config) - if err != nil { - return err + if helmDriver == "memory" { + loadReleasesInMemory(actionConfig) } + }) - settings.TillerHost = fmt.Sprintf("127.0.0.1:%d", tunnel.Local) - debug("Created tunnel using local port: '%d'\n", tunnel.Local) - } - - // Set up the gRPC config. - debug("SERVER: %q\n", settings.TillerHost) - - // Plugin support. - return nil -} - -func teardown() { - if tillerTunnel != nil { - tillerTunnel.Close() - } -} - -func checkArgsLength(argsReceived int, requiredArgs ...string) error { - expectedNum := len(requiredArgs) - if argsReceived != expectedNum { - arg := "arguments" - if expectedNum == 1 { - arg = "argument" + if err := cmd.Execute(); err != nil { + debug("%+v", err) + switch e := err.(type) { + case pluginError: + os.Exit(e.code) + default: + os.Exit(1) } - return fmt.Errorf("This command needs %v %s: %s", expectedNum, arg, strings.Join(requiredArgs, ", ")) } - return nil } -// prettyError unwraps or rewrites certain errors to make them more user-friendly. -func prettyError(err error) error { - // Add this check can prevent the object creation if err is nil. - if err == nil { +func checkOCIFeatureGate() func(_ *cobra.Command, _ []string) error { + return func(_ *cobra.Command, _ []string) error { + if !FeatureGateOCI.IsEnabled() { + return FeatureGateOCI.Error() + } return nil } - // If it's grpc's error, make it more user-friendly. - if s, ok := status.FromError(err); ok { - return fmt.Errorf(s.Message()) - } - // Else return the original error. - return err -} - -// configForContext creates a Kubernetes REST client configuration for a given kubeconfig context. -func configForContext(context string) (*rest.Config, error) { - config, err := kube.GetConfig(context).ClientConfig() - if err != nil { - return nil, fmt.Errorf("could not get Kubernetes config for context %q: %s", context, err) - } - return config, nil -} - -// getKubeClient creates a Kubernetes config and client for a given kubeconfig context. -func getKubeClient(context string) (*rest.Config, kubernetes.Interface, error) { - config, err := configForContext(context) - if err != nil { - return nil, nil, err - } - client, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, nil, fmt.Errorf("could not get Kubernetes client: %s", err) - } - return config, client, nil } -// getInternalKubeClient creates a Kubernetes config and an "internal" client for a given kubeconfig context. -// -// Prefer the similar getKubeClient if you don't need to use such an internal client. -func getInternalKubeClient(context string) (internalclientset.Interface, error) { - config, err := configForContext(context) - if err != nil { - return nil, err - } - client, err := internalclientset.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("could not get Kubernetes client: %s", err) +// This function loads releases into the memory storage if the +// environment variable is properly set. +func loadReleasesInMemory(actionConfig *action.Configuration) { + filePaths := strings.Split(os.Getenv("HELM_MEMORY_DRIVER_DATA"), ":") + if len(filePaths) == 0 { + return } - return client, nil -} -// ensureHelmClient returns a new helm client impl. if h is not nil. -func ensureHelmClient(h helm.Interface) helm.Interface { - if h != nil { - return h + store := actionConfig.Releases + mem, ok := store.Driver.(*driver.Memory) + if !ok { + // For an unexpected reason we are not dealing with the memory storage driver. + return } - return newClient() -} -func newClient() helm.Interface { - options := []helm.Option{helm.Host(settings.TillerHost), helm.ConnectTimeout(settings.TillerConnectionTimeout)} + actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard} - if tlsVerify || tlsEnable { - if tlsCaCertFile == "" { - tlsCaCertFile = settings.Home.TLSCaCert() - } - if tlsCertFile == "" { - tlsCertFile = settings.Home.TLSCert() - } - if tlsKeyFile == "" { - tlsKeyFile = settings.Home.TLSKey() - } - debug("Key=%q, Cert=%q, CA=%q\n", tlsKeyFile, tlsCertFile, tlsCaCertFile) - tlsopts := tlsutil.Options{KeyFile: tlsKeyFile, CertFile: tlsCertFile, InsecureSkipVerify: true} - if tlsVerify { - tlsopts.CaCertFile = tlsCaCertFile - tlsopts.InsecureSkipVerify = false - } - tlscfg, err := tlsutil.ClientConfig(tlsopts) + for _, path := range filePaths { + b, err := ioutil.ReadFile(path) if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(2) + log.Fatal("Unable to read memory driver data", err) } - options = append(options, helm.WithTLS(tlscfg)) - } - return helm.NewClient(options...) -} -// addFlagsTLS adds the flags for supporting client side TLS to the -// helm command (only those that invoke communicate to Tiller.) -func addFlagsTLS(cmd *cobra.Command) *cobra.Command { + releases := []*release.Release{} + if err := yaml.Unmarshal(b, &releases); err != nil { + log.Fatal("Unable to unmarshal memory driver data: ", err) + } - // add flags - cmd.Flags().StringVar(&tlsCaCertFile, "tls-ca-cert", tlsCaCertDefault, "path to TLS CA certificate file") - cmd.Flags().StringVar(&tlsCertFile, "tls-cert", tlsCertDefault, "path to TLS certificate file") - cmd.Flags().StringVar(&tlsKeyFile, "tls-key", tlsKeyDefault, "path to TLS key file") - cmd.Flags().BoolVar(&tlsVerify, "tls-verify", false, "enable TLS for request and verify remote") - cmd.Flags().BoolVar(&tlsEnable, "tls", false, "enable TLS for request") - return cmd + for _, rel := range releases { + if err := store.Create(rel); err != nil { + log.Fatal(err) + } + } + } + // Must reset namespace to the proper one + mem.SetNamespace(settings.Namespace()) } diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 79b8c16f2a9..5e59c41ed62 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,225 +18,223 @@ package main import ( "bytes" - "fmt" - "io" "io/ioutil" "os" - "path/filepath" - "regexp" + "os/exec" + "runtime" "strings" "testing" + shellwords "github.com/mattn/go-shellwords" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/repo" + "helm.sh/helm/v3/internal/test" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli" + kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage" + "helm.sh/helm/v3/pkg/storage/driver" + "helm.sh/helm/v3/pkg/time" ) -// releaseCmd is a command that works with a FakeClient -type releaseCmd func(c *helm.FakeClient, out io.Writer) *cobra.Command +func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } -// runReleaseCases runs a set of release cases through the given releaseCmd. -func runReleaseCases(t *testing.T, tests []releaseCase, rcmd releaseCmd) { - var buf bytes.Buffer +func init() { + action.Timestamper = testTimestamper +} + +func runTestCmd(t *testing.T, tests []cmdTestCase) { + t.Helper() + for _, tt := range tests { + for i := 0; i <= tt.repeat; i++ { + t.Run(tt.name, func(t *testing.T) { + defer resetEnv()() + + storage := storageFixture() + for _, rel := range tt.rels { + if err := storage.Create(rel); err != nil { + t.Fatal(err) + } + } + t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd) + _, out, err := executeActionCommandC(storage, tt.cmd) + if (err != nil) != tt.wantError { + t.Errorf("expected error, got '%v'", err) + } + if tt.golden != "" { + test.AssertGoldenString(t, out, tt.golden) + } + }) + } + } +} + +func runTestActionCmd(t *testing.T, tests []cmdTestCase) { + t.Helper() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &helm.FakeClient{ - Rels: tt.rels, - Responses: tt.responses, + defer resetEnv()() + + store := storageFixture() + for _, rel := range tt.rels { + store.Create(rel) } - cmd := rcmd(c, &buf) - cmd.ParseFlags(tt.flags) - err := cmd.RunE(cmd, tt.args) - if (err != nil) != tt.err { + _, out, err := executeActionCommandC(store, tt.cmd) + if (err != nil) != tt.wantError { t.Errorf("expected error, got '%v'", err) } - re := regexp.MustCompile(tt.expected) - if !re.Match(buf.Bytes()) { - t.Errorf("expected\n%q\ngot\n%q", tt.expected, buf.String()) + if tt.golden != "" { + test.AssertGoldenString(t, out, tt.golden) } - buf.Reset() }) } } -// releaseCase describes a test case that works with releases. -type releaseCase struct { - name string - args []string - flags []string - // expected is the string to be matched. This supports regular expressions. - expected string - err bool - resp *release.Release - // Rels are the available releases at the start of the test. - rels []*release.Release - responses map[string]release.TestRun_Status +func storageFixture() *storage.Storage { + return storage.Init(driver.NewMemory()) +} + +func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) { + return executeActionCommandStdinC(store, nil, cmd) } -// tempHelmHome sets up a Helm Home in a temp dir. -// -// This does not clean up the directory. You must do that yourself. -// You must also set helmHome yourself. -func tempHelmHome(t *testing.T) (helmpath.Home, error) { - oldhome := settings.Home - dir, err := ioutil.TempDir("", "helm_home-") +func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) (*cobra.Command, string, error) { + args, err := shellwords.Parse(cmd) if err != nil { - return helmpath.Home("n/"), err + return nil, "", err } - settings.Home = helmpath.Home(dir) - if err := ensureTestHome(settings.Home, t); err != nil { - return helmpath.Home("n/"), err - } - settings.Home = oldhome - return helmpath.Home(dir), nil -} + buf := new(bytes.Buffer) -// ensureTestHome creates a home directory like ensureHome, but without remote references. -// -// t is used only for logging. -func ensureTestHome(home helmpath.Home, t *testing.T) error { - configDirectories := []string{home.String(), home.Repository(), home.Cache(), home.LocalRepository(), home.Plugins(), home.Starters()} - for _, p := range configDirectories { - if fi, err := os.Stat(p); err != nil { - if err := os.MkdirAll(p, 0755); err != nil { - return fmt.Errorf("Could not create %s: %s", p, err) - } - } else if !fi.IsDir() { - return fmt.Errorf("%s must be a directory", p) - } + actionConfig := &action.Configuration{ + Releases: store, + KubeClient: &kubefake.PrintingKubeClient{Out: ioutil.Discard}, + Capabilities: chartutil.DefaultCapabilities, + Log: func(format string, v ...interface{}) {}, } - repoFile := home.RepositoryFile() - if fi, err := os.Stat(repoFile); err != nil { - rf := repo.NewRepoFile() - rf.Add(&repo.Entry{ - Name: "charts", - URL: "http://example.com/foo", - Cache: "charts-index.yaml", - }, &repo.Entry{ - Name: "local", - URL: "http://localhost.com:7743/foo", - Cache: "local-index.yaml", - }) - if err := rf.WriteFile(repoFile, 0644); err != nil { - return err - } - } else if fi.IsDir() { - return fmt.Errorf("%s must be a file, not a directory", repoFile) - } - if r, err := repo.LoadRepositoriesFile(repoFile); err == repo.ErrRepoOutOfDate { - t.Log("Updating repository file format...") - if err := r.WriteFile(repoFile, 0644); err != nil { - return err - } + root, err := newRootCmd(actionConfig, buf, args) + if err != nil { + return nil, "", err } - localRepoIndexFile := home.LocalRepository(localRepositoryIndexFile) - if fi, err := os.Stat(localRepoIndexFile); err != nil { - i := repo.NewIndexFile() - if err := i.WriteFile(localRepoIndexFile, 0644); err != nil { - return err - } + root.SetOut(buf) + root.SetErr(buf) + root.SetArgs(args) - //TODO: take this out and replace with helm update functionality - os.Symlink(localRepoIndexFile, home.CacheIndex("local")) - } else if fi.IsDir() { - return fmt.Errorf("%s must be a file, not a directory", localRepoIndexFile) + oldStdin := os.Stdin + if in != nil { + root.SetIn(in) + os.Stdin = in } - t.Logf("$HELM_HOME has been configured at %s.\n", settings.Home.String()) - return nil - -} - -func TestRootCmd(t *testing.T) { - cleanup := resetEnv() - defer cleanup() - - tests := []struct { - name string - args []string - envars map[string]string - home string - }{ - { - name: "defaults", - args: []string{"home"}, - home: filepath.Join(os.Getenv("HOME"), "/.helm"), - }, - { - name: "with --home set", - args: []string{"--home", "/foo"}, - home: "/foo", - }, - { - name: "subcommands with --home set", - args: []string{"home", "--home", "/foo"}, - home: "/foo", - }, - { - name: "with $HELM_HOME set", - args: []string{"home"}, - envars: map[string]string{"HELM_HOME": "/bar"}, - home: "/bar", - }, - { - name: "subcommands with $HELM_HOME set", - args: []string{"home"}, - envars: map[string]string{"HELM_HOME": "/bar"}, - home: "/bar", - }, - { - name: "with $HELM_HOME and --home set", - args: []string{"home", "--home", "/foo"}, - envars: map[string]string{"HELM_HOME": "/bar"}, - home: "/foo", - }, + if mem, ok := store.Driver.(*driver.Memory); ok { + mem.SetNamespace(settings.Namespace()) } + c, err := root.ExecuteC() - // ensure not set locally - os.Unsetenv("HELM_HOME") + result := buf.String() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - defer os.Unsetenv("HELM_HOME") + os.Stdin = oldStdin - for k, v := range tt.envars { - os.Setenv(k, v) - } + return c, result, err +} - cmd := newRootCmd(tt.args) - cmd.SetOutput(ioutil.Discard) - cmd.SetArgs(tt.args) - cmd.Run = func(*cobra.Command, []string) {} - if err := cmd.Execute(); err != nil { - t.Errorf("unexpected error: %s", err) - } +// cmdTestCase describes a test case that works with releases. +type cmdTestCase struct { + name string + cmd string + golden string + wantError bool + // Rels are the available releases at the start of the test. + rels []*release.Release + // Number of repeats (in case a feature was previously flaky and the test checks + // it's now stably producing identical results). 0 means test is run exactly once. + repeat int +} - if settings.Home.String() != tt.home { - t.Errorf("expected home %q, got %q", tt.home, settings.Home) - } - homeFlag := cmd.Flag("home").Value.String() - homeFlag = os.ExpandEnv(homeFlag) - if homeFlag != tt.home { - t.Errorf("expected home %q, got %q", tt.home, homeFlag) - } - }) - } +func executeActionCommand(cmd string) (*cobra.Command, string, error) { + return executeActionCommandC(storageFixture(), cmd) } func resetEnv() func() { - origSettings := settings origEnv := os.Environ() return func() { - settings = origSettings + os.Clearenv() for _, pair := range origEnv { kv := strings.SplitN(pair, "=", 2) os.Setenv(kv[0], kv[1]) } + settings = cli.New() + } +} + +func testChdir(t *testing.T, dir string) func() { + t.Helper() + old, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + return func() { os.Chdir(old) } +} + +func TestPluginExitCode(t *testing.T) { + if os.Getenv("RUN_MAIN_FOR_TESTING") == "1" { + os.Args = []string{"helm", "exitwith", "2"} + + // We DO call helm's main() here. So this looks like a normal `helm` process. + main() + + // As main calls os.Exit, we never reach this line. + // But the test called this block of code catches and verifies the exit code. + return + } + + // Currently, plugins assume a Linux subsystem. Skip the execution + // tests until this is fixed + if runtime.GOOS != "windows" { + // Do a second run of this specific test(TestPluginExitCode) with RUN_MAIN_FOR_TESTING=1 set, + // So that the second run is able to run main() and this first run can verify the exit status returned by that. + // + // This technique originates from https://talks.golang.org/2014/testing.slide#23. + cmd := exec.Command(os.Args[0], "-test.run=TestPluginExitCode") + cmd.Env = append( + os.Environ(), + "RUN_MAIN_FOR_TESTING=1", + // See pkg/cli/environment.go for which envvars can be used for configuring these passes + // and also see plugin_test.go for how a plugin env can be set up. + // We just does the same setup as plugin_test.go via envvars + "HELM_PLUGINS=testdata/helmhome/helm/plugins", + "HELM_REPOSITORY_CONFIG=testdata/helmhome/helm/repositories.yaml", + "HELM_REPOSITORY_CACHE=testdata/helmhome/helm/repository", + ) + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + cmd.Stdout = stdout + cmd.Stderr = stderr + err := cmd.Run() + exiterr, ok := err.(*exec.ExitError) + + if !ok { + t.Fatalf("Unexpected error returned by os.Exit: %T", err) + } + + if stdout.String() != "" { + t.Errorf("Expected no write to stdout: Got %q", stdout.String()) + } + + expectedStderr := "Error: plugin \"exitwith\" exited with error\n" + if stderr.String() != expectedStderr { + t.Errorf("Expected %q written to stderr: Got %q", expectedStderr, stderr.String()) + } + + if exiterr.ExitCode() != 2 { + t.Errorf("Expected exit code 2: Got %d", exiterr.ExitCode()) + } } } diff --git a/cmd/helm/history.go b/cmd/helm/history.go index e6366d31d83..06ec07d6d89 100644 --- a/cmd/helm/history.go +++ b/cmd/helm/history.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,30 +17,24 @@ limitations under the License. package main import ( - "encoding/json" "fmt" "io" + "strconv" + "strings" + "time" - "github.com/ghodss/yaml" "github.com/gosuri/uitable" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/timeconv" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/cli/output" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/releaseutil" + helmtime "helm.sh/helm/v3/pkg/time" ) -type releaseInfo struct { - Revision int32 `json:"revision"` - Updated string `json:"updated"` - Status string `json:"status"` - Chart string `json:"chart"` - Description string `json:"description"` -} - -type releaseHistory []releaseInfo - var historyHelp = ` History prints historical revisions for a given release. @@ -49,124 +43,162 @@ configures the maximum length of the revision list returned. The historical release set is printed as a formatted table, e.g: - $ helm history angry-bird --max=4 - REVISION UPDATED STATUS CHART DESCRIPTION - 1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Initial install - 2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Upgraded successfully - 3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Rolled back to 2 - 4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 Upgraded successfully + $ helm history angry-bird + REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION + 1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install + 2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully + 3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Rolled back to 2 + 4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully ` -type historyCmd struct { - max int32 - rls string - out io.Writer - helmc helm.Interface - colWidth uint - outputFormat string -} - -func newHistoryCmd(c helm.Interface, w io.Writer) *cobra.Command { - his := &historyCmd{out: w, helmc: c} +func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewHistory(cfg) + var outfmt output.Format cmd := &cobra.Command{ - Use: "history [flags] RELEASE_NAME", + Use: "history RELEASE_NAME", Long: historyHelp, Short: "fetch release history", Aliases: []string{"hist"}, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compListReleases(toComplete, args, cfg) + }, RunE: func(cmd *cobra.Command, args []string) error { - switch { - case len(args) == 0: - return errReleaseRequired - case his.helmc == nil: - his.helmc = newClient() + history, err := getHistory(client, args[0]) + if err != nil { + return err } - his.rls = args[0] - return his.run() + + return outfmt.Write(out, history) }, } f := cmd.Flags() - f.Int32Var(&his.max, "max", 256, "maximum number of revision to include in history") - f.UintVar(&his.colWidth, "col-width", 60, "specifies the max column width of output") - f.StringVarP(&his.outputFormat, "output", "o", "table", "prints the output in the specified format (json|table|yaml)") + f.IntVar(&client.Max, "max", 256, "maximum number of revision to include in history") + bindOutputFlag(cmd, &outfmt) return cmd } -func (cmd *historyCmd) run() error { - r, err := cmd.helmc.ReleaseHistory(cmd.rls, helm.WithMaxHistory(cmd.max)) - if err != nil { - return prettyError(err) - } - if len(r.Releases) == 0 { - return nil +type releaseInfo struct { + Revision int `json:"revision"` + Updated helmtime.Time `json:"updated"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` + Description string `json:"description"` +} + +type releaseHistory []releaseInfo + +func (r releaseHistory) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, r) +} + +func (r releaseHistory) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, r) +} + +func (r releaseHistory) WriteTable(out io.Writer) error { + tbl := uitable.New() + tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") + for _, item := range r { + tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description) } + return output.EncodeTable(out, tbl) +} - releaseHistory := getReleaseHistory(r.Releases) +func getHistory(client *action.History, name string) (releaseHistory, error) { + hist, err := client.Run(name) + if err != nil { + return nil, err + } - var history []byte - var formattingError error + releaseutil.Reverse(hist, releaseutil.SortByRevision) - switch cmd.outputFormat { - case "yaml": - history, formattingError = yaml.Marshal(releaseHistory) - case "json": - history, formattingError = json.Marshal(releaseHistory) - case "table": - history = formatAsTable(releaseHistory, cmd.colWidth) - default: - return fmt.Errorf("unknown output format %q", cmd.outputFormat) + var rels []*release.Release + for i := 0; i < min(len(hist), client.Max); i++ { + rels = append(rels, hist[i]) } - if formattingError != nil { - return prettyError(formattingError) + if len(rels) == 0 { + return releaseHistory{}, nil } - fmt.Fprintln(cmd.out, string(history)) - return nil + releaseHistory := getReleaseHistory(rels) + + return releaseHistory, nil } func getReleaseHistory(rls []*release.Release) (history releaseHistory) { for i := len(rls) - 1; i >= 0; i-- { r := rls[i] c := formatChartname(r.Chart) - t := timeconv.String(r.Info.LastDeployed) - s := r.Info.Status.Code.String() + s := r.Info.Status.String() v := r.Version d := r.Info.Description + a := formatAppVersion(r.Chart) rInfo := releaseInfo{ Revision: v, - Updated: t, Status: s, Chart: c, + AppVersion: a, Description: d, } + if !r.Info.LastDeployed.IsZero() { + rInfo.Updated = r.Info.LastDeployed + + } history = append(history, rInfo) } return history } -func formatAsTable(releases releaseHistory, colWidth uint) []byte { - tbl := uitable.New() - - tbl.MaxColWidth = colWidth - tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "DESCRIPTION") - for i := 0; i <= len(releases)-1; i++ { - r := releases[i] - tbl.AddRow(r.Revision, r.Updated, r.Status, r.Chart, r.Description) +func formatChartname(c *chart.Chart) string { + if c == nil || c.Metadata == nil { + // This is an edge case that has happened in prod, though we don't + // know how: https://github.com/helm/helm/issues/1347 + return "MISSING" } - return tbl.Bytes() + return fmt.Sprintf("%s-%s", c.Name(), c.Metadata.Version) } -func formatChartname(c *chart.Chart) string { +func formatAppVersion(c *chart.Chart) string { if c == nil || c.Metadata == nil { // This is an edge case that has happened in prod, though we don't - // know how: https://github.com/kubernetes/helm/issues/1347 + // know how: https://github.com/helm/helm/issues/1347 return "MISSING" } - return fmt.Sprintf("%s-%s", c.Metadata.Name, c.Metadata.Version) + return c.AppVersion() +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} + +func compListRevisions(toComplete string, cfg *action.Configuration, releaseName string) ([]string, cobra.ShellCompDirective) { + client := action.NewHistory(cfg) + + var revisions []string + if hist, err := client.Run(releaseName); err == nil { + for _, release := range hist { + version := strconv.Itoa(release.Version) + if strings.HasPrefix(version, toComplete) { + appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion) + chartDesc := fmt.Sprintf("Chart: %s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version) + revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", version, appVersion, chartDesc)) + } + } + return revisions, cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveError } diff --git a/cmd/helm/history_test.go b/cmd/helm/history_test.go index fef43374206..2663e9ee92a 100644 --- a/cmd/helm/history_test.go +++ b/cmd/helm/history_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,69 +17,103 @@ limitations under the License. package main import ( - "io" + "fmt" "testing" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" - rpb "k8s.io/helm/pkg/proto/hapi/release" + "helm.sh/helm/v3/pkg/release" ) func TestHistoryCmd(t *testing.T) { - mk := func(name string, vers int32, code rpb.Status_Code) *rpb.Release { - return helm.ReleaseMock(&helm.MockReleaseOptions{ - Name: name, - Version: vers, - StatusCode: code, + mk := func(name string, vers int, status release.Status) *release.Release { + return release.Mock(&release.MockReleaseOptions{ + Name: name, + Version: vers, + Status: status, }) } - tests := []releaseCase{ - { - name: "get history for release", - args: []string{"angry-bird"}, - rels: []*rpb.Release{ - mk("angry-bird", 4, rpb.Status_DEPLOYED), - mk("angry-bird", 3, rpb.Status_SUPERSEDED), - mk("angry-bird", 2, rpb.Status_SUPERSEDED), - mk("angry-bird", 1, rpb.Status_SUPERSEDED), - }, - expected: "REVISION\tUPDATED \tSTATUS \tCHART \tDESCRIPTION \n1 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n2 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\tRelease mock\n", + tests := []cmdTestCase{{ + name: "get history for release", + cmd: "history angry-bird", + rels: []*release.Release{ + mk("angry-bird", 4, release.StatusDeployed), + mk("angry-bird", 3, release.StatusSuperseded), + mk("angry-bird", 2, release.StatusSuperseded), + mk("angry-bird", 1, release.StatusSuperseded), }, - { - name: "get history with max limit set", - args: []string{"angry-bird"}, - flags: []string{"--max", "2"}, - rels: []*rpb.Release{ - mk("angry-bird", 4, rpb.Status_DEPLOYED), - mk("angry-bird", 3, rpb.Status_SUPERSEDED), - }, - expected: "REVISION\tUPDATED \tSTATUS \tCHART \tDESCRIPTION \n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\tRelease mock\n", + golden: "output/history.txt", + }, { + name: "get history with max limit set", + cmd: "history angry-bird --max 2", + rels: []*release.Release{ + mk("angry-bird", 4, release.StatusDeployed), + mk("angry-bird", 3, release.StatusSuperseded), }, - { - name: "get history with yaml output format", - args: []string{"angry-bird"}, - flags: []string{"--output", "yaml"}, - rels: []*rpb.Release{ - mk("angry-bird", 4, rpb.Status_DEPLOYED), - mk("angry-bird", 3, rpb.Status_SUPERSEDED), - }, - expected: "- chart: foo-0.1.0-beta.1\n description: Release mock\n revision: 3\n status: SUPERSEDED\n updated: (.*)\n- chart: foo-0.1.0-beta.1\n description: Release mock\n revision: 4\n status: DEPLOYED\n updated: (.*)\n\n", + golden: "output/history-limit.txt", + }, { + name: "get history with yaml output format", + cmd: "history angry-bird --output yaml", + rels: []*release.Release{ + mk("angry-bird", 4, release.StatusDeployed), + mk("angry-bird", 3, release.StatusSuperseded), }, - { - name: "get history with json output format", - args: []string{"angry-bird"}, - flags: []string{"--output", "json"}, - rels: []*rpb.Release{ - mk("angry-bird", 4, rpb.Status_DEPLOYED), - mk("angry-bird", 3, rpb.Status_SUPERSEDED), - }, - expected: `[{"revision":3,"updated":".*","status":"SUPERSEDED","chart":"foo\-0.1.0-beta.1","description":"Release mock"},{"revision":4,"updated":".*","status":"DEPLOYED","chart":"foo\-0.1.0-beta.1","description":"Release mock"}]\n`, + golden: "output/history.yaml", + }, { + name: "get history with json output format", + cmd: "history angry-bird --output json", + rels: []*release.Release{ + mk("angry-bird", 4, release.StatusDeployed), + mk("angry-bird", 3, release.StatusSuperseded), }, + golden: "output/history.json", + }} + runTestCmd(t, tests) +} + +func TestHistoryOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "history") +} + +func revisionFlagCompletionTest(t *testing.T, cmdName string) { + mk := func(name string, vers int, status release.Status) *release.Release { + return release.Mock(&release.MockReleaseOptions{ + Name: name, + Version: vers, + Status: status, + }) } - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newHistoryCmd(c, out) - }) + releases := []*release.Release{ + mk("musketeers", 11, release.StatusDeployed), + mk("musketeers", 10, release.StatusSuperseded), + mk("musketeers", 9, release.StatusSuperseded), + mk("musketeers", 8, release.StatusSuperseded), + } + + tests := []cmdTestCase{{ + name: "completion for revision flag", + cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName), + rels: releases, + golden: "output/revision-comp.txt", + }, { + name: "completion for revision flag with too few args", + cmd: fmt.Sprintf("__complete %s --revision ''", cmdName), + rels: releases, + golden: "output/revision-wrong-args-comp.txt", + }, { + name: "completion for revision flag with too many args", + cmd: fmt.Sprintf("__complete %s three musketeers --revision ''", cmdName), + rels: releases, + golden: "output/revision-wrong-args-comp.txt", + }} + runTestCmd(t, tests) +} + +func TestHistoryCompletion(t *testing.T) { + checkReleaseCompletion(t, "history", false) +} + +func TestHistoryFileCompletion(t *testing.T) { + checkFileCompletion(t, "history", false) + checkFileCompletion(t, "history myrelease", false) } diff --git a/cmd/helm/home.go b/cmd/helm/home.go deleted file mode 100644 index e96edd7a127..00000000000 --- a/cmd/helm/home.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -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" -) - -var longHomeHelp = ` -This command displays the location of HELM_HOME. This is where -any helm configuration files live. -` - -func newHomeCmd(out io.Writer) *cobra.Command { - cmd := &cobra.Command{ - Use: "home", - Short: "displays the location of HELM_HOME", - Long: longHomeHelp, - Run: func(cmd *cobra.Command, args []string) { - h := settings.Home - fmt.Fprintln(out, h) - if settings.Debug { - fmt.Fprintf(out, "Repository: %s\n", h.Repository()) - fmt.Fprintf(out, "RepositoryFile: %s\n", h.RepositoryFile()) - fmt.Fprintf(out, "Cache: %s\n", h.Cache()) - fmt.Fprintf(out, "Stable CacheIndex: %s\n", h.CacheIndex("stable")) - fmt.Fprintf(out, "Starters: %s\n", h.Starters()) - fmt.Fprintf(out, "LocalRepository: %s\n", h.LocalRepository()) - fmt.Fprintf(out, "Plugins: %s\n", h.Plugins()) - } - }, - } - return cmd -} diff --git a/cmd/helm/init.go b/cmd/helm/init.go deleted file mode 100644 index d368945ec1e..00000000000 --- a/cmd/helm/init.go +++ /dev/null @@ -1,493 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "time" - - "github.com/spf13/cobra" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/client-go/kubernetes" - - "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/helm/cmd/helm/installer" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/helm/portforwarder" - "k8s.io/helm/pkg/repo" -) - -const initDesc = ` -This command installs Tiller (the Helm server-side component) onto your -Kubernetes Cluster and sets up local configuration in $HELM_HOME (default ~/.helm/). - -As with the rest of the Helm commands, 'helm init' discovers Kubernetes clusters -by reading $KUBECONFIG (default '~/.kube/config') and using the default context. - -To set up just a local environment, use '--client-only'. That will configure -$HELM_HOME, but not attempt to connect to a Kubernetes cluster and install the Tiller -deployment. - -When installing Tiller, 'helm init' will attempt to install the latest released -version. You can specify an alternative image with '--tiller-image'. For those -frequently working on the latest code, the flag '--canary-image' will install -the latest pre-release version of Tiller (e.g. the HEAD commit in the GitHub -repository on the master branch). - -To dump a manifest containing the Tiller deployment YAML, combine the -'--dry-run' and '--debug' flags. -` - -const ( - stableRepository = "stable" - localRepository = "local" - localRepositoryIndexFile = "index.yaml" -) - -var ( - stableRepositoryURL = "https://kubernetes-charts.storage.googleapis.com" - // This is the IPv4 loopback, not localhost, because we have to force IPv4 - // for Dockerized Helm: https://github.com/kubernetes/helm/issues/1410 - localRepositoryURL = "http://127.0.0.1:8879/charts" -) - -type initCmd struct { - image string - clientOnly bool - canary bool - upgrade bool - namespace string - dryRun bool - forceUpgrade bool - skipRefresh bool - out io.Writer - client helm.Interface - home helmpath.Home - opts installer.Options - kubeClient kubernetes.Interface - serviceAccount string - maxHistory int - replicas int - wait bool -} - -func newInitCmd(out io.Writer) *cobra.Command { - i := &initCmd{out: out} - - cmd := &cobra.Command{ - Use: "init", - Short: "initialize Helm on both client and server", - Long: initDesc, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 0 { - return errors.New("This command does not accept arguments") - } - i.namespace = settings.TillerNamespace - i.home = settings.Home - i.client = ensureHelmClient(i.client) - - return i.run() - }, - } - - f := cmd.Flags() - f.StringVarP(&i.image, "tiller-image", "i", "", "override Tiller image") - f.BoolVar(&i.canary, "canary-image", false, "use the canary Tiller image") - f.BoolVar(&i.upgrade, "upgrade", false, "upgrade if Tiller is already installed") - f.BoolVar(&i.forceUpgrade, "force-upgrade", false, "force upgrade of Tiller to the current helm version") - f.BoolVarP(&i.clientOnly, "client-only", "c", false, "if set does not install Tiller") - f.BoolVar(&i.dryRun, "dry-run", false, "do not install local or remote") - f.BoolVar(&i.skipRefresh, "skip-refresh", false, "do not refresh (download) the local repository cache") - f.BoolVar(&i.wait, "wait", false, "block until Tiller is running and ready to receive requests") - - f.BoolVar(&tlsEnable, "tiller-tls", false, "install Tiller with TLS enabled") - f.BoolVar(&tlsVerify, "tiller-tls-verify", false, "install Tiller with TLS enabled and to verify remote certificates") - f.StringVar(&tlsKeyFile, "tiller-tls-key", "", "path to TLS key file to install with Tiller") - f.StringVar(&tlsCertFile, "tiller-tls-cert", "", "path to TLS certificate file to install with Tiller") - f.StringVar(&tlsCaCertFile, "tls-ca-cert", "", "path to CA root certificate") - - f.StringVar(&stableRepositoryURL, "stable-repo-url", stableRepositoryURL, "URL for stable repository") - f.StringVar(&localRepositoryURL, "local-repo-url", localRepositoryURL, "URL for local repository") - - f.BoolVar(&i.opts.EnableHostNetwork, "net-host", false, "install Tiller with net=host") - f.StringVar(&i.serviceAccount, "service-account", "", "name of service account") - f.IntVar(&i.maxHistory, "history-max", 0, "limit the maximum number of revisions saved per release. Use 0 for no limit.") - f.IntVar(&i.replicas, "replicas", 1, "amount of tiller instances to run on the cluster") - - f.StringVar(&i.opts.NodeSelectors, "node-selectors", "", "labels to specify the node on which Tiller is installed (app=tiller,helm=rocks)") - f.VarP(&i.opts.Output, "output", "o", "skip installation and output Tiller's manifest in specified format (json or yaml)") - f.StringArrayVar(&i.opts.Values, "override", []string{}, "override values for the Tiller Deployment manifest (can specify multiple or separate values with commas: key1=val1,key2=val2)") - - return cmd -} - -// tlsOptions sanitizes the tls flags as well as checks for the existence of required -// tls files indicated by those flags, if any. -func (i *initCmd) tlsOptions() error { - i.opts.EnableTLS = tlsEnable || tlsVerify - i.opts.VerifyTLS = tlsVerify - - if i.opts.EnableTLS { - missing := func(file string) bool { - _, err := os.Stat(file) - return os.IsNotExist(err) - } - if i.opts.TLSKeyFile = tlsKeyFile; i.opts.TLSKeyFile == "" || missing(i.opts.TLSKeyFile) { - return errors.New("missing required TLS key file") - } - if i.opts.TLSCertFile = tlsCertFile; i.opts.TLSCertFile == "" || missing(i.opts.TLSCertFile) { - return errors.New("missing required TLS certificate file") - } - if i.opts.VerifyTLS { - if i.opts.TLSCaCertFile = tlsCaCertFile; i.opts.TLSCaCertFile == "" || missing(i.opts.TLSCaCertFile) { - return errors.New("missing required TLS CA file") - } - } - } - return nil -} - -// run initializes local config and installs Tiller to Kubernetes cluster. -func (i *initCmd) run() error { - if err := i.tlsOptions(); err != nil { - return err - } - i.opts.Namespace = i.namespace - i.opts.UseCanary = i.canary - i.opts.ImageSpec = i.image - i.opts.ForceUpgrade = i.forceUpgrade - i.opts.ServiceAccount = i.serviceAccount - i.opts.MaxHistory = i.maxHistory - i.opts.Replicas = i.replicas - - writeYAMLManifest := func(apiVersion, kind, body string, first, last bool) error { - w := i.out - if !first { - // YAML starting document boundary marker - if _, err := fmt.Fprintln(w, "---"); err != nil { - return err - } - } - if _, err := fmt.Fprintln(w, "apiVersion:", apiVersion); err != nil { - return err - } - if _, err := fmt.Fprintln(w, "kind:", kind); err != nil { - return err - } - if _, err := fmt.Fprint(w, body); err != nil { - return err - } - if !last { - return nil - } - // YAML ending document boundary marker - _, err := fmt.Fprintln(w, "...") - return err - } - if len(i.opts.Output) > 0 { - var body string - var err error - const tm = `{"apiVersion":"extensions/v1beta1","kind":"Deployment",` - if body, err = installer.DeploymentManifest(&i.opts); err != nil { - return err - } - switch i.opts.Output.String() { - case "json": - var out bytes.Buffer - jsonb, err := yaml.ToJSON([]byte(body)) - if err != nil { - return err - } - buf := bytes.NewBuffer(make([]byte, 0, len(tm)+len(jsonb)-1)) - buf.WriteString(tm) - // Drop the opening object delimiter ('{'). - buf.Write(jsonb[1:]) - if err := json.Indent(&out, buf.Bytes(), "", " "); err != nil { - return err - } - if _, err = i.out.Write(out.Bytes()); err != nil { - return err - } - - return nil - case "yaml": - if err := writeYAMLManifest("extensions/v1beta1", "Deployment", body, true, false); err != nil { - return err - } - return nil - default: - return fmt.Errorf("unknown output format: %q", i.opts.Output) - } - } - if settings.Debug { - - var body string - var err error - - // write Deployment manifest - if body, err = installer.DeploymentManifest(&i.opts); err != nil { - return err - } - if err := writeYAMLManifest("extensions/v1beta1", "Deployment", body, true, false); err != nil { - return err - } - - // write Service manifest - if body, err = installer.ServiceManifest(i.namespace); err != nil { - return err - } - if err := writeYAMLManifest("v1", "Service", body, false, !i.opts.EnableTLS); err != nil { - return err - } - - // write Secret manifest - if i.opts.EnableTLS { - if body, err = installer.SecretManifest(&i.opts); err != nil { - return err - } - if err := writeYAMLManifest("v1", "Secret", body, false, true); err != nil { - return err - } - } - } - - if i.dryRun { - return nil - } - - if err := ensureDirectories(i.home, i.out); err != nil { - return err - } - if err := ensureDefaultRepos(i.home, i.out, i.skipRefresh); err != nil { - return err - } - if err := ensureRepoFileFormat(i.home.RepositoryFile(), i.out); err != nil { - return err - } - fmt.Fprintf(i.out, "$HELM_HOME has been configured at %s.\n", settings.Home) - - if !i.clientOnly { - if i.kubeClient == nil { - _, c, err := getKubeClient(settings.KubeContext) - if err != nil { - return fmt.Errorf("could not get kubernetes client: %s", err) - } - i.kubeClient = c - } - if err := installer.Install(i.kubeClient, &i.opts); err != nil { - if !apierrors.IsAlreadyExists(err) { - return fmt.Errorf("error installing: %s", err) - } - if i.upgrade { - if err := installer.Upgrade(i.kubeClient, &i.opts); err != nil { - return fmt.Errorf("error when upgrading: %s", err) - } - if err := i.ping(); err != nil { - return err - } - fmt.Fprintln(i.out, "\nTiller (the Helm server-side component) has been upgraded to the current version.") - } else { - fmt.Fprintln(i.out, "Warning: Tiller is already installed in the cluster.\n"+ - "(Use --client-only to suppress this message, or --upgrade to upgrade Tiller to the current version.)") - } - } else { - fmt.Fprintln(i.out, "\nTiller (the Helm server-side component) has been installed into your Kubernetes Cluster.\n\n"+ - "Please note: by default, Tiller is deployed with an insecure 'allow unauthenticated users' policy.\n"+ - "For more information on securing your installation see: https://docs.helm.sh/using_helm/#securing-your-helm-installation") - } - if err := i.ping(); err != nil { - return err - } - } else { - fmt.Fprintln(i.out, "Not installing Tiller due to 'client-only' flag having been set") - } - - fmt.Fprintln(i.out, "Happy Helming!") - return nil -} - -func (i *initCmd) ping() error { - if i.wait { - _, kubeClient, err := getKubeClient(settings.KubeContext) - if err != nil { - return err - } - if !watchTillerUntilReady(settings.TillerNamespace, kubeClient, settings.TillerConnectionTimeout) { - return fmt.Errorf("tiller was not found. polling deadline exceeded") - } - - // establish a connection to Tiller now that we've effectively guaranteed it's available - if err := setupConnection(); err != nil { - return err - } - i.client = newClient() - if err := i.client.PingTiller(); err != nil { - return fmt.Errorf("could not ping Tiller: %s", err) - } - } - - return nil -} - -// ensureDirectories checks to see if $HELM_HOME exists. -// -// If $HELM_HOME does not exist, this function will create it. -func ensureDirectories(home helmpath.Home, out io.Writer) error { - configDirectories := []string{ - home.String(), - home.Repository(), - home.Cache(), - home.LocalRepository(), - home.Plugins(), - home.Starters(), - home.Archive(), - } - for _, p := range configDirectories { - if fi, err := os.Stat(p); err != nil { - fmt.Fprintf(out, "Creating %s \n", p) - if err := os.MkdirAll(p, 0755); err != nil { - return fmt.Errorf("Could not create %s: %s", p, err) - } - } else if !fi.IsDir() { - return fmt.Errorf("%s must be a directory", p) - } - } - - return nil -} - -func ensureDefaultRepos(home helmpath.Home, out io.Writer, skipRefresh bool) error { - repoFile := home.RepositoryFile() - if fi, err := os.Stat(repoFile); err != nil { - fmt.Fprintf(out, "Creating %s \n", repoFile) - f := repo.NewRepoFile() - sr, err := initStableRepo(home.CacheIndex(stableRepository), out, skipRefresh, home) - if err != nil { - return err - } - lr, err := initLocalRepo(home.LocalRepository(localRepositoryIndexFile), home.CacheIndex("local"), out, home) - if err != nil { - return err - } - f.Add(sr) - f.Add(lr) - if err := f.WriteFile(repoFile, 0644); err != nil { - return err - } - } else if fi.IsDir() { - return fmt.Errorf("%s must be a file, not a directory", repoFile) - } - return nil -} - -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, - Cache: cacheFile, - } - r, err := repo.NewChartRepository(&c, getter.All(settings)) - if err != nil { - return nil, err - } - - if skipRefresh { - return &c, nil - } - - // In this case, the cacheFile is always absolute. So passing empty string - // is safe. - if err := r.DownloadIndexFile(""); err != nil { - return nil, fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", stableRepositoryURL, err.Error()) - } - - return &c, nil -} - -func initLocalRepo(indexFile, cacheFile string, out io.Writer, home helmpath.Home) (*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 - } - - //TODO: take this out and replace with helm update functionality - if err := createLink(indexFile, cacheFile, home); err != nil { - return nil, err - } - } else if fi.IsDir() { - return nil, fmt.Errorf("%s must be a file, not a directory", indexFile) - } - - return &repo.Entry{ - Name: localRepository, - URL: localRepositoryURL, - Cache: cacheFile, - }, nil -} - -func ensureRepoFileFormat(file string, out io.Writer) error { - r, err := repo.LoadRepositoriesFile(file) - if err == repo.ErrRepoOutOfDate { - fmt.Fprintln(out, "Updating repository file format...") - if err := r.WriteFile(file, 0644); err != nil { - return err - } - } - - return nil -} - -// watchTillerUntilReady waits for the tiller pod to become available. This is useful in situations where we -// want to wait before we call New(). -// -// Returns true if it exists. If the timeout was reached and it could not find the pod, it returns false. -func watchTillerUntilReady(namespace string, client kubernetes.Interface, timeout int64) bool { - deadlinePollingChan := time.NewTimer(time.Duration(timeout) * time.Second).C - checkTillerPodTicker := time.NewTicker(500 * time.Millisecond) - doneChan := make(chan bool) - - defer checkTillerPodTicker.Stop() - - go func() { - for range checkTillerPodTicker.C { - _, err := portforwarder.GetTillerPodName(client.CoreV1(), namespace) - if err == nil { - doneChan <- true - break - } - } - }() - - for { - select { - case <-deadlinePollingChan: - return false - case <-doneChan: - return true - } - } -} diff --git a/cmd/helm/init_test.go b/cmd/helm/init_test.go deleted file mode 100644 index 6a5767fcaaf..00000000000 --- a/cmd/helm/init_test.go +++ /dev/null @@ -1,357 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "bytes" - "io/ioutil" - "os" - "path/filepath" - "reflect" - "strings" - "testing" - - "github.com/ghodss/yaml" - - "k8s.io/api/core/v1" - "k8s.io/api/extensions/v1beta1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes/fake" - testcore "k8s.io/client-go/testing" - - "encoding/json" - - "k8s.io/helm/cmd/helm/installer" - "k8s.io/helm/pkg/helm/helmpath" -) - -func TestInitCmd(t *testing.T) { - home, err := ioutil.TempDir("", "helm_home") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(home) - - var buf bytes.Buffer - fc := fake.NewSimpleClientset() - cmd := &initCmd{ - out: &buf, - home: helmpath.Home(home), - kubeClient: fc, - namespace: v1.NamespaceDefault, - } - if err := cmd.run(); err != nil { - t.Errorf("expected error: %v", err) - } - actions := fc.Actions() - if len(actions) != 2 { - t.Errorf("Expected 2 actions, got %d", len(actions)) - } - if !actions[0].Matches("create", "deployments") { - t.Errorf("unexpected action: %v, expected create deployment", actions[0]) - } - if !actions[1].Matches("create", "services") { - t.Errorf("unexpected action: %v, expected create service", actions[1]) - } - expected := "Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster." - if !strings.Contains(buf.String(), expected) { - t.Errorf("expected %q, got %q", expected, buf.String()) - } -} - -func TestInitCmd_exists(t *testing.T) { - home, err := ioutil.TempDir("", "helm_home") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(home) - - var buf bytes.Buffer - fc := fake.NewSimpleClientset(&v1beta1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: v1.NamespaceDefault, - Name: "tiller-deploy", - }, - }) - fc.PrependReactor("*", "*", func(action testcore.Action) (bool, runtime.Object, error) { - return true, nil, apierrors.NewAlreadyExists(v1.Resource("deployments"), "1") - }) - cmd := &initCmd{ - out: &buf, - home: helmpath.Home(home), - kubeClient: fc, - namespace: v1.NamespaceDefault, - } - if err := cmd.run(); err != nil { - t.Errorf("expected error: %v", err) - } - expected := "Warning: Tiller is already installed in the cluster.\n" + - "(Use --client-only to suppress this message, or --upgrade to upgrade Tiller to the current version.)" - if !strings.Contains(buf.String(), expected) { - t.Errorf("expected %q, got %q", expected, buf.String()) - } -} - -func TestInitCmd_clientOnly(t *testing.T) { - home, err := ioutil.TempDir("", "helm_home") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(home) - - var buf bytes.Buffer - fc := fake.NewSimpleClientset() - cmd := &initCmd{ - out: &buf, - home: helmpath.Home(home), - kubeClient: fc, - clientOnly: true, - namespace: v1.NamespaceDefault, - } - if err := cmd.run(); err != nil { - t.Errorf("unexpected error: %v", err) - } - if len(fc.Actions()) != 0 { - t.Error("expected client call") - } - expected := "Not installing Tiller due to 'client-only' flag having been set" - if !strings.Contains(buf.String(), expected) { - t.Errorf("expected %q, got %q", expected, buf.String()) - } -} - -func TestInitCmd_dryRun(t *testing.T) { - // This is purely defensive in this case. - home, err := ioutil.TempDir("", "helm_home") - if err != nil { - t.Fatal(err) - } - cleanup := resetEnv() - defer func() { - os.Remove(home) - cleanup() - }() - - settings.Debug = true - - var buf bytes.Buffer - fc := fake.NewSimpleClientset() - cmd := &initCmd{ - out: &buf, - home: helmpath.Home(home), - kubeClient: fc, - clientOnly: true, - dryRun: true, - namespace: v1.NamespaceDefault, - } - if err := cmd.run(); err != nil { - t.Fatal(err) - } - if got := len(fc.Actions()); got != 0 { - t.Errorf("expected no server calls, got %d", got) - } - - docs := bytes.Split(buf.Bytes(), []byte("\n---")) - if got, want := len(docs), 2; got != want { - t.Fatalf("Expected document count of %d, got %d", want, got) - } - for _, doc := range docs { - var y map[string]interface{} - if err := yaml.Unmarshal(doc, &y); err != nil { - t.Errorf("Expected parseable YAML, got %q\n\t%s", doc, err) - } - } -} - -func TestEnsureHome(t *testing.T) { - home, err := ioutil.TempDir("", "helm_home") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(home) - - b := bytes.NewBuffer(nil) - hh := helmpath.Home(home) - settings.Home = hh - if err := ensureDirectories(hh, b); err != nil { - t.Error(err) - } - if err := ensureDefaultRepos(hh, b, false); err != nil { - t.Error(err) - } - if err := ensureDefaultRepos(hh, b, true); err != nil { - t.Error(err) - } - if err := ensureRepoFileFormat(hh.RepositoryFile(), b); err != nil { - t.Error(err) - } - - expectedDirs := []string{hh.String(), hh.Repository(), hh.Cache(), hh.LocalRepository()} - for _, dir := range expectedDirs { - if fi, err := os.Stat(dir); err != nil { - t.Errorf("%s", err) - } else if !fi.IsDir() { - t.Errorf("%s is not a directory", fi) - } - } - - if fi, err := os.Stat(hh.RepositoryFile()); err != nil { - t.Error(err) - } else if fi.IsDir() { - t.Errorf("%s should not be a directory", fi) - } - - if fi, err := os.Stat(hh.LocalRepository(localRepositoryIndexFile)); err != nil { - t.Errorf("%s", err) - } else if fi.IsDir() { - t.Errorf("%s should not be a directory", fi) - } -} - -func TestInitCmd_tlsOptions(t *testing.T) { - const testDir = "../../testdata" - - // tls certificates in testDir - var ( - testCaCertFile = filepath.Join(testDir, "ca.pem") - testCertFile = filepath.Join(testDir, "crt.pem") - testKeyFile = filepath.Join(testDir, "key.pem") - ) - - // these tests verify the effects of permuting the "--tls" and "--tls-verify" flags - // and the install options yieled as a result of (*initCmd).tlsOptions() - // during helm init. - var tests = []struct { - certFile string - keyFile string - caFile string - enable bool - verify bool - describe string - }{ - { // --tls and --tls-verify specified (--tls=true,--tls-verify=true) - certFile: testCertFile, - keyFile: testKeyFile, - caFile: testCaCertFile, - enable: true, - verify: true, - describe: "--tls and --tls-verify specified (--tls=true,--tls-verify=true)", - }, - { // --tls-verify implies --tls (--tls=false,--tls-verify=true) - certFile: testCertFile, - keyFile: testKeyFile, - caFile: testCaCertFile, - enable: false, - verify: true, - describe: "--tls-verify implies --tls (--tls=false,--tls-verify=true)", - }, - { // no --tls-verify (--tls=true,--tls-verify=false) - certFile: testCertFile, - keyFile: testKeyFile, - caFile: "", - enable: true, - verify: false, - describe: "no --tls-verify (--tls=true,--tls-verify=false)", - }, - { // tls is disabled (--tls=false,--tls-verify=false) - certFile: "", - keyFile: "", - caFile: "", - enable: false, - verify: false, - describe: "tls is disabled (--tls=false,--tls-verify=false)", - }, - } - - for _, tt := range tests { - // emulate tls file specific flags - tlsCaCertFile, tlsCertFile, tlsKeyFile = tt.caFile, tt.certFile, tt.keyFile - - // emulate tls enable/verify flags - tlsEnable, tlsVerify = tt.enable, tt.verify - - cmd := &initCmd{} - if err := cmd.tlsOptions(); err != nil { - t.Fatalf("unexpected error: %v", err) - } - - // expected result options - expect := installer.Options{ - TLSCaCertFile: tt.caFile, - TLSCertFile: tt.certFile, - TLSKeyFile: tt.keyFile, - VerifyTLS: tt.verify, - EnableTLS: tt.enable || tt.verify, - } - - if !reflect.DeepEqual(cmd.opts, expect) { - t.Errorf("%s: got %#+v, want %#+v", tt.describe, cmd.opts, expect) - } - } -} - -// TestInitCmd_output tests that init -o formats are unmarshal-able -func TestInitCmd_output(t *testing.T) { - // This is purely defensive in this case. - home, err := ioutil.TempDir("", "helm_home") - if err != nil { - t.Fatal(err) - } - dbg := settings.Debug - settings.Debug = true - defer func() { - os.Remove(home) - settings.Debug = dbg - }() - fc := fake.NewSimpleClientset() - tests := []struct { - expectF func([]byte, interface{}) error - expectName string - }{ - { - json.Unmarshal, - "json", - }, - { - yaml.Unmarshal, - "yaml", - }, - } - for _, s := range tests { - var buf bytes.Buffer - cmd := &initCmd{ - out: &buf, - home: helmpath.Home(home), - kubeClient: fc, - opts: installer.Options{Output: installer.OutputFormat(s.expectName)}, - namespace: v1.NamespaceDefault, - } - if err := cmd.run(); err != nil { - t.Fatal(err) - } - if got := len(fc.Actions()); got != 0 { - t.Errorf("expected no server calls, got %d", got) - } - d := &v1beta1.Deployment{} - if err = s.expectF(buf.Bytes(), &d); err != nil { - t.Errorf("error unmarshalling init %s output %s %s", s.expectName, err, buf.String()) - } - } - -} diff --git a/cmd/helm/init_unix.go b/cmd/helm/init_unix.go deleted file mode 100644 index 1117dd487bd..00000000000 --- a/cmd/helm/init_unix.go +++ /dev/null @@ -1,29 +0,0 @@ -// +build !windows - -/* -Copyright 2017 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 ( - "os" - - "k8s.io/helm/pkg/helm/helmpath" -) - -func createLink(indexFile, cacheFile string, home helmpath.Home) error { - return os.Symlink(indexFile, cacheFile) -} diff --git a/cmd/helm/init_windows.go b/cmd/helm/init_windows.go deleted file mode 100644 index be17bccda3e..00000000000 --- a/cmd/helm/init_windows.go +++ /dev/null @@ -1,29 +0,0 @@ -// +build windows - -/* -Copyright 2017 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 ( - "os" - - "k8s.io/helm/pkg/helm/helmpath" -) - -func createLink(indexFile, cacheFile string, home helmpath.Home) error { - return os.Link(indexFile, cacheFile) -} diff --git a/cmd/helm/inspect.go b/cmd/helm/inspect.go deleted file mode 100644 index 9998569591f..00000000000 --- a/cmd/helm/inspect.go +++ /dev/null @@ -1,264 +0,0 @@ -/* -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" - "strings" - - "github.com/ghodss/yaml" - "github.com/golang/protobuf/ptypes/any" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/kubernetes/pkg/util/slice" -) - -const inspectDesc = ` -This command inspects a chart and displays information. It takes a chart reference -('stable/drupal'), a full path to a directory or packaged chart, or a URL. - -Inspect prints the contents of the Chart.yaml file and the values.yaml file. -` - -const inspectValuesDesc = ` -This command inspects a chart (directory, file, or URL) and displays the contents -of the values.yaml file -` - -const inspectChartDesc = ` -This command inspects a chart (directory, file, or URL) and displays the contents -of the Charts.yaml file -` - -const readmeChartDesc = ` -This command inspects a chart (directory, file, or URL) and displays the contents -of the README file -` - -type inspectCmd struct { - chartpath string - output string - verify bool - keyring string - out io.Writer - version string - repoURL string - username string - password string - - certFile string - keyFile string - caFile string -} - -const ( - chartOnly = "chart" - valuesOnly = "values" - readmeOnly = "readme" - all = "all" -) - -var readmeFileNames = []string{"readme.md", "readme.txt", "readme"} - -func newInspectCmd(out io.Writer) *cobra.Command { - insp := &inspectCmd{ - out: out, - output: all, - } - - inspectCommand := &cobra.Command{ - Use: "inspect [CHART]", - Short: "inspect a chart", - Long: inspectDesc, - RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(len(args), "chart name"); err != nil { - return err - } - cp, err := locateChartPath(insp.repoURL, insp.username, insp.password, args[0], insp.version, insp.verify, insp.keyring, - insp.certFile, insp.keyFile, insp.caFile) - if err != nil { - return err - } - insp.chartpath = cp - return insp.run() - }, - } - - valuesSubCmd := &cobra.Command{ - Use: "values [CHART]", - Short: "shows inspect values", - Long: inspectValuesDesc, - RunE: func(cmd *cobra.Command, args []string) error { - insp.output = valuesOnly - if err := checkArgsLength(len(args), "chart name"); err != nil { - return err - } - cp, err := locateChartPath(insp.repoURL, insp.username, insp.password, args[0], insp.version, insp.verify, insp.keyring, - insp.certFile, insp.keyFile, insp.caFile) - if err != nil { - return err - } - insp.chartpath = cp - return insp.run() - }, - } - - chartSubCmd := &cobra.Command{ - Use: "chart [CHART]", - Short: "shows inspect chart", - Long: inspectChartDesc, - RunE: func(cmd *cobra.Command, args []string) error { - insp.output = chartOnly - if err := checkArgsLength(len(args), "chart name"); err != nil { - return err - } - cp, err := locateChartPath(insp.repoURL, insp.username, insp.password, args[0], insp.version, insp.verify, insp.keyring, - insp.certFile, insp.keyFile, insp.caFile) - if err != nil { - return err - } - insp.chartpath = cp - return insp.run() - }, - } - - readmeSubCmd := &cobra.Command{ - Use: "readme [CHART]", - Short: "shows inspect readme", - Long: readmeChartDesc, - RunE: func(cmd *cobra.Command, args []string) error { - insp.output = readmeOnly - if err := checkArgsLength(len(args), "chart name"); err != nil { - return err - } - cp, err := locateChartPath(insp.repoURL, insp.username, insp.password, args[0], insp.version, insp.verify, insp.keyring, - insp.certFile, insp.keyFile, insp.caFile) - if err != nil { - return err - } - insp.chartpath = cp - return insp.run() - }, - } - - cmds := []*cobra.Command{inspectCommand, readmeSubCmd, valuesSubCmd, chartSubCmd} - vflag := "verify" - vdesc := "verify the provenance data for this chart" - for _, subCmd := range cmds { - subCmd.Flags().BoolVar(&insp.verify, vflag, false, vdesc) - } - - kflag := "keyring" - kdesc := "path to the keyring containing public verification keys" - kdefault := defaultKeyring() - for _, subCmd := range cmds { - subCmd.Flags().StringVar(&insp.keyring, kflag, kdefault, kdesc) - } - - verflag := "version" - verdesc := "version of the chart. By default, the newest chart is shown" - for _, subCmd := range cmds { - subCmd.Flags().StringVar(&insp.version, verflag, "", verdesc) - } - - repoURL := "repo" - repoURLdesc := "chart repository url where to locate the requested chart" - for _, subCmd := range cmds { - subCmd.Flags().StringVar(&insp.repoURL, repoURL, "", repoURLdesc) - } - - username := "username" - usernamedesc := "chart repository username where to locate the requested chart" - inspectCommand.Flags().StringVar(&insp.username, username, "", usernamedesc) - valuesSubCmd.Flags().StringVar(&insp.username, username, "", usernamedesc) - chartSubCmd.Flags().StringVar(&insp.username, username, "", usernamedesc) - - password := "password" - passworddesc := "chart repository password where to locate the requested chart" - inspectCommand.Flags().StringVar(&insp.password, password, "", passworddesc) - valuesSubCmd.Flags().StringVar(&insp.password, password, "", passworddesc) - chartSubCmd.Flags().StringVar(&insp.password, password, "", passworddesc) - - certFile := "cert-file" - certFiledesc := "verify certificates of HTTPS-enabled servers using this CA bundle" - for _, subCmd := range cmds { - subCmd.Flags().StringVar(&insp.certFile, certFile, "", certFiledesc) - } - - keyFile := "key-file" - keyFiledesc := "identify HTTPS client using this SSL key file" - for _, subCmd := range cmds { - subCmd.Flags().StringVar(&insp.keyFile, keyFile, "", keyFiledesc) - } - - caFile := "ca-file" - caFiledesc := "chart repository url where to locate the requested chart" - for _, subCmd := range cmds { - subCmd.Flags().StringVar(&insp.caFile, caFile, "", caFiledesc) - } - - for _, subCmd := range cmds[1:] { - inspectCommand.AddCommand(subCmd) - } - - return inspectCommand -} - -func (i *inspectCmd) run() error { - chrt, err := chartutil.Load(i.chartpath) - if err != nil { - return err - } - cf, err := yaml.Marshal(chrt.Metadata) - if err != nil { - return err - } - - if i.output == chartOnly || i.output == all { - fmt.Fprintln(i.out, string(cf)) - } - - if (i.output == valuesOnly || i.output == all) && chrt.Values != nil { - if i.output == all { - fmt.Fprintln(i.out, "---") - } - fmt.Fprintln(i.out, chrt.Values.Raw) - } - - if i.output == readmeOnly || i.output == all { - if i.output == all { - fmt.Fprintln(i.out, "---") - } - readme := findReadme(chrt.Files) - if readme == nil { - return nil - } - fmt.Fprintln(i.out, string(readme.Value)) - } - return nil -} - -func findReadme(files []*any.Any) (file *any.Any) { - for _, file := range files { - if slice.ContainsString(readmeFileNames, strings.ToLower(file.TypeUrl), nil) { - return file - } - } - return nil -} diff --git a/cmd/helm/inspect_test.go b/cmd/helm/inspect_test.go deleted file mode 100644 index 44d71fbbd4f..00000000000 --- a/cmd/helm/inspect_test.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "bytes" - "io/ioutil" - "strings" - "testing" -) - -func TestInspect(t *testing.T) { - b := bytes.NewBuffer(nil) - - insp := &inspectCmd{ - chartpath: "testdata/testcharts/alpine", - output: all, - out: b, - } - insp.run() - - // Load the data from the textfixture directly. - cdata, err := ioutil.ReadFile("testdata/testcharts/alpine/Chart.yaml") - if err != nil { - t.Fatal(err) - } - data, err := ioutil.ReadFile("testdata/testcharts/alpine/values.yaml") - if err != nil { - t.Fatal(err) - } - readmeData, err := ioutil.ReadFile("testdata/testcharts/alpine/README.md") - if err != nil { - t.Fatal(err) - } - parts := strings.SplitN(b.String(), "---", 3) - if len(parts) != 3 { - t.Fatalf("Expected 2 parts, got %d", len(parts)) - } - - expect := []string{ - strings.Replace(strings.TrimSpace(string(cdata)), "\r", "", -1), - strings.Replace(strings.TrimSpace(string(data)), "\r", "", -1), - strings.Replace(strings.TrimSpace(string(readmeData)), "\r", "", -1), - } - - // Problem: ghodss/yaml doesn't marshal into struct order. To solve, we - // have to carefully craft the Chart.yaml to match. - for i, got := range parts { - got = strings.Replace(strings.TrimSpace(got), "\r", "", -1) - if got != expect[i] { - t.Errorf("Expected\n%q\nGot\n%q\n", expect[i], got) - } - } - - // Regression tests for missing values. See issue #1024. - b.Reset() - insp = &inspectCmd{ - chartpath: "testdata/testcharts/novals", - output: "values", - out: b, - } - insp.run() - if b.Len() != 0 { - t.Errorf("expected empty values buffer, got %q", b.String()) - } -} diff --git a/cmd/helm/install.go b/cmd/helm/install.go index d52dbc6677b..fac2131c1ee 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,30 +17,23 @@ limitations under the License. package main import ( - "bytes" - "errors" - "fmt" "io" - "io/ioutil" - "net/url" - "os" - "path/filepath" - "strings" - "text/template" - - "github.com/Masterminds/sprig" - "github.com/ghodss/yaml" - "github.com/spf13/cobra" + "log" + "time" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/downloader" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/kube" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/repo" - "k8s.io/helm/pkg/strvals" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/cli/output" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/release" ) const installDesc = ` @@ -51,475 +44,223 @@ a path to an unpacked chart directory or a URL. To override values in a chart, use either the '--values' flag and pass in a file or use the '--set' flag and pass configuration from the command line, to force -a string value use '--set-string'. +a string value use '--set-string'. In case a value is large and therefore +you want not to use neither '--values' nor '--set', use '--set-file' to read the +single large value from file. + + $ helm install -f myvalues.yaml myredis ./redis + +or - $ helm install -f myvalues.yaml ./redis + $ helm install --set name=prod myredis ./redis or - $ helm install --set name=prod ./redis + $ helm install --set-string long_int=1234567890 myredis ./redis or - $ helm install --set-string long_int=1234567890 ./redis + $ helm install --set-file my_script=dothings.sh myredis ./redis You can specify the '--values'/'-f' flag multiple times. The priority will be given to the last (right-most) file specified. For example, if both myvalues.yaml and override.yaml contained a key called 'Test', the value set in override.yaml would take precedence: - $ helm install -f myvalues.yaml -f override.yaml ./redis + $ helm install -f myvalues.yaml -f override.yaml myredis ./redis You can specify the '--set' flag multiple times. The priority will be given to the last (right-most) set specified. For example, if both 'bar' and 'newbar' values are set for a key called 'foo', the 'newbar' value would take precedence: - $ helm install --set foo=bar --set foo=newbar ./redis + $ helm install --set foo=bar --set foo=newbar myredis ./redis To check the generated manifests of a release without installing the chart, -the '--debug' and '--dry-run' flags can be combined. This will still require a -round-trip to the Tiller server. +the '--debug' and '--dry-run' flags can be combined. If --verify is set, the chart MUST have a provenance file, and the provenance file MUST pass all verification steps. There are five different ways you can express the chart you want to install: -1. By chart reference: helm install stable/mariadb -2. By path to a packaged chart: helm install ./nginx-1.2.3.tgz -3. By path to an unpacked chart directory: helm install ./nginx -4. By absolute URL: helm install https://example.com/charts/nginx-1.2.3.tgz -5. By chart reference and repo url: helm install --repo https://example.com/charts/ nginx +1. By chart reference: helm install mymaria example/mariadb +2. By path to a packaged chart: helm install mynginx ./nginx-1.2.3.tgz +3. By path to an unpacked chart directory: helm install mynginx ./nginx +4. By absolute URL: helm install mynginx https://example.com/charts/nginx-1.2.3.tgz +5. By chart reference and repo url: helm install --repo https://example.com/charts/ mynginx nginx CHART REFERENCES -A chart reference is a convenient way of reference a chart in a chart repository. +A chart reference is a convenient way of referencing a chart in a chart repository. -When you use a chart reference with a repo prefix ('stable/mariadb'), Helm will look in the local -configuration for a chart repository named 'stable', and will then look for a -chart in that repository whose name is 'mariadb'. It will install the latest -version of that chart unless you also supply a version number with the -'--version' flag. +When you use a chart reference with a repo prefix ('example/mariadb'), Helm will look in the local +configuration for a chart repository named 'example', and will then look for a +chart in that repository whose name is 'mariadb'. It will install the latest stable version of that chart +until you specify '--devel' flag to also include development version (alpha, beta, and release candidate releases), or +supply a version number with the '--version' flag. To see the list of chart repositories, use 'helm repo list'. To search for charts in a repository, use 'helm search'. ` -type installCmd struct { - name string - namespace string - valueFiles valueFiles - chartPath string - dryRun bool - disableHooks bool - replace bool - verify bool - keyring string - out io.Writer - client helm.Interface - values []string - stringValues []string - nameTemplate string - version string - timeout int64 - wait bool - repoURL string - username string - password string - devel bool - depUp bool - - certFile string - keyFile string - caFile string -} - -type valueFiles []string - -func (v *valueFiles) String() string { - return fmt.Sprint(*v) -} - -func (v *valueFiles) Type() string { - return "valueFiles" -} - -func (v *valueFiles) Set(value string) error { - for _, filePath := range strings.Split(value, ",") { - *v = append(*v, filePath) - } - return nil -} - -func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { - inst := &installCmd{ - out: out, - client: c, - } +func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewInstall(cfg) + valueOpts := &values.Options{} + var outfmt output.Format cmd := &cobra.Command{ - Use: "install [CHART]", - Short: "install a chart archive", - Long: installDesc, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, - RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(len(args), "chart name"); err != nil { - return err - } - - debug("Original chart version: %q", inst.version) - if inst.version == "" && inst.devel { - debug("setting version to >0.0.0-0") - inst.version = ">0.0.0-0" - } - - cp, err := locateChartPath(inst.repoURL, inst.username, inst.password, args[0], inst.version, inst.verify, inst.keyring, - inst.certFile, inst.keyFile, inst.caFile) + Use: "install [NAME] [CHART]", + Short: "install a chart", + Long: installDesc, + Args: require.MinimumNArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compInstall(args, toComplete, client) + }, + RunE: func(_ *cobra.Command, args []string) error { + rel, err := runInstall(args, client, valueOpts, out) if err != nil { return err } - inst.chartPath = cp - inst.client = ensureHelmClient(inst.client) - return inst.run() + + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) }, } - f := cmd.Flags() - f.VarP(&inst.valueFiles, "values", "f", "specify values in a YAML file or a URL(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. 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") - f.StringArrayVar(&inst.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - f.StringArrayVar(&inst.stringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release") - f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it") - f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification") - f.StringVar(&inst.version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed") - f.Int64Var(&inst.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") - f.BoolVar(&inst.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") - f.StringVar(&inst.repoURL, "repo", "", "chart repository url where to locate the requested chart") - f.StringVar(&inst.username, "username", "", "chart repository username where to locate the requested chart") - f.StringVar(&inst.password, "password", "", "chart repository password where to locate the requested chart") - f.StringVar(&inst.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") - f.StringVar(&inst.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") - f.StringVar(&inst.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") - f.BoolVar(&inst.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") - f.BoolVar(&inst.depUp, "dep-up", false, "run helm dependency update before installing the chart") + addInstallFlags(cmd, cmd.Flags(), client, valueOpts) + bindOutputFlag(cmd, &outfmt) + bindPostRenderFlag(cmd, &client.PostRenderer) return cmd } -func (i *installCmd) run() error { - debug("CHART PATH: %s\n", i.chartPath) - - if i.namespace == "" { - i.namespace = defaultNamespace() - } - - rawVals, err := vals(i.valueFiles, i.values, i.stringValues) - if err != nil { - return err - } - - // If template is specified, try to run the template. - if i.nameTemplate != "" { - i.name, err = generateName(i.nameTemplate) - if err != nil { - return err +func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { + f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present") + f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install") + f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install") + f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production") + f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout") + f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout") + f.BoolVarP(&client.GenerateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)") + f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release") + f.StringVar(&client.Description, "description", "", "add a custom description") + f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") + f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "run helm dependency update before installing the chart") + f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema") + f.BoolVar(&client.Atomic, "atomic", false, "if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used") + f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present") + f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent") + addValueOptionsFlags(f, valueOpts) + addChartPathOptionsFlags(f, &client.ChartPathOptions) + + err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + requiredArgs := 2 + if client.GenerateName { + requiredArgs = 1 } - // Print the final name so the user knows what the final name of the release is. - fmt.Printf("FINAL NAME: %s\n", i.name) - } - - // Check chart requirements to make sure all dependencies are present in /charts - chartRequested, err := chartutil.Load(i.chartPath) - if err != nil { - return prettyError(err) - } - - if req, err := chartutil.LoadRequirements(chartRequested); err == nil { - // If checkDependencies returns an error, we have unfulfilled dependencies. - // As of Helm 2.4.0, this is treated as a stopping condition: - // https://github.com/kubernetes/helm/issues/2209 - if err := checkDependencies(chartRequested, req); err != nil { - if i.depUp { - man := &downloader.Manager{ - Out: i.out, - ChartPath: i.chartPath, - HelmHome: settings.Home, - Keyring: defaultKeyring(), - SkipUpdate: false, - Getters: getter.All(settings), - } - if err := man.Update(); err != nil { - return prettyError(err) - } - } else { - return prettyError(err) - } - + if len(args) != requiredArgs { + return nil, cobra.ShellCompDirectiveNoFileComp } - } else if err != chartutil.ErrRequirementsNotFound { - return fmt.Errorf("cannot load requirements: %v", err) - } + return compVersionFlag(args[requiredArgs-1], toComplete) + }) - res, err := i.client.InstallReleaseFromChart( - chartRequested, - i.namespace, - helm.ValueOverrides(rawVals), - helm.ReleaseName(i.name), - helm.InstallDryRun(i.dryRun), - helm.InstallReuseName(i.replace), - helm.InstallDisableHooks(i.disableHooks), - helm.InstallTimeout(i.timeout), - helm.InstallWait(i.wait)) if err != nil { - return prettyError(err) + log.Fatal(err) } +} - rel := res.GetRelease() - if rel == nil { - return nil - } - i.printRelease(rel) - - // If this is a dry run, we can't display status. - if i.dryRun { - return nil +func runInstall(args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) { + debug("Original chart version: %q", client.Version) + if client.Version == "" && client.Devel { + debug("setting version to >0.0.0-0") + client.Version = ">0.0.0-0" } - // Print the status like status command does - status, err := i.client.ReleaseStatus(rel.Name) + name, chart, err := client.NameAndChart(args) if err != nil { - return prettyError(err) + return nil, err } - PrintStatus(i.out, status) - return nil -} + client.ReleaseName = name -// Merges source and destination map, preferring values from the source map -func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} { - for k, v := range src { - // If the key doesn't exist already, then just set the key to that value - if _, exists := dest[k]; !exists { - dest[k] = v - continue - } - nextMap, ok := v.(map[string]interface{}) - // If it isn't another map, overwrite the value - if !ok { - dest[k] = v - continue - } - // Edge case: If the key exists in the destination, but isn't a map - destMap, isMap := dest[k].(map[string]interface{}) - // If the source map has a map for this key, prefer it - if !isMap { - dest[k] = v - continue - } - // If we got to this point, it is a map in both, so merge them - dest[k] = mergeValues(destMap, nextMap) + cp, err := client.ChartPathOptions.LocateChart(chart, settings) + if err != nil { + return nil, err } - return dest -} -// vals merges values from files specified via -f/--values and -// directly via --set or --set-string, marshaling them to YAML -func vals(valueFiles valueFiles, values []string, stringValues []string) ([]byte, error) { - base := map[string]interface{}{} - - // User specified a values files via -f/--values - for _, filePath := range valueFiles { - currentMap := map[string]interface{}{} - - var bytes []byte - var err error - if strings.TrimSpace(filePath) == "-" { - bytes, err = ioutil.ReadAll(os.Stdin) - } else { - bytes, err = readFile(filePath) - } + debug("CHART PATH: %s\n", cp) - if err != nil { - return []byte{}, err - } - - if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { - return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) - } - // Merge with the previous map - base = mergeValues(base, currentMap) + p := getter.All(settings) + vals, err := valueOpts.MergeValues(p) + if err != nil { + return nil, err } - // User specified a value via --set - for _, value := range values { - if err := strvals.ParseInto(value, base); err != nil { - return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) - } + // Check chart dependencies to make sure all are present in /charts + chartRequested, err := loader.Load(cp) + if err != nil { + return nil, err } - // User specified a value via --set-string - for _, value := range stringValues { - if err := strvals.ParseIntoString(value, base); err != nil { - return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err) - } + if err := checkIfInstallable(chartRequested); err != nil { + return nil, err } - return yaml.Marshal(base) -} - -// printRelease prints info about a release if the Debug is true. -func (i *installCmd) printRelease(rel *release.Release) { - if rel == nil { - return - } - // TODO: Switch to text/template like everything else. - fmt.Fprintf(i.out, "NAME: %s\n", rel.Name) - if settings.Debug { - printRelease(i.out, rel) + if chartRequested.Metadata.Deprecated { + warning("This chart is deprecated") } -} -// locateChartPath looks for a chart directory in known places, and returns either the full path or an error. -// -// This does not ensure that the chart is well-formed; only that the requested filename exists. -// -// Order of resolution: -// - current working directory -// - if path is absolute or begins with '.', error out here -// - chart repos in $HELM_HOME -// - URL -// -// If 'verify' is true, this will attempt to also verify the chart. -func locateChartPath(repoURL, username, password, name, version string, verify bool, keyring, - certFile, keyFile, caFile string) (string, error) { - name = strings.TrimSpace(name) - version = strings.TrimSpace(version) - if fi, err := os.Stat(name); err == nil { - abs, err := filepath.Abs(name) - if err != nil { - return abs, err - } - if verify { - if fi.IsDir() { - return "", errors.New("cannot verify a directory") - } - if _, err := downloader.VerifyChart(abs, keyring); err != nil { - return "", err + if req := chartRequested.Metadata.Dependencies; req != nil { + // If CheckDependencies returns an error, we have unfulfilled dependencies. + // As of Helm 2.4.0, this is treated as a stopping condition: + // https://github.com/helm/helm/issues/2209 + if err := action.CheckDependencies(chartRequested, req); err != nil { + if client.DependencyUpdate { + man := &downloader.Manager{ + Out: out, + ChartPath: cp, + Keyring: client.ChartPathOptions.Keyring, + SkipUpdate: false, + Getters: p, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + Debug: settings.Debug, + } + if err := man.Update(); err != nil { + return nil, err + } + // Reload the chart with the updated Chart.lock file. + if chartRequested, err = loader.Load(cp); err != nil { + return nil, errors.Wrap(err, "failed reloading chart after repo update") + } + } else { + return nil, err } } - return abs, nil - } - if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { - return name, fmt.Errorf("path %q not found", name) - } - - crepo := filepath.Join(settings.Home.Repository(), name) - if _, err := os.Stat(crepo); err == nil { - return filepath.Abs(crepo) - } - - dl := downloader.ChartDownloader{ - HelmHome: settings.Home, - Out: os.Stdout, - Keyring: keyring, - Getters: getter.All(settings), - Username: username, - Password: password, - } - if verify { - dl.Verify = downloader.VerifyAlways - } - if repoURL != "" { - chartURL, err := repo.FindChartInAuthRepoURL(repoURL, username, password, name, version, - certFile, keyFile, caFile, getter.All(settings)) - if err != nil { - return "", err - } - name = chartURL } - if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) { - os.MkdirAll(settings.Home.Archive(), 0744) - } - - filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive()) - if err == nil { - lname, err := filepath.Abs(filename) - if err != nil { - return filename, err - } - debug("Fetched %s to %s\n", name, filename) - return lname, nil - } else if settings.Debug { - return filename, err - } - - return filename, fmt.Errorf("failed to download %q (hint: running `helm repo update` may help)", name) -} - -func generateName(nameTemplate string) (string, error) { - t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) - if err != nil { - return "", err - } - var b bytes.Buffer - err = t.Execute(&b, nil) - if err != nil { - return "", err - } - return b.String(), nil -} - -func defaultNamespace() string { - if ns, _, err := kube.GetConfig(settings.KubeContext).Namespace(); err == nil { - return ns - } - return "default" + client.Namespace = settings.Namespace() + return client.Run(chartRequested, vals) } -func checkDependencies(ch *chart.Chart, reqs *chartutil.Requirements) error { - missing := []string{} - - deps := ch.GetDependencies() - for _, r := range reqs.Dependencies { - found := false - for _, d := range deps { - if d.Metadata.Name == r.Name { - found = true - break - } - } - if !found { - missing = append(missing, r.Name) - } - } - - if len(missing) > 0 { - return fmt.Errorf("found in requirements.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) +// checkIfInstallable validates if a chart can be installed +// +// Application chart type is only installable +func checkIfInstallable(ch *chart.Chart) error { + switch ch.Metadata.Type { + case "", "application": + return nil } - return nil + return errors.Errorf("%s charts are not installable", ch.Metadata.Type) } -//readFile load a file from the local directory or a remote file with a url. -func readFile(filePath string) ([]byte, error) { - u, _ := url.Parse(filePath) - p := getter.All(settings) - - // FIXME: maybe someone handle other protocols like ftp. - getterConstructor, err := p.ByScheme(u.Scheme) - - if err != nil { - return ioutil.ReadFile(filePath) +// Provide dynamic auto-completion for the install and template commands +func compInstall(args []string, toComplete string, client *action.Install) ([]string, cobra.ShellCompDirective) { + requiredArgs := 1 + if client.GenerateName { + requiredArgs = 0 } - - getter, err := getterConstructor(filePath, "", "", "") - if err != nil { - return []byte{}, err + if len(args) == requiredArgs { + return compListCharts(toComplete, true) } - data, err := getter.Get(filePath) - return data.Bytes(), err + return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index aa828c6ce51..0fae7953441 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,267 +17,238 @@ limitations under the License. package main import ( - "io" - "reflect" - "regexp" - "strings" + "fmt" "testing" - - "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" ) func TestInstall(t *testing.T) { - tests := []releaseCase{ + tests := []cmdTestCase{ // Install, base case { - name: "basic install", - args: []string{"testdata/testcharts/alpine"}, - flags: strings.Split("--name aeneas", " "), - expected: "aeneas", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), - }, - // Install, no hooks - { - name: "install without hooks", - args: []string{"testdata/testcharts/alpine"}, - flags: strings.Split("--name aeneas --no-hooks", " "), - expected: "aeneas", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), + name: "basic install", + cmd: "install aeneas testdata/testcharts/empty --namespace default", + golden: "output/install.txt", }, + // Install, values from cli { - name: "install with values", - args: []string{"testdata/testcharts/alpine"}, - flags: strings.Split("--name virgil --set foo=bar", " "), - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "virgil"}), - expected: "virgil", + name: "install with values", + cmd: "install virgil testdata/testcharts/alpine --set test.Name=bar", + golden: "output/install-with-values.txt", }, // Install, values from cli via multiple --set { - name: "install with multiple values", - args: []string{"testdata/testcharts/alpine"}, - flags: strings.Split("--name virgil --set foo=bar --set bar=foo", " "), - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "virgil"}), - expected: "virgil", + name: "install with multiple values", + cmd: "install virgil testdata/testcharts/alpine --set test.Color=yellow --set test.Name=banana", + golden: "output/install-with-multiple-values.txt", }, // Install, values from yaml { - name: "install with values", - args: []string{"testdata/testcharts/alpine"}, - flags: strings.Split("--name virgil -f testdata/testcharts/alpine/extra_values.yaml", " "), - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "virgil"}), - expected: "virgil", + name: "install with values file", + cmd: "install virgil testdata/testcharts/alpine -f testdata/testcharts/alpine/extra_values.yaml", + golden: "output/install-with-values-file.txt", + }, + // Install, no hooks + { + name: "install without hooks", + cmd: "install aeneas testdata/testcharts/alpine --no-hooks --set test.Name=hello", + golden: "output/install-no-hooks.txt", }, // Install, values from multiple yaml { - name: "install with values", - args: []string{"testdata/testcharts/alpine"}, - flags: strings.Split("--name virgil -f testdata/testcharts/alpine/extra_values.yaml -f testdata/testcharts/alpine/more_values.yaml", " "), - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "virgil"}), - expected: "virgil", + name: "install with values", + cmd: "install virgil testdata/testcharts/alpine -f testdata/testcharts/alpine/extra_values.yaml -f testdata/testcharts/alpine/more_values.yaml", + golden: "output/install-with-multiple-values-files.txt", }, // Install, no charts { - name: "install with no chart specified", - args: []string{}, - err: true, + name: "install with no chart specified", + cmd: "install", + golden: "output/install-no-args.txt", + wantError: true, }, // Install, re-use name { - name: "install and replace release", - args: []string{"testdata/testcharts/alpine"}, - flags: strings.Split("--name aeneas --replace", " "), - expected: "aeneas", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "aeneas"}), + name: "install and replace release", + cmd: "install aeneas testdata/testcharts/empty --replace", + golden: "output/install-and-replace.txt", }, // Install, with timeout { - name: "install with a timeout", - args: []string{"testdata/testcharts/alpine"}, - flags: strings.Split("--name foobar --timeout 120", " "), - expected: "foobar", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "foobar"}), + name: "install with a timeout", + cmd: "install foobar testdata/testcharts/empty --timeout 120s", + golden: "output/install-with-timeout.txt", }, // Install, with wait { - name: "install with a wait", - args: []string{"testdata/testcharts/alpine"}, - flags: strings.Split("--name apollo --wait", " "), - expected: "apollo", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "apollo"}), + name: "install with a wait", + cmd: "install apollo testdata/testcharts/empty --wait", + golden: "output/install-with-wait.txt", + }, + // Install, with wait-for-jobs + { + name: "install with wait-for-jobs", + cmd: "install apollo testdata/testcharts/empty --wait --wait-for-jobs", + golden: "output/install-with-wait-for-jobs.txt", }, // Install, using the name-template { - name: "install with name-template", - args: []string{"testdata/testcharts/alpine"}, - flags: []string{"--name-template", "{{upper \"foobar\"}}"}, - expected: "FOOBAR", - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "FOOBAR"}), + name: "install with name-template", + cmd: "install testdata/testcharts/empty --name-template '{{upper \"foobar\"}}'", + golden: "output/install-name-template.txt", }, // Install, perform chart verification along the way. { - name: "install with verification, missing provenance", - args: []string{"testdata/testcharts/compressedchart-0.1.0.tgz"}, - flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "), - err: true, + name: "install with verification, missing provenance", + cmd: "install bogus testdata/testcharts/compressedchart-0.1.0.tgz --verify --keyring testdata/helm-test-key.pub", + wantError: true, }, { - name: "install with verification, directory instead of file", - args: []string{"testdata/testcharts/signtest"}, - flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "), - err: true, + name: "install with verification, directory instead of file", + cmd: "install bogus testdata/testcharts/signtest --verify --keyring testdata/helm-test-key.pub", + wantError: true, }, { - name: "install with verification, valid", - args: []string{"testdata/testcharts/signtest-0.1.0.tgz"}, - flags: strings.Split("--verify --keyring testdata/helm-test-key.pub", " "), + name: "install with verification, valid", + cmd: "install signtest testdata/testcharts/signtest-0.1.0.tgz --verify --keyring testdata/helm-test-key.pub", }, // Install, chart with missing dependencies in /charts { - name: "install chart with missing dependencies", - args: []string{"testdata/testcharts/chart-missing-deps"}, - err: true, + name: "install chart with missing dependencies", + cmd: "install nodeps testdata/testcharts/chart-missing-deps", + wantError: true, }, - // Install, chart with bad requirements.yaml in /charts + // Install chart with update-dependency { - name: "install chart with bad requirements.yaml", - args: []string{"testdata/testcharts/chart-bad-requirements"}, - err: true, + name: "install chart with missing dependencies", + cmd: "install --dependency-update updeps testdata/testcharts/chart-with-subchart-update", + golden: "output/chart-with-subchart-update.txt", }, - } - - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newInstallCmd(c, out) - }) -} - -type nameTemplateTestCase struct { - tpl string - expected string - expectedErrorStr string -} - -func TestNameTemplate(t *testing.T) { - testCases := []nameTemplateTestCase{ - // Just a straight up nop please + // Install, chart with bad dependencies in Chart.yaml in /charts { - tpl: "foobar", - expected: "foobar", - expectedErrorStr: "", + name: "install chart with bad dependencies in Chart.yaml", + cmd: "install badreq testdata/testcharts/chart-bad-requirements", + wantError: true, }, - // Random numbers at the end for fun & profit + // Install, chart with library chart dependency { - tpl: "foobar-{{randNumeric 6}}", - expected: "foobar-[0-9]{6}$", - expectedErrorStr: "", + name: "install chart with library chart dependency", + cmd: "install withlibchartp testdata/testcharts/chart-with-lib-dep", }, - // Random numbers in the middle for fun & profit + // Install, library chart { - tpl: "foobar-{{randNumeric 4}}-baz", - expected: "foobar-[0-9]{4}-baz$", - expectedErrorStr: "", + name: "install library chart", + cmd: "install libchart testdata/testcharts/lib-chart", + wantError: true, + golden: "output/template-lib-chart.txt", }, - // No such function + // Install, chart with bad type { - tpl: "foobar-{{randInt}}", - expected: "", - expectedErrorStr: "function \"randInt\" not defined", + name: "install chart with bad type", + cmd: "install badtype testdata/testcharts/chart-bad-type", + wantError: true, + golden: "output/install-chart-bad-type.txt", }, - // Invalid template + // Install, values from yaml, schematized { - tpl: "foobar-{{", - expected: "", - expectedErrorStr: "unexpected unclosed action", + name: "install with schema file", + cmd: "install schema testdata/testcharts/chart-with-schema", + golden: "output/schema.txt", }, - } - - for _, tc := range testCases { - - n, err := generateName(tc.tpl) - if err != nil { - if tc.expectedErrorStr == "" { - t.Errorf("Was not expecting error, but got: %v", err) - continue - } - re, compErr := regexp.Compile(tc.expectedErrorStr) - if compErr != nil { - t.Errorf("Expected error string failed to compile: %v", compErr) - continue - } - if !re.MatchString(err.Error()) { - t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err) - continue - } - } - if err == nil && tc.expectedErrorStr != "" { - t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr) - } - - if tc.expected != "" { - re, err := regexp.Compile(tc.expected) - if err != nil { - t.Errorf("Expected string failed to compile: %v", err) - continue - } - if !re.MatchString(n) { - t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n) - } - } - } -} - -func TestMergeValues(t *testing.T) { - nestedMap := map[string]interface{}{ - "foo": "bar", - "baz": map[string]string{ - "cool": "stuff", + // Install, values from yaml, schematized with errors + { + name: "install with schema file, with errors", + cmd: "install schema testdata/testcharts/chart-with-schema-negative", + wantError: true, + golden: "output/schema-negative.txt", }, - } - anotherNestedMap := map[string]interface{}{ - "foo": "bar", - "baz": map[string]string{ - "cool": "things", - "awesome": "stuff", + // Install, values from yaml, extra values from yaml, schematized with errors + { + name: "install with schema file, extra values from yaml, with errors", + cmd: "install schema testdata/testcharts/chart-with-schema -f testdata/testcharts/chart-with-schema/extra-values.yaml", + wantError: true, + golden: "output/schema-negative.txt", + }, + // Install, values from yaml, extra values from cli, schematized with errors + { + name: "install with schema file, extra values from cli, with errors", + cmd: "install schema testdata/testcharts/chart-with-schema --set age=-5", + wantError: true, + golden: "output/schema-negative-cli.txt", + }, + // Install with subchart, values from yaml, schematized with errors + { + name: "install with schema file and schematized subchart, with errors", + cmd: "install schema testdata/testcharts/chart-with-schema-and-subchart", + wantError: true, + golden: "output/subchart-schema-negative.txt", + }, + // Install with subchart, values from yaml, extra values from cli, schematized with errors + { + name: "install with schema file and schematized subchart, extra values from cli", + cmd: "install schema testdata/testcharts/chart-with-schema-and-subchart --set lastname=doe --set subchart-with-schema.age=25", + golden: "output/subchart-schema-cli.txt", + }, + // Install with subchart, values from yaml, extra values from cli, schematized with errors + { + name: "install with schema file and schematized subchart, extra values from cli, with errors", + cmd: "install schema testdata/testcharts/chart-with-schema-and-subchart --set lastname=doe --set subchart-with-schema.age=-25", + wantError: true, + golden: "output/subchart-schema-cli-negative.txt", + }, + // Install deprecated chart + { + name: "install with warning about deprecated chart", + cmd: "install aeneas testdata/testcharts/deprecated --namespace default", + golden: "output/deprecated-chart.txt", + }, + // Install chart with only crds + { + name: "install chart with only crds", + cmd: "install crd-test testdata/testcharts/chart-with-only-crds --namespace default", }, - } - flatMap := map[string]interface{}{ - "foo": "bar", - "baz": "stuff", - } - anotherFlatMap := map[string]interface{}{ - "testing": "fun", } - testMap := mergeValues(flatMap, nestedMap) - equal := reflect.DeepEqual(testMap, nestedMap) - if !equal { - t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap) - } + runTestActionCmd(t, tests) +} - testMap = mergeValues(nestedMap, flatMap) - equal = reflect.DeepEqual(testMap, flatMap) - if !equal { - t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap) - } +func TestInstallOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "install") +} - testMap = mergeValues(nestedMap, anotherNestedMap) - equal = reflect.DeepEqual(testMap, anotherNestedMap) - if !equal { - t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap) - } +func TestInstallVersionCompletion(t *testing.T) { + repoFile := "testdata/helmhome/helm/repositories.yaml" + repoCache := "testdata/helmhome/helm/repository" + + repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache) + + tests := []cmdTestCase{{ + name: "completion for install version flag with release name", + cmd: fmt.Sprintf("%s __complete install releasename testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for install version flag with generate-name", + cmd: fmt.Sprintf("%s __complete install --generate-name testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for install version flag too few args", + cmd: fmt.Sprintf("%s __complete install testing/alpine --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for install version flag too many args", + cmd: fmt.Sprintf("%s __complete install releasename testing/alpine badarg --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for install version flag invalid chart", + cmd: fmt.Sprintf("%s __complete install releasename invalid/invalid --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }} + runTestCmd(t, tests) +} - testMap = mergeValues(anotherFlatMap, anotherNestedMap) - expectedMap := map[string]interface{}{ - "testing": "fun", - "foo": "bar", - "baz": map[string]string{ - "cool": "things", - "awesome": "stuff", - }, - } - equal = reflect.DeepEqual(testMap, expectedMap) - if !equal { - t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) - } +func TestInstallFileCompletion(t *testing.T) { + checkFileCompletion(t, "install", false) + checkFileCompletion(t, "install --generate-name", true) + checkFileCompletion(t, "install myname", true) + checkFileCompletion(t, "install myname mychart", false) } diff --git a/cmd/helm/installer/install.go b/cmd/helm/installer/install.go deleted file mode 100644 index a45179a48ce..00000000000 --- a/cmd/helm/installer/install.go +++ /dev/null @@ -1,374 +0,0 @@ -/* -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 installer // import "k8s.io/helm/cmd/helm/installer" - -import ( - "errors" - "fmt" - "io/ioutil" - "strings" - - "github.com/Masterminds/semver" - "github.com/ghodss/yaml" - "k8s.io/api/core/v1" - "k8s.io/api/extensions/v1beta1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/client-go/kubernetes" - corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - extensionsclient "k8s.io/client-go/kubernetes/typed/extensions/v1beta1" - "k8s.io/helm/pkg/version" - - "k8s.io/helm/pkg/chartutil" -) - -// Install uses Kubernetes client to install Tiller. -// -// Returns an error if the command failed. -func Install(client kubernetes.Interface, opts *Options) error { - if err := createDeployment(client.ExtensionsV1beta1(), opts); err != nil { - return err - } - if err := createService(client.CoreV1(), opts.Namespace); err != nil { - return err - } - if opts.tls() { - if err := createSecret(client.CoreV1(), opts); err != nil { - return err - } - } - return nil -} - -// Upgrade uses Kubernetes client to upgrade Tiller to current version. -// -// Returns an error if the command failed. -func Upgrade(client kubernetes.Interface, opts *Options) error { - obj, err := client.ExtensionsV1beta1().Deployments(opts.Namespace).Get(deploymentName, metav1.GetOptions{}) - if err != nil { - return err - } - tillerImage := obj.Spec.Template.Spec.Containers[0].Image - if semverCompare(tillerImage) == -1 && !opts.ForceUpgrade { - return errors.New("current Tiller version is newer, use --force-upgrade to downgrade") - } - obj.Spec.Template.Spec.Containers[0].Image = opts.selectImage() - obj.Spec.Template.Spec.Containers[0].ImagePullPolicy = opts.pullPolicy() - obj.Spec.Template.Spec.ServiceAccountName = opts.ServiceAccount - if _, err := client.ExtensionsV1beta1().Deployments(opts.Namespace).Update(obj); err != nil { - return err - } - // If the service does not exists that would mean we are upgrading from a Tiller version - // that didn't deploy the service, so install it. - _, err = client.CoreV1().Services(opts.Namespace).Get(serviceName, metav1.GetOptions{}) - if apierrors.IsNotFound(err) { - return createService(client.CoreV1(), opts.Namespace) - } - return err -} - -// semverCompare returns whether the client's version is older, equal or newer than the given image's version. -func semverCompare(image string) int { - split := strings.Split(image, ":") - if len(split) < 2 { - // If we don't know the version, we consider the client version newer. - return 1 - } - tillerVersion, err := semver.NewVersion(split[1]) - if err != nil { - // same thing with unparsable tiller versions (e.g. canary releases). - return 1 - } - clientVersion, err := semver.NewVersion(version.Version) - if err != nil { - // aaaaaand same thing with unparsable helm versions (e.g. canary releases). - return 1 - } - return clientVersion.Compare(tillerVersion) -} - -// createDeployment creates the Tiller Deployment resource. -func createDeployment(client extensionsclient.DeploymentsGetter, opts *Options) error { - obj, err := deployment(opts) - if err != nil { - return err - } - _, err = client.Deployments(obj.Namespace).Create(obj) - return err - -} - -// deployment gets the deployment object that installs Tiller. -func deployment(opts *Options) (*v1beta1.Deployment, error) { - return generateDeployment(opts) -} - -// createService creates the Tiller service resource -func createService(client corev1.ServicesGetter, namespace string) error { - obj := service(namespace) - _, err := client.Services(obj.Namespace).Create(obj) - return err -} - -// service gets the service object that installs Tiller. -func service(namespace string) *v1.Service { - return generateService(namespace) -} - -// DeploymentManifest gets the manifest (as a string) that describes the Tiller Deployment -// resource. -func DeploymentManifest(opts *Options) (string, error) { - obj, err := deployment(opts) - if err != nil { - return "", err - } - buf, err := yaml.Marshal(obj) - return string(buf), err -} - -// ServiceManifest gets the manifest (as a string) that describes the Tiller Service -// resource. -func ServiceManifest(namespace string) (string, error) { - obj := service(namespace) - buf, err := yaml.Marshal(obj) - return string(buf), err -} - -func generateLabels(labels map[string]string) map[string]string { - labels["app"] = "helm" - return labels -} - -// parseNodeSelectors parses a comma delimited list of key=values pairs into a map. -func parseNodeSelectorsInto(labels string, m map[string]string) error { - kv := strings.Split(labels, ",") - for _, v := range kv { - el := strings.Split(v, "=") - if len(el) == 2 { - m[el[0]] = el[1] - } else { - return fmt.Errorf("invalid nodeSelector label: %q", kv) - } - } - return nil -} -func generateDeployment(opts *Options) (*v1beta1.Deployment, error) { - labels := generateLabels(map[string]string{"name": "tiller"}) - nodeSelectors := map[string]string{} - if len(opts.NodeSelectors) > 0 { - err := parseNodeSelectorsInto(opts.NodeSelectors, nodeSelectors) - if err != nil { - return nil, err - } - } - automountServiceAccountToken := opts.ServiceAccount != "" - d := &v1beta1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: opts.Namespace, - Name: deploymentName, - Labels: labels, - }, - Spec: v1beta1.DeploymentSpec{ - Replicas: opts.getReplicas(), - Template: v1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: v1.PodSpec{ - ServiceAccountName: opts.ServiceAccount, - AutomountServiceAccountToken: &automountServiceAccountToken, - Containers: []v1.Container{ - { - Name: "tiller", - Image: opts.selectImage(), - ImagePullPolicy: opts.pullPolicy(), - Ports: []v1.ContainerPort{ - {ContainerPort: 44134, Name: "tiller"}, - {ContainerPort: 44135, Name: "http"}, - }, - Env: []v1.EnvVar{ - {Name: "TILLER_NAMESPACE", Value: opts.Namespace}, - {Name: "TILLER_HISTORY_MAX", Value: fmt.Sprintf("%d", opts.MaxHistory)}, - }, - LivenessProbe: &v1.Probe{ - Handler: v1.Handler{ - HTTPGet: &v1.HTTPGetAction{ - Path: "/liveness", - Port: intstr.FromInt(44135), - }, - }, - InitialDelaySeconds: 1, - TimeoutSeconds: 1, - }, - ReadinessProbe: &v1.Probe{ - Handler: v1.Handler{ - HTTPGet: &v1.HTTPGetAction{ - Path: "/readiness", - Port: intstr.FromInt(44135), - }, - }, - InitialDelaySeconds: 1, - TimeoutSeconds: 1, - }, - }, - }, - HostNetwork: opts.EnableHostNetwork, - NodeSelector: nodeSelectors, - }, - }, - }, - } - - if opts.tls() { - const certsDir = "/etc/certs" - - var tlsVerify, tlsEnable = "", "1" - if opts.VerifyTLS { - tlsVerify = "1" - } - - // Mount secret to "/etc/certs" - d.Spec.Template.Spec.Containers[0].VolumeMounts = append(d.Spec.Template.Spec.Containers[0].VolumeMounts, v1.VolumeMount{ - Name: "tiller-certs", - ReadOnly: true, - MountPath: certsDir, - }) - // Add environment variable required for enabling TLS - d.Spec.Template.Spec.Containers[0].Env = append(d.Spec.Template.Spec.Containers[0].Env, []v1.EnvVar{ - {Name: "TILLER_TLS_VERIFY", Value: tlsVerify}, - {Name: "TILLER_TLS_ENABLE", Value: tlsEnable}, - {Name: "TILLER_TLS_CERTS", Value: certsDir}, - }...) - // Add secret volume to deployment - d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, v1.Volume{ - Name: "tiller-certs", - VolumeSource: v1.VolumeSource{ - Secret: &v1.SecretVolumeSource{ - SecretName: "tiller-secret", - }, - }, - }) - } - // if --override values were specified, ultimately convert values and deployment to maps, - // merge them and convert back to Deployment - if len(opts.Values) > 0 { - // base deployment struct - var dd v1beta1.Deployment - // get YAML from original deployment - dy, err := yaml.Marshal(d) - if err != nil { - return nil, fmt.Errorf("Error marshalling base Tiller Deployment: %s", err) - } - // convert deployment YAML to values - dv, err := chartutil.ReadValues(dy) - if err != nil { - return nil, fmt.Errorf("Error converting Deployment manifest: %s ", err) - } - dm := dv.AsMap() - // merge --set values into our map - sm, err := opts.valuesMap(dm) - if err != nil { - return nil, fmt.Errorf("Error merging --set values into Deployment manifest") - } - finalY, err := yaml.Marshal(sm) - if err != nil { - return nil, fmt.Errorf("Error marshalling merged map to YAML: %s ", err) - } - // convert merged values back into deployment - err = yaml.Unmarshal(finalY, &dd) - if err != nil { - return nil, fmt.Errorf("Error unmarshalling Values to Deployment manifest: %s ", err) - } - d = &dd - } - - return d, nil -} - -func generateService(namespace string) *v1.Service { - labels := generateLabels(map[string]string{"name": "tiller"}) - s := &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: serviceName, - Labels: labels, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeClusterIP, - Ports: []v1.ServicePort{ - { - Name: "tiller", - Port: 44134, - TargetPort: intstr.FromString("tiller"), - }, - }, - Selector: labels, - }, - } - return s -} - -// SecretManifest gets the manifest (as a string) that describes the Tiller Secret resource. -func SecretManifest(opts *Options) (string, error) { - o, err := generateSecret(opts) - if err != nil { - return "", err - } - buf, err := yaml.Marshal(o) - return string(buf), err -} - -// createSecret creates the Tiller secret resource. -func createSecret(client corev1.SecretsGetter, opts *Options) error { - o, err := generateSecret(opts) - if err != nil { - return err - } - _, err = client.Secrets(o.Namespace).Create(o) - return err -} - -// generateSecret builds the secret object that hold Tiller secrets. -func generateSecret(opts *Options) (*v1.Secret, error) { - - labels := generateLabels(map[string]string{"name": "tiller"}) - secret := &v1.Secret{ - Type: v1.SecretTypeOpaque, - Data: make(map[string][]byte), - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Labels: labels, - Namespace: opts.Namespace, - }, - } - var err error - if secret.Data["tls.key"], err = read(opts.TLSKeyFile); err != nil { - return nil, err - } - if secret.Data["tls.crt"], err = read(opts.TLSCertFile); err != nil { - return nil, err - } - if opts.VerifyTLS { - if secret.Data["ca.crt"], err = read(opts.TLSCaCertFile); err != nil { - return nil, err - } - } - return secret, nil -} - -func read(path string) (b []byte, err error) { return ioutil.ReadFile(path) } diff --git a/cmd/helm/installer/install_test.go b/cmd/helm/installer/install_test.go deleted file mode 100644 index 80219505a0a..00000000000 --- a/cmd/helm/installer/install_test.go +++ /dev/null @@ -1,743 +0,0 @@ -/* -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 installer // import "k8s.io/helm/cmd/helm/installer" - -import ( - "os" - "path/filepath" - "reflect" - "testing" - - "github.com/ghodss/yaml" - "k8s.io/api/core/v1" - "k8s.io/api/extensions/v1beta1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes/fake" - testcore "k8s.io/client-go/testing" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/version" -) - -func TestDeploymentManifest(t *testing.T) { - tests := []struct { - name string - image string - canary bool - expect string - imagePullPolicy v1.PullPolicy - }{ - {"default", "", false, "gcr.io/kubernetes-helm/tiller:" + version.Version, "IfNotPresent"}, - {"canary", "example.com/tiller", true, "gcr.io/kubernetes-helm/tiller:canary", "Always"}, - {"custom", "example.com/tiller:latest", false, "example.com/tiller:latest", "IfNotPresent"}, - } - - for _, tt := range tests { - o, err := DeploymentManifest(&Options{Namespace: v1.NamespaceDefault, ImageSpec: tt.image, UseCanary: tt.canary}) - if err != nil { - t.Fatalf("%s: error %q", tt.name, err) - } - var dep v1beta1.Deployment - if err := yaml.Unmarshal([]byte(o), &dep); err != nil { - t.Fatalf("%s: error %q", tt.name, err) - } - - if got := dep.Spec.Template.Spec.Containers[0].Image; got != tt.expect { - t.Errorf("%s: expected image %q, got %q", tt.name, tt.expect, got) - } - - if got := dep.Spec.Template.Spec.Containers[0].ImagePullPolicy; got != tt.imagePullPolicy { - t.Errorf("%s: expected imagePullPolicy %q, got %q", tt.name, tt.imagePullPolicy, got) - } - - if got := dep.Spec.Template.Spec.Containers[0].Env[0].Value; got != v1.NamespaceDefault { - t.Errorf("%s: expected namespace %q, got %q", tt.name, v1.NamespaceDefault, got) - } - } -} - -func TestDeploymentManifestForServiceAccount(t *testing.T) { - tests := []struct { - name string - image string - canary bool - expect string - imagePullPolicy v1.PullPolicy - serviceAccount string - }{ - {"withSA", "", false, "gcr.io/kubernetes-helm/tiller:latest", "IfNotPresent", "service-account"}, - {"withoutSA", "", false, "gcr.io/kubernetes-helm/tiller:latest", "IfNotPresent", ""}, - } - for _, tt := range tests { - o, err := DeploymentManifest(&Options{Namespace: v1.NamespaceDefault, ImageSpec: tt.image, UseCanary: tt.canary, ServiceAccount: tt.serviceAccount}) - if err != nil { - t.Fatalf("%s: error %q", tt.name, err) - } - - var d v1beta1.Deployment - if err := yaml.Unmarshal([]byte(o), &d); err != nil { - t.Fatalf("%s: error %q", tt.name, err) - } - if got := d.Spec.Template.Spec.ServiceAccountName; got != tt.serviceAccount { - t.Errorf("%s: expected service account value %q, got %q", tt.name, tt.serviceAccount, got) - } - if got := *d.Spec.Template.Spec.AutomountServiceAccountToken; got != (tt.serviceAccount != "") { - t.Errorf("%s: unexpected automountServiceAccountToken = %t for serviceAccount %q", tt.name, got, tt.serviceAccount) - } - } -} - -func TestDeploymentManifest_WithTLS(t *testing.T) { - tests := []struct { - opts Options - name string - enable string - verify string - }{ - { - Options{Namespace: v1.NamespaceDefault, EnableTLS: true, VerifyTLS: true}, - "tls enable (true), tls verify (true)", - "1", - "1", - }, - { - Options{Namespace: v1.NamespaceDefault, EnableTLS: true, VerifyTLS: false}, - "tls enable (true), tls verify (false)", - "1", - "", - }, - { - Options{Namespace: v1.NamespaceDefault, EnableTLS: false, VerifyTLS: true}, - "tls enable (false), tls verify (true)", - "1", - "1", - }, - } - for _, tt := range tests { - o, err := DeploymentManifest(&tt.opts) - if err != nil { - t.Fatalf("%s: error %q", tt.name, err) - } - - var d v1beta1.Deployment - if err := yaml.Unmarshal([]byte(o), &d); err != nil { - t.Fatalf("%s: error %q", tt.name, err) - } - // verify environment variable in deployment reflect the use of tls being enabled. - if got := d.Spec.Template.Spec.Containers[0].Env[2].Value; got != tt.verify { - t.Errorf("%s: expected tls verify env value %q, got %q", tt.name, tt.verify, got) - } - if got := d.Spec.Template.Spec.Containers[0].Env[3].Value; got != tt.enable { - t.Errorf("%s: expected tls enable env value %q, got %q", tt.name, tt.enable, got) - } - } -} - -func TestServiceManifest(t *testing.T) { - o, err := ServiceManifest(v1.NamespaceDefault) - if err != nil { - t.Fatalf("error %q", err) - } - var svc v1.Service - if err := yaml.Unmarshal([]byte(o), &svc); err != nil { - t.Fatalf("error %q", err) - } - - if got := svc.ObjectMeta.Namespace; got != v1.NamespaceDefault { - t.Errorf("expected namespace %s, got %s", v1.NamespaceDefault, got) - } -} - -func TestSecretManifest(t *testing.T) { - o, err := SecretManifest(&Options{ - VerifyTLS: true, - EnableTLS: true, - Namespace: v1.NamespaceDefault, - TLSKeyFile: tlsTestFile(t, "key.pem"), - TLSCertFile: tlsTestFile(t, "crt.pem"), - TLSCaCertFile: tlsTestFile(t, "ca.pem"), - }) - - if err != nil { - t.Fatalf("error %q", err) - } - - var obj v1.Secret - if err := yaml.Unmarshal([]byte(o), &obj); err != nil { - t.Fatalf("error %q", err) - } - - if got := obj.ObjectMeta.Namespace; got != v1.NamespaceDefault { - t.Errorf("expected namespace %s, got %s", v1.NamespaceDefault, got) - } - if _, ok := obj.Data["tls.key"]; !ok { - t.Errorf("missing 'tls.key' in generated secret object") - } - if _, ok := obj.Data["tls.crt"]; !ok { - t.Errorf("missing 'tls.crt' in generated secret object") - } - if _, ok := obj.Data["ca.crt"]; !ok { - t.Errorf("missing 'ca.crt' in generated secret object") - } -} - -func TestInstall(t *testing.T) { - image := "gcr.io/kubernetes-helm/tiller:v2.0.0" - - fc := &fake.Clientset{} - fc.AddReactor("create", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.CreateAction).GetObject().(*v1beta1.Deployment) - l := obj.GetLabels() - if reflect.DeepEqual(l, map[string]string{"app": "helm"}) { - t.Errorf("expected labels = '', got '%s'", l) - } - i := obj.Spec.Template.Spec.Containers[0].Image - if i != image { - t.Errorf("expected image = '%s', got '%s'", image, i) - } - ports := len(obj.Spec.Template.Spec.Containers[0].Ports) - if ports != 2 { - t.Errorf("expected ports = 2, got '%d'", ports) - } - replicas := obj.Spec.Replicas - if int(*replicas) != 1 { - t.Errorf("expected replicas = 1, got '%d'", replicas) - } - return true, obj, nil - }) - fc.AddReactor("create", "services", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.CreateAction).GetObject().(*v1.Service) - l := obj.GetLabels() - if reflect.DeepEqual(l, map[string]string{"app": "helm"}) { - t.Errorf("expected labels = '', got '%s'", l) - } - n := obj.ObjectMeta.Namespace - if n != v1.NamespaceDefault { - t.Errorf("expected namespace = '%s', got '%s'", v1.NamespaceDefault, n) - } - return true, obj, nil - }) - - opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image} - if err := Install(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 2 { - t.Errorf("unexpected actions: %v, expected 2 actions got %d", actions, len(actions)) - } -} - -func TestInstallHA(t *testing.T) { - image := "gcr.io/kubernetes-helm/tiller:v2.0.0" - - fc := &fake.Clientset{} - fc.AddReactor("create", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.CreateAction).GetObject().(*v1beta1.Deployment) - replicas := obj.Spec.Replicas - if int(*replicas) != 2 { - t.Errorf("expected replicas = 2, got '%d'", replicas) - } - return true, obj, nil - }) - - opts := &Options{ - Namespace: v1.NamespaceDefault, - ImageSpec: image, - Replicas: 2, - } - if err := Install(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } -} - -func TestInstall_WithTLS(t *testing.T) { - image := "gcr.io/kubernetes-helm/tiller:v2.0.0" - name := "tiller-secret" - - fc := &fake.Clientset{} - fc.AddReactor("create", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.CreateAction).GetObject().(*v1beta1.Deployment) - l := obj.GetLabels() - if reflect.DeepEqual(l, map[string]string{"app": "helm"}) { - t.Errorf("expected labels = '', got '%s'", l) - } - i := obj.Spec.Template.Spec.Containers[0].Image - if i != image { - t.Errorf("expected image = '%s', got '%s'", image, i) - } - return true, obj, nil - }) - fc.AddReactor("create", "services", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.CreateAction).GetObject().(*v1.Service) - l := obj.GetLabels() - if reflect.DeepEqual(l, map[string]string{"app": "helm"}) { - t.Errorf("expected labels = '', got '%s'", l) - } - n := obj.ObjectMeta.Namespace - if n != v1.NamespaceDefault { - t.Errorf("expected namespace = '%s', got '%s'", v1.NamespaceDefault, n) - } - return true, obj, nil - }) - fc.AddReactor("create", "secrets", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.CreateAction).GetObject().(*v1.Secret) - if l := obj.GetLabels(); reflect.DeepEqual(l, map[string]string{"app": "helm"}) { - t.Errorf("expected labels = '', got '%s'", l) - } - if n := obj.ObjectMeta.Namespace; n != v1.NamespaceDefault { - t.Errorf("expected namespace = '%s', got '%s'", v1.NamespaceDefault, n) - } - if s := obj.ObjectMeta.Name; s != name { - t.Errorf("expected name = '%s', got '%s'", name, s) - } - if _, ok := obj.Data["tls.key"]; !ok { - t.Errorf("missing 'tls.key' in generated secret object") - } - if _, ok := obj.Data["tls.crt"]; !ok { - t.Errorf("missing 'tls.crt' in generated secret object") - } - if _, ok := obj.Data["ca.crt"]; !ok { - t.Errorf("missing 'ca.crt' in generated secret object") - } - return true, obj, nil - }) - - opts := &Options{ - Namespace: v1.NamespaceDefault, - ImageSpec: image, - EnableTLS: true, - VerifyTLS: true, - TLSKeyFile: tlsTestFile(t, "key.pem"), - TLSCertFile: tlsTestFile(t, "crt.pem"), - TLSCaCertFile: tlsTestFile(t, "ca.pem"), - } - - if err := Install(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 3 { - t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) - } -} - -func TestInstall_canary(t *testing.T) { - fc := &fake.Clientset{} - fc.AddReactor("create", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.CreateAction).GetObject().(*v1beta1.Deployment) - i := obj.Spec.Template.Spec.Containers[0].Image - if i != "gcr.io/kubernetes-helm/tiller:canary" { - t.Errorf("expected canary image, got '%s'", i) - } - return true, obj, nil - }) - fc.AddReactor("create", "services", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.CreateAction).GetObject().(*v1.Service) - return true, obj, nil - }) - - opts := &Options{Namespace: v1.NamespaceDefault, UseCanary: true} - if err := Install(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 2 { - t.Errorf("unexpected actions: %v, expected 2 actions got %d", actions, len(actions)) - } -} - -func TestUpgrade(t *testing.T) { - image := "gcr.io/kubernetes-helm/tiller:v2.0.0" - serviceAccount := "newServiceAccount" - existingDeployment, _ := deployment(&Options{ - Namespace: v1.NamespaceDefault, - ImageSpec: "imageToReplace:v1.0.0", - ServiceAccount: "serviceAccountToReplace", - UseCanary: false, - }) - existingService := service(v1.NamespaceDefault) - - fc := &fake.Clientset{} - fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingDeployment, nil - }) - fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) - i := obj.Spec.Template.Spec.Containers[0].Image - if i != image { - t.Errorf("expected image = '%s', got '%s'", image, i) - } - sa := obj.Spec.Template.Spec.ServiceAccountName - if sa != serviceAccount { - t.Errorf("expected serviceAccountName = '%s', got '%s'", serviceAccount, sa) - } - return true, obj, nil - }) - fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingService, nil - }) - - opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image, ServiceAccount: serviceAccount} - if err := Upgrade(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 3 { - t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) - } -} - -func TestUpgrade_serviceNotFound(t *testing.T) { - image := "gcr.io/kubernetes-helm/tiller:v2.0.0" - - existingDeployment, _ := deployment(&Options{ - Namespace: v1.NamespaceDefault, - ImageSpec: "imageToReplace", - UseCanary: false, - }) - - fc := &fake.Clientset{} - fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingDeployment, nil - }) - fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) - i := obj.Spec.Template.Spec.Containers[0].Image - if i != image { - t.Errorf("expected image = '%s', got '%s'", image, i) - } - return true, obj, nil - }) - fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { - return true, nil, apierrors.NewNotFound(v1.Resource("services"), "1") - }) - fc.AddReactor("create", "services", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.CreateAction).GetObject().(*v1.Service) - n := obj.ObjectMeta.Namespace - if n != v1.NamespaceDefault { - t.Errorf("expected namespace = '%s', got '%s'", v1.NamespaceDefault, n) - } - return true, obj, nil - }) - - opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image} - if err := Upgrade(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 4 { - t.Errorf("unexpected actions: %v, expected 4 actions got %d", actions, len(actions)) - } -} - -func TestUgrade_newerVersion(t *testing.T) { - image := "gcr.io/kubernetes-helm/tiller:v2.0.0" - serviceAccount := "newServiceAccount" - existingDeployment, _ := deployment(&Options{ - Namespace: v1.NamespaceDefault, - ImageSpec: "imageToReplace:v100.5.0", - ServiceAccount: "serviceAccountToReplace", - UseCanary: false, - }) - existingService := service(v1.NamespaceDefault) - - fc := &fake.Clientset{} - fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingDeployment, nil - }) - fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) - i := obj.Spec.Template.Spec.Containers[0].Image - if i != image { - t.Errorf("expected image = '%s', got '%s'", image, i) - } - sa := obj.Spec.Template.Spec.ServiceAccountName - if sa != serviceAccount { - t.Errorf("expected serviceAccountName = '%s', got '%s'", serviceAccount, sa) - } - return true, obj, nil - }) - fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingService, nil - }) - - opts := &Options{ - Namespace: v1.NamespaceDefault, - ImageSpec: image, - ServiceAccount: serviceAccount, - ForceUpgrade: false, - } - if err := Upgrade(fc, opts); err == nil { - t.Errorf("Expected error because the deployed version is newer") - } - - if actions := fc.Actions(); len(actions) != 1 { - t.Errorf("unexpected actions: %v, expected 1 action got %d", actions, len(actions)) - } - - opts = &Options{ - Namespace: v1.NamespaceDefault, - ImageSpec: image, - ServiceAccount: serviceAccount, - ForceUpgrade: true, - } - if err := Upgrade(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 4 { - t.Errorf("unexpected actions: %v, expected 4 action got %d", actions, len(actions)) - } -} - -func TestUpgrade_identical(t *testing.T) { - image := "gcr.io/kubernetes-helm/tiller:v2.0.0" - serviceAccount := "newServiceAccount" - existingDeployment, _ := deployment(&Options{ - Namespace: v1.NamespaceDefault, - ImageSpec: "imageToReplace:v2.0.0", - ServiceAccount: "serviceAccountToReplace", - UseCanary: false, - }) - existingService := service(v1.NamespaceDefault) - - fc := &fake.Clientset{} - fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingDeployment, nil - }) - fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) - i := obj.Spec.Template.Spec.Containers[0].Image - if i != image { - t.Errorf("expected image = '%s', got '%s'", image, i) - } - sa := obj.Spec.Template.Spec.ServiceAccountName - if sa != serviceAccount { - t.Errorf("expected serviceAccountName = '%s', got '%s'", serviceAccount, sa) - } - return true, obj, nil - }) - fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingService, nil - }) - - opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image, ServiceAccount: serviceAccount} - if err := Upgrade(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 3 { - t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) - } -} - -func TestUpgrade_canaryClient(t *testing.T) { - image := "gcr.io/kubernetes-helm/tiller:canary" - serviceAccount := "newServiceAccount" - existingDeployment, _ := deployment(&Options{ - Namespace: v1.NamespaceDefault, - ImageSpec: "imageToReplace:v1.0.0", - ServiceAccount: "serviceAccountToReplace", - UseCanary: false, - }) - existingService := service(v1.NamespaceDefault) - - fc := &fake.Clientset{} - fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingDeployment, nil - }) - fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) - i := obj.Spec.Template.Spec.Containers[0].Image - if i != image { - t.Errorf("expected image = '%s', got '%s'", image, i) - } - sa := obj.Spec.Template.Spec.ServiceAccountName - if sa != serviceAccount { - t.Errorf("expected serviceAccountName = '%s', got '%s'", serviceAccount, sa) - } - return true, obj, nil - }) - fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingService, nil - }) - - opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image, ServiceAccount: serviceAccount} - if err := Upgrade(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 3 { - t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) - } -} - -func TestUpgrade_canaryServer(t *testing.T) { - image := "gcr.io/kubernetes-helm/tiller:v2.0.0" - serviceAccount := "newServiceAccount" - existingDeployment, _ := deployment(&Options{ - Namespace: v1.NamespaceDefault, - ImageSpec: "imageToReplace:canary", - ServiceAccount: "serviceAccountToReplace", - UseCanary: false, - }) - existingService := service(v1.NamespaceDefault) - - fc := &fake.Clientset{} - fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingDeployment, nil - }) - fc.AddReactor("update", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - obj := action.(testcore.UpdateAction).GetObject().(*v1beta1.Deployment) - i := obj.Spec.Template.Spec.Containers[0].Image - if i != image { - t.Errorf("expected image = '%s', got '%s'", image, i) - } - sa := obj.Spec.Template.Spec.ServiceAccountName - if sa != serviceAccount { - t.Errorf("expected serviceAccountName = '%s', got '%s'", serviceAccount, sa) - } - return true, obj, nil - }) - fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) { - return true, existingService, nil - }) - - opts := &Options{Namespace: v1.NamespaceDefault, ImageSpec: image, ServiceAccount: serviceAccount} - if err := Upgrade(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 3 { - t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions)) - } -} - -func tlsTestFile(t *testing.T, path string) string { - const tlsTestDir = "../../../testdata" - path = filepath.Join(tlsTestDir, path) - if _, err := os.Stat(path); os.IsNotExist(err) { - t.Fatalf("tls test file %s does not exist", path) - } - return path -} -func TestDeploymentManifest_WithNodeSelectors(t *testing.T) { - tests := []struct { - opts Options - name string - expect map[string]interface{} - }{ - { - Options{Namespace: v1.NamespaceDefault, NodeSelectors: "app=tiller"}, - "nodeSelector app=tiller", - map[string]interface{}{"app": "tiller"}, - }, - { - Options{Namespace: v1.NamespaceDefault, NodeSelectors: "app=tiller,helm=rocks"}, - "nodeSelector app=tiller, helm=rocks", - map[string]interface{}{"app": "tiller", "helm": "rocks"}, - }, - // note: nodeSelector key and value are strings - { - Options{Namespace: v1.NamespaceDefault, NodeSelectors: "app=tiller,minCoolness=1"}, - "nodeSelector app=tiller, helm=rocks", - map[string]interface{}{"app": "tiller", "minCoolness": "1"}, - }, - } - for _, tt := range tests { - o, err := DeploymentManifest(&tt.opts) - if err != nil { - t.Fatalf("%s: error %q", tt.name, err) - } - - var d v1beta1.Deployment - if err := yaml.Unmarshal([]byte(o), &d); err != nil { - t.Fatalf("%s: error %q", tt.name, err) - } - // Verify that environment variables in Deployment reflect the use of TLS being enabled. - got := d.Spec.Template.Spec.NodeSelector - for k, v := range tt.expect { - if got[k] != v { - t.Errorf("%s: expected nodeSelector value %q, got %q", tt.name, tt.expect, got) - } - } - } -} -func TestDeploymentManifest_WithSetValues(t *testing.T) { - tests := []struct { - opts Options - name string - expectPath string - expect interface{} - }{ - { - Options{Namespace: v1.NamespaceDefault, Values: []string{"spec.template.spec.nodeselector.app=tiller"}}, - "setValues spec.template.spec.nodeSelector.app=tiller", - "spec.template.spec.nodeSelector.app", - "tiller", - }, - { - Options{Namespace: v1.NamespaceDefault, Values: []string{"spec.replicas=2"}}, - "setValues spec.replicas=2", - "spec.replicas", - 2, - }, - { - Options{Namespace: v1.NamespaceDefault, Values: []string{"spec.template.spec.activedeadlineseconds=120"}}, - "setValues spec.template.spec.activedeadlineseconds=120", - "spec.template.spec.activeDeadlineSeconds", - 120, - }, - } - for _, tt := range tests { - o, err := DeploymentManifest(&tt.opts) - if err != nil { - t.Fatalf("%s: error %q", tt.name, err) - } - values, err := chartutil.ReadValues([]byte(o)) - if err != nil { - t.Errorf("Error converting Deployment manifest to Values: %s", err) - } - // path value - pv, err := values.PathValue(tt.expectPath) - if err != nil { - t.Errorf("Error retrieving path value from Deployment Values: %s", err) - } - - // convert our expected value to match the result type for comparison - ev := tt.expect - switch pvt := pv.(type) { - case float64: - floatType := reflect.TypeOf(float64(0)) - v := reflect.ValueOf(ev) - v = reflect.Indirect(v) - if !v.Type().ConvertibleTo(floatType) { - t.Fatalf("Error converting expected value %v to float64", v.Type()) - } - fv := v.Convert(floatType) - if fv.Float() != pvt { - t.Errorf("%s: expected value %q, got %q", tt.name, tt.expect, pv) - } - default: - if pv != tt.expect { - t.Errorf("%s: expected value %q, got %q", tt.name, tt.expect, pv) - } - } - } -} diff --git a/cmd/helm/installer/options.go b/cmd/helm/installer/options.go deleted file mode 100644 index 13cf43dccec..00000000000 --- a/cmd/helm/installer/options.go +++ /dev/null @@ -1,164 +0,0 @@ -/* -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 installer // import "k8s.io/helm/cmd/helm/installer" - -import ( - "fmt" - - "k8s.io/api/core/v1" - "k8s.io/helm/pkg/strvals" - "k8s.io/helm/pkg/version" -) - -const defaultImage = "gcr.io/kubernetes-helm/tiller" - -// Options control how to install Tiller into a cluster, upgrade, and uninstall Tiller from a cluster. -type Options struct { - // EnableTLS instructs Tiller to serve with TLS enabled. - // - // Implied by VerifyTLS. If set the TLSKey and TLSCert are required. - EnableTLS bool - - // VerifyTLS instructs Tiller to serve with TLS enabled verify remote certificates. - // - // If set TLSKey, TLSCert, TLSCaCert are required. - VerifyTLS bool - - // UseCanary indicates that Tiller should deploy using the latest Tiller image. - UseCanary bool - - // Namespace is the Kubernetes namespace to use to deploy Tiller. - Namespace string - - // ServiceAccount is the Kubernetes service account to add to Tiller. - ServiceAccount string - - // Force allows to force upgrading tiller if deployed version is greater than current version - ForceUpgrade bool - - // ImageSpec indentifies the image Tiller will use when deployed. - // - // Valid if and only if UseCanary is false. - ImageSpec string - - // TLSKeyFile identifies the file containing the pem encoded TLS private - // key Tiller should use. - // - // Required and valid if and only if EnableTLS or VerifyTLS is set. - TLSKeyFile string - - // TLSCertFile identifies the file containing the pem encoded TLS - // certificate Tiller should use. - // - // Required and valid if and only if EnableTLS or VerifyTLS is set. - TLSCertFile string - - // TLSCaCertFile identifies the file containing the pem encoded TLS CA - // certificate Tiller should use to verify remotes certificates. - // - // Required and valid if and only if VerifyTLS is set. - TLSCaCertFile string - - // EnableHostNetwork installs Tiller with net=host. - EnableHostNetwork bool - - // MaxHistory sets the maximum number of release versions stored per release. - // - // Less than or equal to zero means no limit. - MaxHistory int - - // Replicas sets the amount of Tiller replicas to start - // - // Less than or equals to 1 means 1. - Replicas int - - // NodeSelectors determine which nodes Tiller can land on. - NodeSelectors string - - // Output dumps the Tiller manifest in the specified format (e.g. JSON) but skips Helm/Tiller installation. - Output OutputFormat - - // Set merges additional values into the Tiller Deployment manifest. - Values []string -} - -func (opts *Options) selectImage() string { - switch { - case opts.UseCanary: - return defaultImage + ":canary" - case opts.ImageSpec == "": - return fmt.Sprintf("%s:%s", defaultImage, version.Version) - default: - return opts.ImageSpec - } -} - -func (opts *Options) pullPolicy() v1.PullPolicy { - if opts.UseCanary { - return v1.PullAlways - } - return v1.PullIfNotPresent -} - -func (opts *Options) getReplicas() *int32 { - replicas := int32(1) - if opts.Replicas > 1 { - replicas = int32(opts.Replicas) - } - return &replicas -} - -func (opts *Options) tls() bool { return opts.EnableTLS || opts.VerifyTLS } - -// valuesMap returns user set values in map format -func (opts *Options) valuesMap(m map[string]interface{}) (map[string]interface{}, error) { - for _, skv := range opts.Values { - if err := strvals.ParseInto(skv, m); err != nil { - return nil, err - } - } - return m, nil -} - -// OutputFormat defines valid values for init output (json, yaml) -type OutputFormat string - -// String returns the string value of the OutputFormat -func (f *OutputFormat) String() string { - return string(*f) -} - -// Type returns the string value of the OutputFormat -func (f *OutputFormat) Type() string { - return "OutputFormat" -} - -const ( - fmtJSON OutputFormat = "json" - fmtYAML OutputFormat = "yaml" -) - -// Set validates and sets the value of the OutputFormat -func (f *OutputFormat) Set(s string) error { - for _, of := range []OutputFormat{fmtJSON, fmtYAML} { - if s == string(of) { - *f = of - return nil - } - } - return fmt.Errorf("unknown output format %q", s) -} diff --git a/cmd/helm/installer/uninstall.go b/cmd/helm/installer/uninstall.go deleted file mode 100644 index 818827ddbd3..00000000000 --- a/cmd/helm/installer/uninstall.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -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 installer // import "k8s.io/helm/cmd/helm/installer" - -import ( - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/kubernetes/pkg/apis/extensions" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" - coreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" - "k8s.io/kubernetes/pkg/kubectl" -) - -const ( - deploymentName = "tiller-deploy" - serviceName = "tiller-deploy" - secretName = "tiller-secret" -) - -// Uninstall uses Kubernetes client to uninstall Tiller. -func Uninstall(client internalclientset.Interface, opts *Options) error { - if err := deleteService(client.Core(), opts.Namespace); err != nil { - return err - } - if err := deleteDeployment(client, opts.Namespace); err != nil { - return err - } - return deleteSecret(client.Core(), opts.Namespace) -} - -// deleteService deletes the Tiller Service resource -func deleteService(client coreclient.ServicesGetter, namespace string) error { - err := client.Services(namespace).Delete(serviceName, &metav1.DeleteOptions{}) - return ingoreNotFound(err) -} - -// deleteDeployment deletes the Tiller Deployment resource -// We need to use the reaper instead of the kube API because GC for deployment dependents -// is not yet supported at the k8s server level (<= 1.5) -func deleteDeployment(client internalclientset.Interface, namespace string) error { - reaper, _ := kubectl.ReaperFor(extensions.Kind("Deployment"), client) - err := reaper.Stop(namespace, deploymentName, 0, nil) - return ingoreNotFound(err) -} - -// deleteSecret deletes the Tiller Secret resource -func deleteSecret(client coreclient.SecretsGetter, namespace string) error { - err := client.Secrets(namespace).Delete(secretName, &metav1.DeleteOptions{}) - return ingoreNotFound(err) -} - -func ingoreNotFound(err error) error { - if apierrors.IsNotFound(err) { - return nil - } - return err -} diff --git a/cmd/helm/installer/uninstall_test.go b/cmd/helm/installer/uninstall_test.go deleted file mode 100644 index 91b257d47d8..00000000000 --- a/cmd/helm/installer/uninstall_test.go +++ /dev/null @@ -1,88 +0,0 @@ -/* -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 installer // import "k8s.io/helm/cmd/helm/installer" - -import ( - "testing" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - testcore "k8s.io/client-go/testing" - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" -) - -func TestUninstall(t *testing.T) { - fc := &fake.Clientset{} - opts := &Options{Namespace: core.NamespaceDefault} - if err := Uninstall(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 7 { - t.Errorf("unexpected actions: %v, expected 7 actions got %d", actions, len(actions)) - } -} - -func TestUninstall_serviceNotFound(t *testing.T) { - fc := &fake.Clientset{} - fc.AddReactor("delete", "services", func(action testcore.Action) (bool, runtime.Object, error) { - return true, nil, apierrors.NewNotFound(schema.GroupResource{Resource: "services"}, "1") - }) - - opts := &Options{Namespace: core.NamespaceDefault} - if err := Uninstall(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 7 { - t.Errorf("unexpected actions: %v, expected 7 actions got %d", actions, len(actions)) - } -} - -func TestUninstall_deploymentNotFound(t *testing.T) { - fc := &fake.Clientset{} - fc.AddReactor("delete", "deployments", func(action testcore.Action) (bool, runtime.Object, error) { - return true, nil, apierrors.NewNotFound(core.Resource("deployments"), "1") - }) - - opts := &Options{Namespace: core.NamespaceDefault} - if err := Uninstall(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 7 { - t.Errorf("unexpected actions: %v, expected 7 actions got %d", actions, len(actions)) - } -} - -func TestUninstall_secretNotFound(t *testing.T) { - fc := &fake.Clientset{} - fc.AddReactor("delete", "secrets", func(action testcore.Action) (bool, runtime.Object, error) { - return true, nil, apierrors.NewNotFound(core.Resource("secrets"), "1") - }) - - opts := &Options{Namespace: core.NamespaceDefault} - if err := Uninstall(fc, opts); err != nil { - t.Errorf("unexpected error: %#+v", err) - } - - if actions := fc.Actions(); len(actions) != 7 { - t.Errorf("unexpected actions: %v, expect 7 actions got %d", actions, len(actions)) - } -} diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index 63f11c062e7..a7aac172ab7 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,21 +17,18 @@ limitations under the License. package main import ( - "errors" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" - "github.com/ghodss/yaml" + "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/lint" - "k8s.io/helm/pkg/lint/support" - "k8s.io/helm/pkg/strvals" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/getter" ) var longLintHelp = ` @@ -43,166 +40,87 @@ it will emit [ERROR] messages. If it encounters issues that break with conventio or recommendation, it will emit [WARNING] messages. ` -type lintCmd struct { - valueFiles valueFiles - values []string - sValues []string - namespace string - strict bool - paths []string - out io.Writer -} - func newLintCmd(out io.Writer) *cobra.Command { - l := &lintCmd{ - paths: []string{"."}, - out: out, - } + client := action.NewLint() + valueOpts := &values.Options{} + cmd := &cobra.Command{ - Use: "lint [flags] PATH", - Short: "examines a chart for possible issues", + Use: "lint PATH", + Short: "examine a chart for possible issues", Long: longLintHelp, RunE: func(cmd *cobra.Command, args []string) error { + paths := []string{"."} if len(args) > 0 { - l.paths = args + paths = args } - return l.run() - }, - } - - cmd.Flags().VarP(&l.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)") - cmd.Flags().StringArrayVar(&l.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - cmd.Flags().StringArrayVar(&l.sValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - cmd.Flags().StringVar(&l.namespace, "namespace", "default", "namespace to install the release into (only used if --install is set)") - cmd.Flags().BoolVar(&l.strict, "strict", false, "fail on lint warnings") - - return cmd -} - -var errLintNoChart = errors.New("No chart found for linting (missing Chart.yaml)") - -func (l *lintCmd) run() error { - var lowestTolerance int - if l.strict { - lowestTolerance = support.WarningSev - } else { - lowestTolerance = support.ErrorSev - } - - // Get the raw values - rvals, err := l.vals() - if err != nil { - return err - } - - var total int - var failures int - for _, path := range l.paths { - if linter, err := lintChart(path, rvals, l.namespace, l.strict); err != nil { - fmt.Println("==> Skipping", path) - fmt.Println(err) - if err == errLintNoChart { - failures = failures + 1 + if client.WithSubcharts { + for _, p := range paths { + filepath.Walk(filepath.Join(p, "charts"), func(path string, info os.FileInfo, err error) error { + if info != nil { + if info.Name() == "Chart.yaml" { + paths = append(paths, filepath.Dir(path)) + } else if strings.HasSuffix(path, ".tgz") || strings.HasSuffix(path, ".tar.gz") { + paths = append(paths, path) + } + } + return nil + }) + } } - } else { - fmt.Println("==> Linting", path) - if len(linter.Messages) == 0 { - fmt.Println("Lint OK") + client.Namespace = settings.Namespace() + vals, err := valueOpts.MergeValues(getter.All(settings)) + if err != nil { + return err } - for _, msg := range linter.Messages { - fmt.Println(msg) - } - - total = total + 1 - if linter.HighestSeverity >= lowestTolerance { - failures = failures + 1 - } - } - fmt.Println("") - } + var message strings.Builder + failed := 0 - msg := fmt.Sprintf("%d chart(s) linted", total) - if failures > 0 { - return fmt.Errorf("%s, %d chart(s) failed", msg, failures) - } + for _, path := range paths { + fmt.Fprintf(&message, "==> Linting %s\n", path) - fmt.Fprintf(l.out, "%s, no failures\n", msg) + result := client.Run([]string{path}, vals) - return nil -} + // All the Errors that are generated by a chart + // that failed a lint will be included in the + // results.Messages so we only need to print + // the Errors if there are no Messages. + if len(result.Messages) == 0 { + for _, err := range result.Errors { + fmt.Fprintf(&message, "Error %s\n", err) + } + } -func lintChart(path string, vals []byte, namespace string, strict bool) (support.Linter, error) { - var chartPath string - linter := support.Linter{} - - if strings.HasSuffix(path, ".tgz") { - tempDir, err := ioutil.TempDir("", "helm-lint") - if err != nil { - return linter, err - } - defer os.RemoveAll(tempDir) - - file, err := os.Open(path) - if err != nil { - return linter, err - } - defer file.Close() - - if err = chartutil.Expand(tempDir, file); err != nil { - return linter, err - } - - lastHyphenIndex := strings.LastIndex(filepath.Base(path), "-") - if lastHyphenIndex <= 0 { - return linter, fmt.Errorf("unable to parse chart archive %q, missing '-'", filepath.Base(path)) - } - base := filepath.Base(path)[:lastHyphenIndex] - chartPath = filepath.Join(tempDir, base) - } else { - chartPath = path - } + for _, msg := range result.Messages { + fmt.Fprintf(&message, "%s\n", msg) + } - // Guard: Error out of this is not a chart. - if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { - return linter, errLintNoChart - } + if len(result.Errors) != 0 { + failed++ + } - return lint.All(chartPath, vals, namespace, strict), nil -} + // Adding extra new line here to break up the + // results, stops this from being a big wall of + // text and makes it easier to follow. + fmt.Fprint(&message, "\n") + } -func (l *lintCmd) vals() ([]byte, error) { - base := map[string]interface{}{} - - // User specified a values files via -f/--values - for _, filePath := range l.valueFiles { - currentMap := map[string]interface{}{} - bytes, err := ioutil.ReadFile(filePath) - if err != nil { - return []byte{}, err - } - - if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { - return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) - } - // Merge with the previous map - base = mergeValues(base, currentMap) - } + fmt.Fprint(out, message.String()) - // User specified a value via --set - for _, value := range l.values { - if err := strvals.ParseInto(value, base); err != nil { - return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) - } + summary := fmt.Sprintf("%d chart(s) linted, %d chart(s) failed", len(paths), failed) + if failed > 0 { + return errors.New(summary) + } + fmt.Fprintln(out, summary) + return nil + }, } - // User specified a value via --set-string - for _, value := range l.sValues { - if err := strvals.ParseIntoString(value, base); err != nil { - return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err) - } - } + f := cmd.Flags() + f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings") + f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts") + addValueOptionsFlags(f, valueOpts) - return yaml.Marshal(base) + return cmd } diff --git a/cmd/helm/lint_test.go b/cmd/helm/lint_test.go index 973af9b6314..3501ccf875a 100644 --- a/cmd/helm/lint_test.go +++ b/cmd/helm/lint_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,38 +17,27 @@ limitations under the License. package main import ( + "fmt" "testing" ) -var ( - values = []byte{} - namespace = "testNamespace" - strict = false - archivedChartPath = "testdata/testcharts/compressedchart-0.1.0.tgz" - archivedChartPathWithHyphens = "testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz" - invalidArchivedChartPath = "testdata/testcharts/invalidcompressedchart0.1.0.tgz" - chartDirPath = "testdata/testcharts/decompressedchart/" - chartMissingManifest = "testdata/testcharts/chart-missing-manifest" -) - -func TestLintChart(t *testing.T) { - if _, err := lintChart(chartDirPath, values, namespace, strict); err != nil { - t.Errorf("%s", err) - } - - if _, err := lintChart(archivedChartPath, values, namespace, strict); err != nil { - t.Errorf("%s", err) - } - - if _, err := lintChart(archivedChartPathWithHyphens, values, namespace, strict); err != nil { - t.Errorf("%s", err) - } - - if _, err := lintChart(invalidArchivedChartPath, values, namespace, strict); err == nil { - t.Errorf("Expected a chart parsing error") - } +func TestLintCmdWithSubchartsFlag(t *testing.T) { + testChart := "testdata/testcharts/chart-with-bad-subcharts" + tests := []cmdTestCase{{ + name: "lint good chart with bad subcharts", + cmd: fmt.Sprintf("lint %s", testChart), + golden: "output/lint-chart-with-bad-subcharts.txt", + wantError: true, + }, { + name: "lint good chart with bad subcharts using --with-subcharts flag", + cmd: fmt.Sprintf("lint --with-subcharts %s", testChart), + golden: "output/lint-chart-with-bad-subcharts-with-subcharts.txt", + wantError: true, + }} + runTestCmd(t, tests) +} - if _, err := lintChart(chartMissingManifest, values, namespace, strict); err == nil { - t.Errorf("Expected a chart parsing error") - } +func TestLintFileCompletion(t *testing.T) { + checkFileCompletion(t, "lint", true) + checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given } diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 4332ceb2169..f8be65b1723 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,34 +19,35 @@ package main import ( "fmt" "io" - "strings" + "os" + "strconv" "github.com/gosuri/uitable" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/timeconv" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" + "helm.sh/helm/v3/pkg/release" ) var listHelp = ` -This command lists all of the releases. +This command lists all of the releases for a specified namespace (uses current namespace context if namespace not specified). By default, it lists only releases that are deployed or failed. Flags like -'--deleted' and '--all' will alter this behavior. Such flags can be combined: -'--deleted --failed'. +'--uninstalled' and '--all' will alter this behavior. Such flags can be combined: +'--uninstalled --failed'. By default, items are sorted alphabetically. Use the '-d' flag to sort by release date. -If an argument is provided, it will be treated as a filter. Filters are +If the --filter flag is provided, it will be treated as a filter. Filters are regular expressions (Perl compatible) that are applied to the list of releases. Only items that match the filter will be returned. - $ helm list 'ara[a-z]+' - NAME UPDATED CHART - maudlin-arachnid Mon May 9 16:07:08 2016 alpine-0.1.0 + $ helm list --filter 'ara[a-z]+' + NAME UPDATED CHART + maudlin-arachnid 2020-06-18 14:17:46.125134977 +0000 UTC alpine-0.1.0 If no results are found, 'helm list' will exit 0, but with no output (or in the case of no '-q' flag, only headers). @@ -57,198 +58,186 @@ 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 listCmd struct { - filter string - short bool - limit int - offset string - byDate bool - sortDesc bool - out io.Writer - all bool - deleted bool - deleting bool - deployed bool - failed bool - namespace string - superseded bool - pending bool - client helm.Interface - colWidth uint -} - -func newListCmd(client helm.Interface, out io.Writer) *cobra.Command { - list := &listCmd{ - out: out, - client: client, - } +func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewList(cfg) + var outfmt output.Format cmd := &cobra.Command{ - Use: "list [flags] [FILTER]", - Short: "list releases", - Long: listHelp, - Aliases: []string{"ls"}, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + Use: "list", + Short: "list releases", + Long: listHelp, + Aliases: []string{"ls"}, + Args: require.NoArgs, + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - list.filter = strings.Join(args, " ") + if client.AllNamespaces { + if err := cfg.Init(settings.RESTClientGetter(), "", os.Getenv("HELM_DRIVER"), debug); err != nil { + return err + } } - if list.client == nil { - list.client = newClient() + client.SetStateMask() + + results, err := client.Run() + if err != nil { + return err + } + + if client.Short { + + names := make([]string, 0) + for _, res := range results { + names = append(names, res.Name) + } + + outputFlag := cmd.Flag("output") + + switch outputFlag.Value.String() { + case "json": + output.EncodeJSON(out, names) + return nil + case "yaml": + output.EncodeYAML(out, names) + return nil + case "table": + for _, res := range results { + fmt.Fprintln(out, res.Name) + } + return nil + default: + return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat)) + } } - return list.run() + + return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat)) }, } f := cmd.Flags() - f.BoolVarP(&list.short, "short", "q", false, "output short (quiet) listing format") - f.BoolVarP(&list.byDate, "date", "d", false, "sort by release date") - f.BoolVarP(&list.sortDesc, "reverse", "r", false, "reverse the sort order") - f.IntVarP(&list.limit, "max", "m", 256, "maximum number of releases to fetch") - f.StringVarP(&list.offset, "offset", "o", "", "next release name in the list, used to offset from start value") - f.BoolVarP(&list.all, "all", "a", false, "show all releases, not just the ones marked DEPLOYED") - f.BoolVar(&list.deleted, "deleted", false, "show deleted releases") - f.BoolVar(&list.deleting, "deleting", false, "show releases that are currently being deleted") - f.BoolVar(&list.deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled") - f.BoolVar(&list.failed, "failed", false, "show failed releases") - f.BoolVar(&list.pending, "pending", false, "show pending releases") - f.StringVar(&list.namespace, "namespace", "", "show releases within a specific namespace") - f.UintVar(&list.colWidth, "col-width", 60, "specifies the max column width of output") - - // TODO: Do we want this as a feature of 'helm list'? - //f.BoolVar(&list.superseded, "history", true, "show historical releases") + f.BoolVarP(&client.Short, "short", "q", false, "output short (quiet) listing format") + f.StringVar(&client.TimeFormat, "time-format", "", `format time using golang time formatter. Example: --time-format "2006-01-02 15:04:05Z0700"`) + f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date") + f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order") + f.BoolVarP(&client.All, "all", "a", false, "show all releases without any filter applied") + f.BoolVar(&client.Uninstalled, "uninstalled", false, "show uninstalled releases (if 'helm uninstall --keep-history' was used)") + f.BoolVar(&client.Superseded, "superseded", false, "show superseded releases") + f.BoolVar(&client.Uninstalling, "uninstalling", false, "show releases that are currently being uninstalled") + f.BoolVar(&client.Deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled") + f.BoolVar(&client.Failed, "failed", false, "show failed releases") + f.BoolVar(&client.Pending, "pending", false, "show pending releases") + f.BoolVarP(&client.AllNamespaces, "all-namespaces", "A", false, "list releases across all namespaces") + f.IntVarP(&client.Limit, "max", "m", 256, "maximum number of releases to fetch") + f.IntVar(&client.Offset, "offset", 0, "next release index in the list, used to offset from start value") + f.StringVarP(&client.Filter, "filter", "f", "", "a regular expression (Perl compatible). Any releases that match the expression will be included in the results") + f.StringVarP(&client.Selector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Works only for secret(default) and configmap storage backends.") + bindOutputFlag(cmd, &outfmt) return cmd } -func (l *listCmd) run() error { - sortBy := services.ListSort_NAME - if l.byDate { - sortBy = services.ListSort_LAST_RELEASED - } - - sortOrder := services.ListSort_ASC - if l.sortDesc { - sortOrder = services.ListSort_DESC - } +type releaseElement struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Revision string `json:"revision"` + Updated string `json:"updated"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` +} - stats := l.statusCodes() +type releaseListWriter struct { + releases []releaseElement +} - 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)), - helm.ReleaseListStatuses(stats), - helm.ReleaseListNamespace(l.namespace), - ) +func newReleaseListWriter(releases []*release.Release, timeFormat string) *releaseListWriter { + // Initialize the array so no results returns an empty array instead of null + elements := make([]releaseElement, 0, len(releases)) + for _, r := range releases { + element := releaseElement{ + Name: r.Name, + Namespace: r.Namespace, + Revision: strconv.Itoa(r.Version), + Status: r.Info.Status.String(), + Chart: fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version), + AppVersion: r.Chart.Metadata.AppVersion, + } - if err != nil { - return prettyError(err) - } + t := "-" + if tspb := r.Info.LastDeployed; !tspb.IsZero() { + if timeFormat != "" { + t = tspb.Format(timeFormat) + } else { + t = tspb.String() + } + } + element.Updated = t - if len(res.GetReleases()) == 0 { - return nil + elements = append(elements, element) } + return &releaseListWriter{elements} +} - if res.Next != "" && !l.short { - fmt.Fprintf(l.out, "\tnext: %s\n", res.Next) +func (r *releaseListWriter) WriteTable(out io.Writer) error { + table := uitable.New() + table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION") + for _, r := range r.releases { + table.AddRow(r.Name, r.Namespace, r.Revision, r.Updated, r.Status, r.Chart, r.AppVersion) } + return output.EncodeTable(out, table) +} - rels := filterList(res.Releases) +func (r *releaseListWriter) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, r.releases) +} - if l.short { - for _, r := range rels { - fmt.Fprintln(l.out, r.Name) - } - return nil - } - fmt.Fprintln(l.out, formatList(rels, l.colWidth)) - return nil +func (r *releaseListWriter) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, r.releases) } -// filterList returns a list scrubbed of old releases. -func filterList(rels []*release.Release) []*release.Release { - idx := map[string]int32{} +// Returns all releases from 'releases', except those with names matching 'ignoredReleases' +func filterReleases(releases []*release.Release, ignoredReleaseNames []string) []*release.Release { + // if ignoredReleaseNames is nil, just return releases + if ignoredReleaseNames == nil { + return releases + } - for _, r := range rels { - name, version := r.GetName(), r.GetVersion() - if max, ok := idx[name]; ok { - // check if we have a greater version already - if max > version { - continue + var filteredReleases []*release.Release + for _, rel := range releases { + found := false + for _, ignoredName := range ignoredReleaseNames { + if rel.Name == ignoredName { + found = true + break } } - idx[name] = version - } - - uniq := make([]*release.Release, 0, len(idx)) - for _, r := range rels { - if idx[r.GetName()] == r.GetVersion() { - uniq = append(uniq, r) + if !found { + filteredReleases = append(filteredReleases, rel) } } - return uniq + + return filteredReleases } -// statusCodes gets the list of status codes that are to be included in the results. -func (l *listCmd) statusCodes() []release.Status_Code { - if l.all { - return []release.Status_Code{ - release.Status_UNKNOWN, - release.Status_DEPLOYED, - release.Status_DELETED, - release.Status_DELETING, - release.Status_FAILED, - release.Status_PENDING_INSTALL, - release.Status_PENDING_UPGRADE, - release.Status_PENDING_ROLLBACK, - } - } - status := []release.Status_Code{} - if l.deployed { - status = append(status, release.Status_DEPLOYED) - } - if l.deleted { - status = append(status, release.Status_DELETED) - } - if l.deleting { - status = append(status, release.Status_DELETING) - } - if l.failed { - status = append(status, release.Status_FAILED) - } - if l.superseded { - status = append(status, release.Status_SUPERSEDED) - } - if l.pending { - status = append(status, release.Status_PENDING_INSTALL, release.Status_PENDING_UPGRADE, release.Status_PENDING_ROLLBACK) - } +// Provide dynamic auto-completion for release names +func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *action.Configuration) ([]string, cobra.ShellCompDirective) { + cobra.CompDebugln(fmt.Sprintf("compListReleases with toComplete %s", toComplete), settings.Debug) - // Default case. - if len(status) == 0 { - status = append(status, release.Status_DEPLOYED, release.Status_FAILED) - } - return status -} + client := action.NewList(cfg) + client.All = true + client.Limit = 0 + client.Filter = fmt.Sprintf("^%s", toComplete) -func formatList(rels []*release.Release, colWidth uint) string { - table := uitable.New() + client.SetStateMask() + releases, err := client.Run() + if err != nil { + return nil, cobra.ShellCompDirectiveDefault + } - table.MaxColWidth = colWidth - table.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "NAMESPACE") - for _, r := range rels { - md := r.GetChart().GetMetadata() - c := fmt.Sprintf("%s-%s", md.GetName(), md.GetVersion()) - t := "-" - if tspb := r.GetInfo().GetLastDeployed(); tspb != nil { - t = timeconv.String(tspb) - } - s := r.GetInfo().GetStatus().GetCode().String() - v := r.GetVersion() - n := r.GetNamespace() - table.AddRow(r.GetName(), v, t, s, c, n) + var choices []string + filteredReleases := filterReleases(releases, ignoredReleaseNames) + for _, rel := range filteredReleases { + choices = append(choices, + fmt.Sprintf("%s\t%s-%s -> %s", rel.Name, rel.Chart.Metadata.Name, rel.Chart.Metadata.Version, rel.Info.Status.String())) } - return table.String() + + return choices, cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index d853fa6b155..b3b29356ea2 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,112 +17,225 @@ limitations under the License. package main import ( - "io" "testing" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/time" ) func TestListCmd(t *testing.T) { - tests := []releaseCase{ + defaultNamespace := "default" + + sampleTimeSeconds := int64(1452902400) + timestamp1 := time.Unix(sampleTimeSeconds+1, 0).UTC() + timestamp2 := time.Unix(sampleTimeSeconds+2, 0).UTC() + timestamp3 := time.Unix(sampleTimeSeconds+3, 0).UTC() + timestamp4 := time.Unix(sampleTimeSeconds+4, 0).UTC() + chartInfo := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "chickadee", + Version: "1.0.0", + AppVersion: "0.0.1", + }, + } + + releaseFixture := []*release.Release{ { - name: "with a release", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}), + Name: "starlord", + Version: 1, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp1, + Status: release.StatusSuperseded, }, - expected: "thomas-guide", + Chart: chartInfo, }, { - name: "list", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas"}), + Name: "starlord", + Version: 2, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp1, + Status: release.StatusDeployed, }, - expected: "NAME \tREVISION\tUPDATED \tSTATUS \tCHART \tNAMESPACE\natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\tdefault \n", + Chart: chartInfo, }, { - name: "list, one deployed, one failed", - flags: []string{"-q"}, - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_FAILED}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + Name: "groot", + Version: 1, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp1, + Status: release.StatusUninstalled, }, - expected: "thomas-guide\natlas-guide", + Chart: chartInfo, }, { - name: "with a release, multiple flags", - flags: []string{"--deleted", "--deployed", "--failed", "-q"}, - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_DELETED}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + Name: "gamora", + Version: 1, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp1, + Status: release.StatusSuperseded, }, - // Note: We're really only testing that the flags parsed correctly. Which results are returned - // depends on the backend. And until pkg/helm is done, we can't mock this. - expected: "thomas-guide\natlas-guide", + Chart: chartInfo, }, { - name: "with a release, multiple flags", - flags: []string{"--all", "-q"}, - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_DELETED}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + Name: "rocket", + Version: 1, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp2, + Status: release.StatusFailed, }, - // See note on previous test. - expected: "thomas-guide\natlas-guide", + Chart: chartInfo, }, { - name: "with a release, multiple flags, deleting", - flags: []string{"--all", "-q"}, - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_DELETING}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + Name: "drax", + Version: 1, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp1, + Status: release.StatusUninstalling, }, - // See note on previous test. - expected: "thomas-guide\natlas-guide", + Chart: chartInfo, }, { - name: "namespace defined, multiple flags", - flags: []string{"--all", "-q", "--namespace test123"}, - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Namespace: "test123"}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Namespace: "test321"}), + Name: "thanos", + Version: 1, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp1, + Status: release.StatusPendingInstall, }, - // See note on previous test. - expected: "thomas-guide", + Chart: chartInfo, }, { - name: "with a pending release, multiple flags", - flags: []string{"--all", "-q"}, - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_PENDING_INSTALL}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + Name: "hummingbird", + Version: 1, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp3, + Status: release.StatusDeployed, }, - expected: "thomas-guide\natlas-guide", + Chart: chartInfo, }, { - name: "with a pending release, pending flag", - flags: []string{"--pending", "-q"}, - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_PENDING_INSTALL}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "wild-idea", StatusCode: release.Status_PENDING_UPGRADE}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-maps", StatusCode: release.Status_PENDING_ROLLBACK}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), + Name: "iguana", + Version: 2, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp4, + Status: release.StatusDeployed, }, - expected: "thomas-guide\nwild-idea\ncrazy-maps", + Chart: chartInfo, }, { - name: "with old releases", - rels: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}), - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", StatusCode: release.Status_FAILED}), + Name: "starlord", + Version: 2, + Namespace: "milano", + Info: &release.Info{ + LastDeployed: timestamp1, + Status: release.StatusDeployed, }, - expected: "thomas-guide", + Chart: chartInfo, }, } - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newListCmd(c, out) - }) + tests := []cmdTestCase{{ + name: "list releases", + cmd: "list", + golden: "output/list.txt", + rels: releaseFixture, + }, { + name: "list all releases", + cmd: "list --all", + golden: "output/list-all.txt", + rels: releaseFixture, + }, { + name: "list releases sorted by release date", + cmd: "list --date", + golden: "output/list-date.txt", + rels: releaseFixture, + }, { + name: "list failed releases", + cmd: "list --failed", + golden: "output/list-failed.txt", + rels: releaseFixture, + }, { + name: "list filtered releases", + cmd: "list --filter='.*'", + golden: "output/list-filter.txt", + rels: releaseFixture, + }, { + name: "list releases, limited to one release", + cmd: "list --max 1", + golden: "output/list-max.txt", + rels: releaseFixture, + }, { + name: "list releases, offset by one", + cmd: "list --offset 1", + golden: "output/list-offset.txt", + rels: releaseFixture, + }, { + name: "list pending releases", + cmd: "list --pending", + golden: "output/list-pending.txt", + rels: releaseFixture, + }, { + name: "list releases in reverse order", + cmd: "list --reverse", + golden: "output/list-reverse.txt", + rels: releaseFixture, + }, { + name: "list releases sorted by reversed release date", + cmd: "list --date --reverse", + golden: "output/list-date-reversed.txt", + rels: releaseFixture, + }, { + name: "list releases in short output format", + cmd: "list --short", + golden: "output/list-short.txt", + rels: releaseFixture, + }, { + name: "list releases in short output format", + cmd: "list --short --output yaml", + golden: "output/list-short-yaml.txt", + rels: releaseFixture, + }, { + name: "list releases in short output format", + cmd: "list --short --output json", + golden: "output/list-short-json.txt", + rels: releaseFixture, + }, { + name: "list superseded releases", + cmd: "list --superseded", + golden: "output/list-superseded.txt", + rels: releaseFixture, + }, { + name: "list uninstalled releases", + cmd: "list --uninstalled", + golden: "output/list-uninstalled.txt", + rels: releaseFixture, + }, { + name: "list releases currently uninstalling", + cmd: "list --uninstalling", + golden: "output/list-uninstalling.txt", + rels: releaseFixture, + }, { + name: "list releases in another namespace", + cmd: "list -n milano", + golden: "output/list-namespace.txt", + rels: releaseFixture, + }} + runTestCmd(t, tests) +} + +func TestListOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "list") +} + +func TestListFileCompletion(t *testing.T) { + checkFileCompletion(t, "list", false) } diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go index ef24e7883f2..70002b0b065 100644 --- a/cmd/helm/load_plugins.go +++ b/cmd/helm/load_plugins.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,18 +16,35 @@ limitations under the License. package main import ( + "bytes" "fmt" "io" + "io/ioutil" + "log" "os" "os/exec" "path/filepath" + "strconv" "strings" + "syscall" + "github.com/pkg/errors" "github.com/spf13/cobra" + "sigs.k8s.io/yaml" - "k8s.io/helm/pkg/plugin" + "helm.sh/helm/v3/pkg/plugin" ) +const ( + pluginStaticCompletionFile = "completion.yaml" + pluginDynamicCompletionExecutable = "plugin.complete" +) + +type pluginError struct { + error + code int +} + // loadPlugins loads plugins into the command list. // // This follows a different pattern than the other commands because it has @@ -40,21 +57,12 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) { return } - // debug("HELM_PLUGIN_DIRS=%s", settings.PluginDirs()) - found, err := findPlugins(settings.PluginDirs()) + found, err := plugin.FindPlugins(settings.PluginsDirectory) if err != nil { - fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err) + fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err) return } - processParent := func(cmd *cobra.Command, args []string) ([]string, error) { - k, u := manuallyProcessArgs(args) - if err := cmd.Parent().ParseFlags(k); err != nil { - return nil, err - } - return u, nil - } - // Now we create commands for all of these. for _, plug := range found { plug := plug @@ -77,39 +85,67 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) { // PrepareCommand uses os.ExpandEnv and expects the // setupEnv vars. plugin.SetupPluginEnv(settings, md.Name, plug.Dir) - main, argv := plug.PrepareCommand(u) - - prog := exec.Command(main, argv...) - prog.Env = os.Environ() - prog.Stdin = os.Stdin - prog.Stdout = out - prog.Stderr = os.Stderr - if err := prog.Run(); err != nil { - if eerr, ok := err.(*exec.ExitError); ok { - os.Stderr.Write(eerr.Stderr) - return fmt.Errorf("plugin %q exited with error", md.Name) - } - return err + main, argv, prepCmdErr := plug.PrepareCommand(u) + if prepCmdErr != nil { + os.Stderr.WriteString(prepCmdErr.Error()) + return errors.Errorf("plugin %q exited with error", md.Name) } - return nil + + return callPluginExecutable(md.Name, main, argv, out) }, // This passes all the flags to the subcommand. DisableFlagParsing: true, } - if md.UseTunnel { - c.PreRunE = func(cmd *cobra.Command, args []string) error { - // Parse the parent flag, but not the local flags. - if _, err := processParent(cmd, args); err != nil { - return err - } - return setupConnection() - } - } - // TODO: Make sure a command with this name does not already exist. baseCmd.AddCommand(c) + + // For completion, we try to load more details about the plugins so as to allow for command and + // flag completion of the plugin itself. + // We only do this when necessary (for the "completion" and "__complete" commands) to avoid the + // risk of a rogue plugin affecting Helm's normal behavior. + subCmd, _, err := baseCmd.Find(os.Args[1:]) + if (err == nil && + ((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) || + /* for the tests */ subCmd == baseCmd.Root() { + loadCompletionForPlugin(c, plug) + } + } +} + +func processParent(cmd *cobra.Command, args []string) ([]string, error) { + k, u := manuallyProcessArgs(args) + if err := cmd.Parent().ParseFlags(k); err != nil { + return nil, err + } + return u, nil +} + +// This function is used to setup the environment for the plugin and then +// call the executable specified by the parameter 'main' +func callPluginExecutable(pluginName string, main string, argv []string, out io.Writer) error { + env := os.Environ() + for k, v := range settings.EnvVars() { + env = append(env, fmt.Sprintf("%s=%s", k, v)) } + + prog := exec.Command(main, argv...) + prog.Env = env + prog.Stdin = os.Stdin + prog.Stdout = out + prog.Stderr = os.Stderr + if err := prog.Run(); err != nil { + if eerr, ok := err.(*exec.ExitError); ok { + os.Stderr.Write(eerr.Stderr) + status := eerr.Sys().(syscall.WaitStatus) + return pluginError{ + error: errors.Errorf("plugin %q exited with error", pluginName), + code: status.ExitStatus(), + } + } + return err + } + return nil } // manuallyProcessArgs processes an arg array, removing special args. @@ -118,7 +154,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) { func manuallyProcessArgs(args []string) ([]string, []string) { known := []string{} unknown := []string{} - kvargs := []string{"--host", "--kube-context", "--home", "--tiller-namespace"} + kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--kube-as-user", "--kube-as-group", "--kube-ca-file", "--registry-config", "--repository-cache", "--repository-config"} knownArg := func(a string) bool { for _, pre := range kvargs { if strings.HasPrefix(a, pre+"=") { @@ -127,13 +163,26 @@ func manuallyProcessArgs(args []string) ([]string, []string) { } return false } + + isKnown := func(v string) string { + for _, i := range kvargs { + if i == v { + return v + } + } + return "" + } + for i := 0; i < len(args); i++ { switch a := args[i]; a { case "--debug": known = append(known, a) - case "--host", "--kube-context", "--home": - known = append(known, a, args[i+1]) + case isKnown(a): + known = append(known, a) i++ + if i < len(args) { + known = append(known, args[i]) + } default: if knownArg(a) { known = append(known, a) @@ -145,16 +194,184 @@ func manuallyProcessArgs(args []string) ([]string, []string) { return known, unknown } -// findPlugins returns a list of YAML files that describe plugins. -func findPlugins(plugdirs string) ([]*plugin.Plugin, error) { - found := []*plugin.Plugin{} - // Let's get all UNIXy and allow path separators - for _, p := range filepath.SplitList(plugdirs) { - matches, err := plugin.LoadAll(p) - if err != nil { - return matches, err +// pluginCommand represents the optional completion.yaml file of a plugin +type pluginCommand struct { + Name string `json:"name"` + ValidArgs []string `json:"validArgs"` + Flags []string `json:"flags"` + Commands []pluginCommand `json:"commands"` +} + +// loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin +// and add the dynamic completion hook to call the optional plugin.complete +func loadCompletionForPlugin(pluginCmd *cobra.Command, plugin *plugin.Plugin) { + // Parse the yaml file providing the plugin's sub-commands and flags + cmds, err := loadFile(strings.Join( + []string{plugin.Dir, pluginStaticCompletionFile}, string(filepath.Separator))) + + if err != nil { + // The file could be missing or invalid. No static completion for this plugin. + if settings.Debug { + log.Output(2, fmt.Sprintf("[info] %s\n", err.Error())) } - found = append(found, matches...) + // Continue to setup dynamic completion. + cmds = &pluginCommand{} } - return found, nil + + // Preserve the Usage string specified for the plugin + cmds.Name = pluginCmd.Use + + addPluginCommands(plugin, pluginCmd, cmds) +} + +// addPluginCommands is a recursive method that adds each different level +// of sub-commands and flags for the plugins that have provided such information +func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) { + if cmds == nil { + return + } + + if len(cmds.Name) == 0 { + // Missing name for a command + if settings.Debug { + log.Output(2, fmt.Sprintf("[info] sub-command name field missing for %s", baseCmd.CommandPath())) + } + return + } + + baseCmd.Use = cmds.Name + baseCmd.ValidArgs = cmds.ValidArgs + // Setup the same dynamic completion for each plugin sub-command. + // This is because if dynamic completion is triggered, there is a single executable + // to call (plugin.complete), so every sub-commands calls it in the same fashion. + if cmds.Commands == nil { + // Only setup dynamic completion if there are no sub-commands. This avoids + // calling plugin.complete at every completion, which greatly simplifies + // development of plugin.complete for plugin developers. + baseCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return pluginDynamicComp(plugin, cmd, args, toComplete) + } + } + + // Create fake flags. + if len(cmds.Flags) > 0 { + // The flags can be created with any type, since we only need them for completion. + // pflag does not allow to create short flags without a corresponding long form + // so we look for all short flags and match them to any long flag. This will allow + // plugins to provide short flags without a long form. + // If there are more short-flags than long ones, we'll create an extra long flag with + // the same single letter as the short form. + shorts := []string{} + longs := []string{} + for _, flag := range cmds.Flags { + if len(flag) == 1 { + shorts = append(shorts, flag) + } else { + longs = append(longs, flag) + } + } + + f := baseCmd.Flags() + if len(longs) >= len(shorts) { + for i := range longs { + if i < len(shorts) { + f.BoolP(longs[i], shorts[i], false, "") + } else { + f.Bool(longs[i], false, "") + } + } + } else { + for i := range shorts { + if i < len(longs) { + f.BoolP(longs[i], shorts[i], false, "") + } else { + // Create a long flag with the same name as the short flag. + // Not a perfect solution, but its better than ignoring the extra short flags. + f.BoolP(shorts[i], shorts[i], false, "") + } + } + } + } + + // Recursively add any sub-commands + for _, cmd := range cmds.Commands { + // Create a fake command so that completion can be done for the sub-commands of the plugin + subCmd := &cobra.Command{ + // This prevents Cobra from removing the flags. We want to keep the flags to pass them + // to the dynamic completion script of the plugin. + DisableFlagParsing: true, + // A Run is required for it to be a valid command without subcommands + Run: func(cmd *cobra.Command, args []string) {}, + } + baseCmd.AddCommand(subCmd) + addPluginCommands(plugin, subCmd, &cmd) + } +} + +// loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object +func loadFile(path string) (*pluginCommand, error) { + cmds := new(pluginCommand) + b, err := ioutil.ReadFile(path) + if err != nil { + return cmds, errors.New(fmt.Sprintf("File (%s) not provided by plugin. No plugin auto-completion possible.", path)) + } + + err = yaml.Unmarshal(b, cmds) + return cmds, err +} + +// pluginDynamicComp call the plugin.complete script of the plugin (if available) +// to obtain the dynamic completion choices. It must pass all the flags and sub-commands +// specified in the command-line to the plugin.complete executable (except helm's global flags) +func pluginDynamicComp(plug *plugin.Plugin, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + md := plug.Metadata + + u, err := processParent(cmd, args) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + // We will call the dynamic completion script of the plugin + main := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator)) + + // We must include all sub-commands passed on the command-line. + // To do that, we pass-in the entire CommandPath, except the first two elements + // which are 'helm' and 'pluginName'. + argv := strings.Split(cmd.CommandPath(), " ")[2:] + if !md.IgnoreFlags { + argv = append(argv, u...) + argv = append(argv, toComplete) + } + plugin.SetupPluginEnv(settings, md.Name, plug.Dir) + + cobra.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv), settings.Debug) + buf := new(bytes.Buffer) + if err := callPluginExecutable(md.Name, main, argv, buf); err != nil { + // The dynamic completion file is optional for a plugin, so this error is ok. + cobra.CompDebugln(fmt.Sprintf("Unable to call %s: %v", main, err.Error()), settings.Debug) + return nil, cobra.ShellCompDirectiveDefault + } + + var completions []string + for _, comp := range strings.Split(buf.String(), "\n") { + // Remove any empty lines + if len(comp) > 0 { + completions = append(completions, comp) + } + } + + // Check if the last line of output is of the form :, which + // indicates the BashCompletionDirective. + directive := cobra.ShellCompDirectiveDefault + if len(completions) > 0 { + lastLine := completions[len(completions)-1] + if len(lastLine) > 1 && lastLine[0] == ':' { + if strInt, err := strconv.Atoi(lastLine[1:]); err == nil { + directive = cobra.ShellCompDirective(strInt) + completions = completions[:len(completions)-1] + } + } + } + + return completions, directive } diff --git a/cmd/helm/package.go b/cmd/helm/package.go index bf171e534ae..7134a8784d5 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,25 +17,19 @@ limitations under the License. package main import ( - "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" - "syscall" - "github.com/Masterminds/semver" + "github.com/pkg/errors" "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/downloader" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/provenance" - "k8s.io/helm/pkg/repo" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" ) const packageDesc = ` @@ -43,215 +37,88 @@ This command packages a chart into a versioned chart archive file. If a path is given, this will look at that path for a chart (which must contain a Chart.yaml file) and then package that directory. -If no path is given, this will look in the present working directory for a -Chart.yaml file, and (if found) build the current directory into a chart. - Versioned chart archives are used by Helm package repositories. -` -type packageCmd struct { - save bool - sign bool - path string - valueFiles valueFiles - values []string - stringValues []string - key string - keyring string - version string - appVersion string - destination string - dependencyUpdate bool +To sign a chart, use the '--sign' flag. In most cases, you should also +provide '--keyring path/to/secret/keys' and '--key keyname'. - out io.Writer - home helmpath.Home -} + $ helm package --sign ./mychart --key mykey --keyring ~/.gnupg/secring.gpg + +If '--keyring' is not specified, Helm usually defaults to the public keyring +unless your environment is otherwise configured. +` func newPackageCmd(out io.Writer) *cobra.Command { - pkg := &packageCmd{out: out} + client := action.NewPackage() + valueOpts := &values.Options{} cmd := &cobra.Command{ - Use: "package [flags] [CHART_PATH] [...]", + Use: "package [CHART_PATH] [...]", Short: "package a chart directory into a chart archive", Long: packageDesc, RunE: func(cmd *cobra.Command, args []string) error { - pkg.home = settings.Home if len(args) == 0 { - return fmt.Errorf("need at least one argument, the path to the chart") + return errors.Errorf("need at least one argument, the path to the chart") } - if pkg.sign { - if pkg.key == "" { + if client.Sign { + if client.Key == "" { return errors.New("--key is required for signing a package") } - if pkg.keyring == "" { + if client.Keyring == "" { return errors.New("--keyring is required for signing a package") } } + client.RepositoryConfig = settings.RepositoryConfig + client.RepositoryCache = settings.RepositoryCache + p := getter.All(settings) + vals, err := valueOpts.MergeValues(p) + if err != nil { + return err + } + for i := 0; i < len(args); i++ { - pkg.path = args[i] - if err := pkg.run(); err != nil { + path, err := filepath.Abs(args[i]) + if err != nil { return err } + if _, err := os.Stat(args[i]); err != nil { + return err + } + + if client.DependencyUpdate { + downloadManager := &downloader.Manager{ + Out: ioutil.Discard, + ChartPath: path, + Keyring: client.Keyring, + Getters: p, + Debug: settings.Debug, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + } + + if err := downloadManager.Update(); err != nil { + return err + } + } + p, err := client.Run(path, vals) + if err != nil { + return err + } + fmt.Fprintf(out, "Successfully packaged chart and saved it to: %s\n", p) } return nil }, } f := cmd.Flags() - f.VarP(&pkg.valueFiles, "values", "f", "specify values in a YAML file or a URL(can specify multiple)") - f.StringArrayVar(&pkg.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - f.StringArrayVar(&pkg.stringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - f.BoolVar(&pkg.save, "save", true, "save packaged chart to local chart repository") - f.BoolVar(&pkg.sign, "sign", false, "use a PGP private key to sign this package") - f.StringVar(&pkg.key, "key", "", "name of the key to use when signing. Used if --sign is true") - f.StringVar(&pkg.keyring, "keyring", defaultKeyring(), "location of a public keyring") - f.StringVar(&pkg.version, "version", "", "set the version on the chart to this semver version") - f.StringVar(&pkg.appVersion, "app-version", "", "set the appVersion on the chart to this version") - f.StringVarP(&pkg.destination, "destination", "d", ".", "location to write the chart.") - f.BoolVarP(&pkg.dependencyUpdate, "dependency-update", "u", false, `update dependencies from "requirements.yaml" to dir "charts/" before packaging`) + f.BoolVar(&client.Sign, "sign", false, "use a PGP private key to sign this package") + f.StringVar(&client.Key, "key", "", "name of the key to use when signing. Used if --sign is true") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "location of a public keyring") + f.StringVar(&client.PassphraseFile, "passphrase-file", "", `location of a file which contains the passphrase for the signing key. Use "-" in order to read from stdin.`) + f.StringVar(&client.Version, "version", "", "set the version on the chart to this semver version") + f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version") + f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.") + f.BoolVarP(&client.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`) return cmd } - -func (p *packageCmd) run() error { - path, err := filepath.Abs(p.path) - if err != nil { - return err - } - - if p.dependencyUpdate { - downloadManager := &downloader.Manager{ - Out: p.out, - ChartPath: path, - HelmHome: settings.Home, - Keyring: p.keyring, - Getters: getter.All(settings), - Debug: settings.Debug, - } - - if err := downloadManager.Update(); err != nil { - return err - } - } - - ch, err := chartutil.LoadDir(path) - if err != nil { - return err - } - - overrideVals, err := vals(p.valueFiles, p.values, p.stringValues) - if err != nil { - return err - } - combinedVals, err := chartutil.CoalesceValues(ch, &chart.Config{Raw: string(overrideVals)}) - if err != nil { - return err - } - newVals, err := combinedVals.YAML() - if err != nil { - return err - } - ch.Values = &chart.Config{Raw: newVals} - - // If version is set, modify the version. - if len(p.version) != 0 { - if err := setVersion(ch, p.version); err != nil { - return err - } - debug("Setting version to %s", p.version) - } - - if p.appVersion != "" { - ch.Metadata.AppVersion = p.appVersion - debug("Setting appVersion to %s", p.appVersion) - } - - 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) - } - - if reqs, err := chartutil.LoadRequirements(ch); err == nil { - if err := checkDependencies(ch, reqs); err != nil { - return err - } - } else { - if err != chartutil.ErrRequirementsNotFound { - return err - } - } - - var dest string - if p.destination == "." { - // Save to the current working directory. - dest, err = os.Getwd() - if err != nil { - return err - } - } else { - // Otherwise save to set destination - dest = p.destination - } - - name, err := chartutil.Save(ch, dest) - if err == nil { - fmt.Fprintf(p.out, "Successfully packaged chart and saved it to: %s\n", name) - } else { - return fmt.Errorf("Failed to save: %s", err) - } - - // Save to $HELM_HOME/local directory. This is second, because we don't want - // the case where we saved here, but didn't save to the default destination. - if p.save { - lr := p.home.LocalRepository() - if err := repo.AddChartToLocalRepo(ch, lr); err != nil { - return err - } - debug("Successfully saved %s to %s\n", name, lr) - } - - if p.sign { - err = p.clearsign(name) - } - - return err -} - -func setVersion(ch *chart.Chart, ver string) error { - // Verify that version is a SemVer, and error out if it is not. - if _, err := semver.NewVersion(ver); err != nil { - return err - } - - // Set the version field on the chart. - ch.Metadata.Version = ver - return nil -} - -func (p *packageCmd) clearsign(filename string) error { - // Load keyring - signer, err := provenance.NewFromKeyring(p.keyring, p.key) - if err != nil { - return err - } - - if err := signer.DecryptKey(promptUser); err != nil { - return err - } - - sig, err := signer.ClearSign(filename) - if err != nil { - return err - } - - debug(sig) - - return ioutil.WriteFile(filename+".prov", []byte(sig), 0755) -} - -// promptUser implements provenance.PassphraseFetcher -func promptUser(name string) ([]byte, error) { - fmt.Printf("Password for key %q > ", name) - pw, err := terminal.ReadPassword(int(syscall.Stdin)) - fmt.Println() - return pw, err -} diff --git a/cmd/helm/package_test.go b/cmd/helm/package_test.go index 4a2df3f546b..ecb3ee36c77 100644 --- a/cmd/helm/package_test.go +++ b/cmd/helm/package_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -17,43 +17,19 @@ package main import ( "bytes" - "io/ioutil" "os" "path/filepath" "regexp" - "strings" "testing" "github.com/spf13/cobra" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/proto/hapi/chart" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" ) -func TestSetVersion(t *testing.T) { - c := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "prow", - Version: "0.0.1", - }, - } - expect := "1.2.3-beta.5" - if err := setVersion(c, expect); err != nil { - t.Fatal(err) - } - - if c.Metadata.Version != expect { - t.Errorf("Expected %q, got %q", expect, c.Metadata.Version) - } - - if err := setVersion(c, "monkeyface"); err == nil { - t.Error("Expected bogus version to return an error.") - } -} - func TestPackage(t *testing.T) { - tests := []struct { name string flags map[string]string @@ -96,6 +72,12 @@ func TestPackage(t *testing.T) { expect: "", hasfile: "alpine-0.1.0.tgz", }, + { + name: "package testdata/testcharts/issue1979", + args: []string{"testdata/testcharts/issue1979"}, + expect: "", + hasfile: "alpine-0.1.0.tgz", + }, { name: "package --destination toot", args: []string{"testdata/testcharts/alpine"}, @@ -103,13 +85,6 @@ func TestPackage(t *testing.T) { expect: "", hasfile: "toot/alpine-0.1.0.tgz", }, - { - name: "package --destination does-not-exist", - args: []string{"testdata/testcharts/alpine"}, - flags: map[string]string{"destination": "does-not-exist"}, - expect: "stat does-not-exist: no such file or directory", - err: true, - }, { name: "package --sign --key=KEY --keyring=KEYRING testdata/testcharts/alpine", args: []string{"testdata/testcharts/alpine"}, @@ -124,282 +99,111 @@ func TestPackage(t *testing.T) { err: true, }, { - name: "package --values does-not-exist", - args: []string{"testdata/testcharts/alpine"}, - flags: map[string]string{"values": "does-not-exist"}, - expect: "does-not-exist: no such file or directory", - err: true, + name: "package testdata/testcharts/chart-bad-type", + args: []string{"testdata/testcharts/chart-bad-type"}, + err: true, }, } - // Because these tests are destructive, we run them in a tempdir. origDir, err := os.Getwd() if err != nil { t.Fatal(err) } - tmp, err := ioutil.TempDir("", "helm-package-test-") - if err != nil { - t.Fatal(err) - } - - t.Logf("Running tests in %s", tmp) - if err := os.Chdir(tmp); err != nil { - t.Fatal(err) - } - - if err := os.Mkdir("toot", 0777); err != nil { - t.Fatal(err) - } - - ensureTestHome(helmpath.Home(tmp), t) - cleanup := resetEnv() - defer func() { - os.Chdir(origDir) - os.RemoveAll(tmp) - cleanup() - }() - - settings.Home = helmpath.Home(tmp) for _, tt := range tests { - buf := bytes.NewBuffer(nil) - c := newPackageCmd(buf) + t.Run(tt.name, func(t *testing.T) { + cachePath := ensure.TempDir(t) + defer testChdir(t, cachePath)() - // This is an unfortunate byproduct of the tmpdir - if v, ok := tt.flags["keyring"]; ok && len(v) > 0 { - tt.flags["keyring"] = filepath.Join(origDir, v) - } + if err := os.MkdirAll("toot", 0777); err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + c := newPackageCmd(&buf) - setFlags(c, tt.flags) - re := regexp.MustCompile(tt.expect) + // This is an unfortunate byproduct of the tmpdir + if v, ok := tt.flags["keyring"]; ok && len(v) > 0 { + tt.flags["keyring"] = filepath.Join(origDir, v) + } - adjustedArgs := make([]string, len(tt.args)) - for i, f := range tt.args { - adjustedArgs[i] = filepath.Join(origDir, f) - } + setFlags(c, tt.flags) + re := regexp.MustCompile(tt.expect) - err := c.RunE(c, adjustedArgs) - if err != nil { - if tt.err && re.MatchString(err.Error()) { - continue + adjustedArgs := make([]string, len(tt.args)) + for i, f := range tt.args { + adjustedArgs[i] = filepath.Join(origDir, f) } - t.Errorf("%q: expected error %q, got %q", tt.name, tt.expect, err) - continue - } - if !re.Match(buf.Bytes()) { - t.Errorf("%q: expected output %q, got %q", tt.name, tt.expect, buf.String()) - } + err := c.RunE(c, adjustedArgs) + if err != nil { + if tt.err && re.MatchString(err.Error()) { + return + } + t.Fatalf("%q: expected error %q, got %q", tt.name, tt.expect, err) + } + + if !re.Match(buf.Bytes()) { + t.Errorf("%q: expected output %q, got %q", tt.name, tt.expect, buf.String()) + } - if len(tt.hasfile) > 0 { - if fi, err := os.Stat(tt.hasfile); err != nil { - t.Errorf("%q: expected file %q, got err %q", tt.name, tt.hasfile, err) - } else if fi.Size() == 0 { - t.Errorf("%q: file %q has zero bytes.", tt.name, tt.hasfile) + if len(tt.hasfile) > 0 { + if fi, err := os.Stat(tt.hasfile); err != nil { + t.Errorf("%q: expected file %q, got err %q", tt.name, tt.hasfile, err) + } else if fi.Size() == 0 { + t.Errorf("%q: file %q has zero bytes.", tt.name, tt.hasfile) + } } - } - if v, ok := tt.flags["sign"]; ok && v == "1" { - if fi, err := os.Stat(tt.hasfile + ".prov"); err != nil { - t.Errorf("%q: expected provenance file", tt.name) - } else if fi.Size() == 0 { - t.Errorf("%q: provenance file is empty", tt.name) + if v, ok := tt.flags["sign"]; ok && v == "1" { + if fi, err := os.Stat(tt.hasfile + ".prov"); err != nil { + t.Errorf("%q: expected provenance file", tt.name) + } else if fi.Size() == 0 { + t.Errorf("%q: provenance file is empty", tt.name) + } } - } + }) } } func TestSetAppVersion(t *testing.T) { var ch *chart.Chart expectedAppVersion := "app-version-foo" - tmp, _ := ioutil.TempDir("", "helm-package-app-version-") - - thome, err := tempHelmHome(t) - if err != nil { - t.Fatal(err) - } - cleanup := resetEnv() - defer func() { - os.RemoveAll(tmp) - os.RemoveAll(thome.String()) - cleanup() - }() - settings.Home = helmpath.Home(thome) + dir := ensure.TempDir(t) c := newPackageCmd(&bytes.Buffer{}) flags := map[string]string{ - "destination": tmp, + "destination": dir, "app-version": expectedAppVersion, } setFlags(c, flags) - err = c.RunE(c, []string{"testdata/testcharts/alpine"}) - if err != nil { + if err := c.RunE(c, []string{"testdata/testcharts/alpine"}); err != nil { t.Errorf("unexpected error %q", err) } - chartPath := filepath.Join(tmp, "alpine-0.1.0.tgz") + chartPath := filepath.Join(dir, "alpine-0.1.0.tgz") if fi, err := os.Stat(chartPath); err != nil { t.Errorf("expected file %q, got err %q", chartPath, err) } else if fi.Size() == 0 { t.Errorf("file %q has zero bytes.", chartPath) } - ch, err = chartutil.Load(chartPath) + ch, err := loader.Load(chartPath) if err != nil { - t.Errorf("unexpected error loading packaged chart: %v", err) + t.Fatalf("unexpected error loading packaged chart: %v", err) } if ch.Metadata.AppVersion != expectedAppVersion { t.Errorf("expected app-version %q, found %q", expectedAppVersion, ch.Metadata.AppVersion) } } -func TestPackageValues(t *testing.T) { - testCases := []struct { - desc string - args []string - valuefilesContents []string - flags map[string]string - expected []string - }{ - { - desc: "helm package, single values file", - args: []string{"testdata/testcharts/alpine"}, - valuefilesContents: []string{"Name: chart-name-foo"}, - expected: []string{"Name: chart-name-foo"}, - }, - { - desc: "helm package, multiple values files", - args: []string{"testdata/testcharts/alpine"}, - valuefilesContents: []string{"Name: chart-name-foo", "foo: bar"}, - expected: []string{"Name: chart-name-foo", "foo: bar"}, - }, - { - desc: "helm package, with set option", - args: []string{"testdata/testcharts/alpine"}, - flags: map[string]string{"set": "Name=chart-name-foo"}, - expected: []string{"Name: chart-name-foo"}, - }, - { - desc: "helm package, set takes precedence over value file", - args: []string{"testdata/testcharts/alpine"}, - valuefilesContents: []string{"Name: chart-name-foo"}, - flags: map[string]string{"set": "Name=chart-name-bar"}, - expected: []string{"Name: chart-name-bar"}, - }, - } - - thome, err := tempHelmHome(t) - if err != nil { - t.Fatal(err) - } - cleanup := resetEnv() - defer func() { - os.RemoveAll(thome.String()) - cleanup() - }() - - settings.Home = thome - - for _, tc := range testCases { - var files []string - for _, contents := range tc.valuefilesContents { - f, err := createValuesFile(contents) - if err != nil { - t.Errorf("%q unexpected error creating temporary values file: %q", tc.desc, err) - } - defer os.RemoveAll(filepath.Dir(f)) - files = append(files, f) - } - valueFiles := strings.Join(files, ",") - - expected, err := chartutil.ReadValues([]byte(strings.Join(tc.expected, "\n"))) - if err != nil { - t.Errorf("unexpected error parsing values: %q", err) - } - - runAndVerifyPackageCommandValues(t, tc.args, tc.flags, valueFiles, expected) - } -} - -func runAndVerifyPackageCommandValues(t *testing.T, args []string, flags map[string]string, valueFiles string, expected chartutil.Values) { - outputDir, err := ioutil.TempDir("", "helm-package") - if err != nil { - t.Errorf("unexpected error creating temporary output directory: %q", err) - } - defer os.RemoveAll(outputDir) - - if len(flags) == 0 { - flags = make(map[string]string) - } - flags["destination"] = outputDir - - if len(valueFiles) > 0 { - flags["values"] = valueFiles - } - - cmd := newPackageCmd(&bytes.Buffer{}) - setFlags(cmd, flags) - err = cmd.RunE(cmd, args) - if err != nil { - t.Errorf("unexpected error: %q", err) - } - - outputFile := filepath.Join(outputDir, "alpine-0.1.0.tgz") - verifyOutputChartExists(t, outputFile) - - var actual chartutil.Values - actual, err = getChartValues(outputFile) - if err != nil { - t.Errorf("unexpected error extracting chart values: %q", err) - } - - verifyValues(t, actual, expected) -} - -func createValuesFile(data string) (string, error) { - outputDir, err := ioutil.TempDir("", "values-file") - if err != nil { - return "", err - } - - outputFile := filepath.Join(outputDir, "values.yaml") - if err = ioutil.WriteFile(outputFile, []byte(data), 0755); err != nil { - os.RemoveAll(outputFile) - return "", err - } - - return outputFile, nil -} - -func getChartValues(chartPath string) (chartutil.Values, error) { - - chart, err := chartutil.Load(chartPath) - if err != nil { - return nil, err - } - - return chartutil.ReadValues([]byte(chart.Values.Raw)) -} - -func verifyValues(t *testing.T, actual, expected chartutil.Values) { - for key, value := range expected.AsMap() { - if got := actual[key]; got != value { - t.Errorf("Expected %q, got %q (%v)", value, got, actual) - } - } -} - -func verifyOutputChartExists(t *testing.T, chartPath string) { - if chartFile, err := os.Stat(chartPath); err != nil { - t.Errorf("expected file %q, got err %q", chartPath, err) - } else if chartFile.Size() == 0 { - t.Errorf("file %q has zero bytes.", chartPath) - } -} - func setFlags(cmd *cobra.Command, flags map[string]string) { dest := cmd.Flags() for f, v := range flags { dest.Set(f, v) } } + +func TestPackageFileCompletion(t *testing.T) { + checkFileCompletion(t, "package", true) + checkFileCompletion(t, "package mypath", true) // Multiple paths can be given +} diff --git a/cmd/helm/plugin.go b/cmd/helm/plugin.go index cf0b02f0947..8e1044f5465 100644 --- a/cmd/helm/plugin.go +++ b/cmd/helm/plugin.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,14 +16,14 @@ limitations under the License. package main import ( - "fmt" "io" "os" "os/exec" - "k8s.io/helm/pkg/plugin" - + "github.com/pkg/errors" "github.com/spf13/cobra" + + "helm.sh/helm/v3/pkg/plugin" ) const pluginHelp = ` @@ -33,13 +33,13 @@ Manage client-side Helm plugins. func newPluginCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "plugin", - Short: "add, list, or remove Helm plugins", + Short: "install, list, or uninstall Helm plugins", Long: pluginHelp, } cmd.AddCommand( newPluginInstallCmd(out), newPluginListCmd(out), - newPluginRemoveCmd(out), + newPluginUninstallCmd(out), newPluginUpdateCmd(out), ) return cmd @@ -47,7 +47,7 @@ func newPluginCmd(out io.Writer) *cobra.Command { // runHook will execute a plugin hook. func runHook(p *plugin.Plugin, event string) error { - hook := p.Metadata.Hooks.Get(event) + hook := p.Metadata.Hooks[event] if hook == "" { return nil } @@ -64,7 +64,7 @@ func runHook(p *plugin.Plugin, event string) error { if err := prog.Run(); err != nil { if eerr, ok := err.(*exec.ExitError); ok { os.Stderr.Write(eerr.Stderr) - return fmt.Errorf("plugin %s hook for %q exited with error", event, p.Metadata.Name) + return errors.Errorf("plugin %s hook for %q exited with error", event, p.Metadata.Name) } return err } diff --git a/cmd/helm/plugin_install.go b/cmd/helm/plugin_install.go index f3617880825..4e8ee327b88 100644 --- a/cmd/helm/plugin_install.go +++ b/cmd/helm/plugin_install.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -19,57 +19,59 @@ import ( "fmt" "io" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/plugin" - "k8s.io/helm/pkg/plugin/installer" - + "github.com/pkg/errors" "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/plugin" + "helm.sh/helm/v3/pkg/plugin/installer" ) -type pluginInstallCmd struct { +type pluginInstallOptions struct { source string version string - home helmpath.Home - out io.Writer } const pluginInstallDesc = ` This command allows you to install a plugin from a url to a VCS repo or a local path. - -Example usage: - $ helm plugin install https://github.com/technosophos/helm-template ` func newPluginInstallCmd(out io.Writer) *cobra.Command { - pcmd := &pluginInstallCmd{out: out} + o := &pluginInstallOptions{} cmd := &cobra.Command{ - Use: "install [options] ...", - Short: "install one or more Helm plugins", - Long: pluginInstallDesc, + Use: "install [options] ...", + Short: "install one or more Helm plugins", + Long: pluginInstallDesc, + Aliases: []string{"add"}, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + // We do file completion, in case the plugin is local + return nil, cobra.ShellCompDirectiveDefault + } + // No more completion once the plugin path has been specified + return nil, cobra.ShellCompDirectiveNoFileComp + }, PreRunE: func(cmd *cobra.Command, args []string) error { - return pcmd.complete(args) + return o.complete(args) }, RunE: func(cmd *cobra.Command, args []string) error { - return pcmd.run() + return o.run(out) }, } - cmd.Flags().StringVar(&pcmd.version, "version", "", "specify a version constraint. If this is not specified, the latest version is installed") + cmd.Flags().StringVar(&o.version, "version", "", "specify a version constraint. If this is not specified, the latest version is installed") return cmd } -func (pcmd *pluginInstallCmd) complete(args []string) error { - if err := checkArgsLength(len(args), "plugin"); err != nil { - return err - } - pcmd.source = args[0] - pcmd.home = settings.Home +func (o *pluginInstallOptions) complete(args []string) error { + o.source = args[0] return nil } -func (pcmd *pluginInstallCmd) run() error { +func (o *pluginInstallOptions) run(out io.Writer) error { installer.Debug = settings.Debug - i, err := installer.NewForSource(pcmd.source, pcmd.version, pcmd.home) + i, err := installer.NewForSource(o.source, o.version) if err != nil { return err } @@ -80,13 +82,13 @@ func (pcmd *pluginInstallCmd) run() error { debug("loading plugin from %s", i.Path()) p, err := plugin.LoadDir(i.Path()) if err != nil { - return err + return errors.Wrap(err, "plugin is installed but unusable") } if err := runHook(p, plugin.Install); err != nil { return err } - fmt.Fprintf(pcmd.out, "Installed plugin: %s\n", p.Metadata.Name) + fmt.Fprintf(out, "Installed plugin: %s\n", p.Metadata.Name) return nil } diff --git a/cmd/helm/plugin_list.go b/cmd/helm/plugin_list.go index e7618f38a71..6c3926a9d55 100644 --- a/cmd/helm/plugin_list.go +++ b/cmd/helm/plugin_list.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -18,43 +18,74 @@ package main import ( "fmt" "io" - - "k8s.io/helm/pkg/helm/helmpath" + "strings" "github.com/gosuri/uitable" "github.com/spf13/cobra" -) -type pluginListCmd struct { - home helmpath.Home - out io.Writer -} + "helm.sh/helm/v3/pkg/plugin" +) func newPluginListCmd(out io.Writer) *cobra.Command { - pcmd := &pluginListCmd{out: out} cmd := &cobra.Command{ - Use: "list", - Short: "list installed Helm plugins", + Use: "list", + Aliases: []string{"ls"}, + Short: "list installed Helm plugins", + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { - pcmd.home = settings.Home - return pcmd.run() + debug("pluginDirs: %s", settings.PluginsDirectory) + plugins, err := plugin.FindPlugins(settings.PluginsDirectory) + if err != nil { + return err + } + + table := uitable.New() + table.AddRow("NAME", "VERSION", "DESCRIPTION") + for _, p := range plugins { + table.AddRow(p.Metadata.Name, p.Metadata.Version, p.Metadata.Description) + } + fmt.Fprintln(out, table) + return nil }, } return cmd } -func (pcmd *pluginListCmd) run() error { - debug("pluginDirs: %s", settings.PluginDirs()) - plugins, err := findPlugins(settings.PluginDirs()) - if err != nil { - return err +// Returns all plugins from plugins, except those with names matching ignoredPluginNames +func filterPlugins(plugins []*plugin.Plugin, ignoredPluginNames []string) []*plugin.Plugin { + // if ignoredPluginNames is nil, just return plugins + if ignoredPluginNames == nil { + return plugins } - table := uitable.New() - table.AddRow("NAME", "VERSION", "DESCRIPTION") - for _, p := range plugins { - table.AddRow(p.Metadata.Name, p.Metadata.Version, p.Metadata.Description) + var filteredPlugins []*plugin.Plugin + for _, plugin := range plugins { + found := false + for _, ignoredName := range ignoredPluginNames { + if plugin.Metadata.Name == ignoredName { + found = true + break + } + } + if !found { + filteredPlugins = append(filteredPlugins, plugin) + } + } + + return filteredPlugins +} + +// Provide dynamic auto-completion for plugin names +func compListPlugins(toComplete string, ignoredPluginNames []string) []string { + var pNames []string + plugins, err := plugin.FindPlugins(settings.PluginsDirectory) + if err == nil && len(plugins) > 0 { + filteredPlugins := filterPlugins(plugins, ignoredPluginNames) + for _, p := range filteredPlugins { + if strings.HasPrefix(p.Metadata.Name, toComplete) { + pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage)) + } + } } - fmt.Fprintln(pcmd.out, table) - return nil + return pNames } diff --git a/cmd/helm/plugin_remove.go b/cmd/helm/plugin_remove.go deleted file mode 100644 index ec11547344e..00000000000 --- a/cmd/helm/plugin_remove.go +++ /dev/null @@ -1,99 +0,0 @@ -/* -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" - "strings" - - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/plugin" - - "github.com/spf13/cobra" -) - -type pluginRemoveCmd struct { - names []string - home helmpath.Home - out io.Writer -} - -func newPluginRemoveCmd(out io.Writer) *cobra.Command { - pcmd := &pluginRemoveCmd{out: out} - cmd := &cobra.Command{ - Use: "remove ...", - Short: "remove one or more Helm plugins", - PreRunE: func(cmd *cobra.Command, args []string) error { - return pcmd.complete(args) - }, - RunE: func(cmd *cobra.Command, args []string) error { - return pcmd.run() - }, - } - return cmd -} - -func (pcmd *pluginRemoveCmd) complete(args []string) error { - if len(args) == 0 { - return errors.New("please provide plugin name to remove") - } - pcmd.names = args - pcmd.home = settings.Home - return nil -} - -func (pcmd *pluginRemoveCmd) run() error { - debug("loading installed plugins from %s", settings.PluginDirs()) - plugins, err := findPlugins(settings.PluginDirs()) - if err != nil { - return err - } - var errorPlugins []string - for _, name := range pcmd.names { - if found := findPlugin(plugins, name); found != nil { - if err := removePlugin(found); err != nil { - errorPlugins = append(errorPlugins, fmt.Sprintf("Failed to remove plugin %s, got error (%v)", name, err)) - } else { - fmt.Fprintf(pcmd.out, "Removed plugin: %s\n", name) - } - } else { - errorPlugins = append(errorPlugins, fmt.Sprintf("Plugin: %s not found", name)) - } - } - if len(errorPlugins) > 0 { - return fmt.Errorf(strings.Join(errorPlugins, "\n")) - } - return nil -} - -func removePlugin(p *plugin.Plugin) error { - if err := os.Remove(p.Dir); err != nil { - return err - } - return runHook(p, plugin.Delete) -} - -func findPlugin(plugins []*plugin.Plugin, name string) *plugin.Plugin { - for _, p := range plugins { - if p.Metadata.Name == name { - return p - } - } - return nil -} diff --git a/cmd/helm/plugin_test.go b/cmd/helm/plugin_test.go index 2a4a0e9aadb..87fd1768112 100644 --- a/cmd/helm/plugin_test.go +++ b/cmd/helm/plugin_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -18,34 +18,53 @@ package main import ( "bytes" "os" - "path/filepath" "runtime" + "sort" "strings" "testing" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/plugin" - "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "helm.sh/helm/v3/pkg/release" ) func TestManuallyProcessArgs(t *testing.T) { input := []string{ "--debug", "--foo", "bar", - "--host", "example.com", + "--kubeconfig=/home/foo", + "--kubeconfig", "/home/foo", + "--kube-context=test1", "--kube-context", "test1", + "--kube-as-user", "pikachu", + "--kube-as-group", "teatime", + "--kube-as-group", "admins", + "-n=test2", + "-n", "test2", + "--namespace=test2", + "--namespace", "test2", "--home=/tmp", - "--tiller-namespace=hello", "command", } expectKnown := []string{ - "--debug", "--host", "example.com", "--kube-context", "test1", "--home=/tmp", "--tiller-namespace=hello", + "--debug", + "--kubeconfig=/home/foo", + "--kubeconfig", "/home/foo", + "--kube-context=test1", + "--kube-context", "test1", + "--kube-as-user", "pikachu", + "--kube-as-group", "teatime", + "--kube-as-group", "admins", + "-n=test2", + "-n", "test2", + "--namespace=test2", + "--namespace", "test2", } expectUnknown := []string{ - "--foo", "bar", "command", + "--foo", "bar", "--home=/tmp", "command", } known, unknown := manuallyProcessArgs(input) @@ -64,27 +83,22 @@ func TestManuallyProcessArgs(t *testing.T) { } func TestLoadPlugins(t *testing.T) { - cleanup := resetEnv() - defer cleanup() + settings.PluginsDirectory = "testdata/helmhome/helm/plugins" + settings.RepositoryConfig = "testdata/helmhome/helm/repositories.yaml" + settings.RepositoryCache = "testdata/helmhome/helm/repository" - settings.Home = "testdata/helmhome" - - os.Setenv("HELM_HOME", settings.Home.String()) - hh := settings.Home - - out := bytes.NewBuffer(nil) - cmd := &cobra.Command{} - loadPlugins(cmd, out) + var ( + out bytes.Buffer + cmd cobra.Command + ) + loadPlugins(&cmd, &out) envs := strings.Join([]string{ "fullenv", - hh.Plugins() + "/fullenv", - hh.Plugins(), - hh.String(), - hh.Repository(), - hh.RepositoryFile(), - hh.Cache(), - hh.LocalRepository(), + "testdata/helmhome/helm/plugins/fullenv", + "testdata/helmhome/helm/plugins", + "testdata/helmhome/helm/repositories.yaml", + "testdata/helmhome/helm/repository", os.Args[0], }, "\n") @@ -95,11 +109,13 @@ func TestLoadPlugins(t *testing.T) { long string expect string args []string + code int }{ - {"args", "echo args", "This echos args", "-a -b -c\n", []string{"-a", "-b", "-c"}}, - {"echo", "echo stuff", "This echos stuff", "hello\n", []string{}}, - {"env", "env stuff", "show the env", hh.String() + "\n", []string{}}, - {"fullenv", "show env vars", "show all env vars", envs + "\n", []string{}}, + {"args", "echo args", "This echos args", "-a -b -c\n", []string{"-a", "-b", "-c"}, 0}, + {"echo", "echo stuff", "This echos stuff", "hello\n", []string{}, 0}, + {"env", "env stuff", "show the env", "env\n", []string{}, 0}, + {"exitwith", "exitwith code", "This exits with the specified exit code", "", []string{"2"}, 2}, + {"fullenv", "show env vars", "show all env vars", envs + "\n", []string{}, 0}, } plugins := cmd.Commands() @@ -126,7 +142,17 @@ func TestLoadPlugins(t *testing.T) { // tests until this is fixed if runtime.GOOS != "windows" { if err := pp.RunE(pp, tt.args); err != nil { - t.Errorf("Error running %s: %s", tt.use, err) + if tt.code > 0 { + perr, ok := err.(pluginError) + if !ok { + t.Errorf("Expected %s to return pluginError: got %v(%T)", tt.use, err, err) + } + if perr.code != tt.code { + t.Errorf("Expected %s to return %d: got %d", tt.use, tt.code, perr.code) + } + } else { + t.Errorf("Error running %s: %+v", tt.use, err) + } } if out.String() != tt.expect { t.Errorf("Expected %s to output:\n%s\ngot\n%s", tt.use, tt.expect, out.String()) @@ -135,11 +161,137 @@ func TestLoadPlugins(t *testing.T) { } } -func TestLoadPlugins_HelmNoPlugins(t *testing.T) { - cleanup := resetEnv() - defer cleanup() +type staticCompletionDetails struct { + use string + validArgs []string + flags []string + next []staticCompletionDetails +} + +func TestLoadPluginsForCompletion(t *testing.T) { + settings.PluginsDirectory = "testdata/helmhome/helm/plugins" + + var out bytes.Buffer + + cmd := &cobra.Command{ + Use: "completion", + } + + loadPlugins(cmd, &out) + + tests := []staticCompletionDetails{ + {"args", []string{}, []string{}, []staticCompletionDetails{}}, + {"echo", []string{}, []string{}, []staticCompletionDetails{}}, + {"env", []string{}, []string{"global"}, []staticCompletionDetails{ + {"list", []string{}, []string{"a", "all", "log"}, []staticCompletionDetails{}}, + {"remove", []string{"all", "one"}, []string{}, []staticCompletionDetails{}}, + }}, + {"exitwith", []string{}, []string{}, []staticCompletionDetails{ + {"code", []string{}, []string{"a", "b"}, []staticCompletionDetails{}}, + }}, + {"fullenv", []string{}, []string{"q", "z"}, []staticCompletionDetails{ + {"empty", []string{}, []string{}, []staticCompletionDetails{}}, + {"full", []string{}, []string{}, []staticCompletionDetails{ + {"less", []string{}, []string{"a", "all"}, []staticCompletionDetails{}}, + {"more", []string{"one", "two"}, []string{"b", "ball"}, []staticCompletionDetails{}}, + }}, + }}, + } + checkCommand(t, cmd.Commands(), tests) +} + +func checkCommand(t *testing.T, plugins []*cobra.Command, tests []staticCompletionDetails) { + if len(plugins) != len(tests) { + t.Fatalf("Expected commands %v, got %v", tests, plugins) + } + + for i := 0; i < len(plugins); i++ { + pp := plugins[i] + tt := tests[i] + if pp.Use != tt.use { + t.Errorf("%s: Expected Use=%q, got %q", pp.Name(), tt.use, pp.Use) + } + + targs := tt.validArgs + pargs := pp.ValidArgs + if len(targs) != len(pargs) { + t.Fatalf("%s: expected args %v, got %v", pp.Name(), targs, pargs) + } - settings.Home = "testdata/helmhome" + sort.Strings(targs) + sort.Strings(pargs) + for j := range targs { + if targs[j] != pargs[j] { + t.Errorf("%s: expected validArg=%q, got %q", pp.Name(), targs[j], pargs[j]) + } + } + + tflags := tt.flags + var pflags []string + pp.LocalFlags().VisitAll(func(flag *pflag.Flag) { + pflags = append(pflags, flag.Name) + if len(flag.Shorthand) > 0 && flag.Shorthand != flag.Name { + pflags = append(pflags, flag.Shorthand) + } + }) + if len(tflags) != len(pflags) { + t.Fatalf("%s: expected flags %v, got %v", pp.Name(), tflags, pflags) + } + + sort.Strings(tflags) + sort.Strings(pflags) + for j := range tflags { + if tflags[j] != pflags[j] { + t.Errorf("%s: expected flag=%q, got %q", pp.Name(), tflags[j], pflags[j]) + } + } + // Check the next level + checkCommand(t, pp.Commands(), tt.next) + } +} + +func TestPluginDynamicCompletion(t *testing.T) { + + tests := []cmdTestCase{{ + name: "completion for plugin", + cmd: "__complete args ''", + golden: "output/plugin_args_comp.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin with flag", + cmd: "__complete args --myflag ''", + golden: "output/plugin_args_flag_comp.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin with global flag", + cmd: "__complete args --namespace mynamespace ''", + golden: "output/plugin_args_ns_comp.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin with multiple args", + cmd: "__complete args --myflag --namespace mynamespace start", + golden: "output/plugin_args_many_args_comp.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin no directive", + cmd: "__complete echo -n mynamespace ''", + golden: "output/plugin_echo_no_directive.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin bad directive", + cmd: "__complete echo ''", + golden: "output/plugin_echo_bad_directive.txt", + rels: []*release.Release{}, + }} + for _, test := range tests { + settings.PluginsDirectory = "testdata/helmhome/helm/plugins" + runTestCmd(t, []cmdTestCase{test}) + } +} + +func TestLoadPlugins_HelmNoPlugins(t *testing.T) { + settings.PluginsDirectory = "testdata/helmhome/helm/plugins" + settings.RepositoryConfig = "testdata/helmhome/helm/repository" os.Setenv("HELM_NO_PLUGINS", "1") @@ -153,35 +305,69 @@ func TestLoadPlugins_HelmNoPlugins(t *testing.T) { } } -func TestSetupEnv(t *testing.T) { - name := "pequod" - settings.Home = helmpath.Home("testdata/helmhome") - base := filepath.Join(settings.Home.Plugins(), name) - settings.Debug = true - defer func() { - settings.Debug = false - }() - - plugin.SetupPluginEnv(settings, name, base) - for _, tt := range []struct { - name string - expect string - }{ - {"HELM_PLUGIN_NAME", name}, - {"HELM_PLUGIN_DIR", base}, - {"HELM_PLUGIN", settings.Home.Plugins()}, - {"HELM_DEBUG", "1"}, - {"HELM_HOME", settings.Home.String()}, - {"HELM_PATH_REPOSITORY", settings.Home.Repository()}, - {"HELM_PATH_REPOSITORY_FILE", settings.Home.RepositoryFile()}, - {"HELM_PATH_CACHE", settings.Home.Cache()}, - {"HELM_PATH_LOCAL_REPOSITORY", settings.Home.LocalRepository()}, - {"HELM_PATH_STARTER", settings.Home.Starters()}, - {"TILLER_HOST", settings.TillerHost}, - {"TILLER_NAMESPACE", settings.TillerNamespace}, - } { - if got := os.Getenv(tt.name); got != tt.expect { - t.Errorf("Expected $%s=%q, got %q", tt.name, tt.expect, got) - } +func TestPluginCmdsCompletion(t *testing.T) { + + tests := []cmdTestCase{{ + name: "completion for plugin update", + cmd: "__complete plugin update ''", + golden: "output/plugin_list_comp.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin update repetition", + cmd: "__complete plugin update args ''", + golden: "output/plugin_repeat_comp.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin uninstall", + cmd: "__complete plugin uninstall ''", + golden: "output/plugin_list_comp.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin uninstall repetition", + cmd: "__complete plugin uninstall args ''", + golden: "output/plugin_repeat_comp.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin list", + cmd: "__complete plugin list ''", + golden: "output/empty_nofile_comp.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin install no args", + cmd: "__complete plugin install ''", + golden: "output/empty_default_comp.txt", + rels: []*release.Release{}, + }, { + name: "completion for plugin install one arg", + cmd: "__complete plugin list /tmp ''", + golden: "output/empty_nofile_comp.txt", + rels: []*release.Release{}, + }, {}} + for _, test := range tests { + settings.PluginsDirectory = "testdata/helmhome/helm/plugins" + runTestCmd(t, []cmdTestCase{test}) } } + +func TestPluginFileCompletion(t *testing.T) { + checkFileCompletion(t, "plugin", false) +} + +func TestPluginInstallFileCompletion(t *testing.T) { + checkFileCompletion(t, "plugin install", true) + checkFileCompletion(t, "plugin install mypath", false) +} + +func TestPluginListFileCompletion(t *testing.T) { + checkFileCompletion(t, "plugin list", false) +} + +func TestPluginUninstallFileCompletion(t *testing.T) { + checkFileCompletion(t, "plugin uninstall", false) + checkFileCompletion(t, "plugin uninstall myplugin", false) +} + +func TestPluginUpdateFileCompletion(t *testing.T) { + checkFileCompletion(t, "plugin update", false) + checkFileCompletion(t, "plugin update myplugin", false) +} diff --git a/cmd/helm/plugin_uninstall.go b/cmd/helm/plugin_uninstall.go new file mode 100644 index 00000000000..ee4a47beb87 --- /dev/null +++ b/cmd/helm/plugin_uninstall.go @@ -0,0 +1,100 @@ +/* +Copyright The Helm Authors. +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" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "helm.sh/helm/v3/pkg/plugin" +) + +type pluginUninstallOptions struct { + names []string +} + +func newPluginUninstallCmd(out io.Writer) *cobra.Command { + o := &pluginUninstallOptions{} + + cmd := &cobra.Command{ + Use: "uninstall ...", + Aliases: []string{"rm", "remove"}, + Short: "uninstall one or more Helm plugins", + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp + }, + PreRunE: func(cmd *cobra.Command, args []string) error { + return o.complete(args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return o.run(out) + }, + } + return cmd +} + +func (o *pluginUninstallOptions) complete(args []string) error { + if len(args) == 0 { + return errors.New("please provide plugin name to uninstall") + } + o.names = args + return nil +} + +func (o *pluginUninstallOptions) run(out io.Writer) error { + debug("loading installed plugins from %s", settings.PluginsDirectory) + plugins, err := plugin.FindPlugins(settings.PluginsDirectory) + if err != nil { + return err + } + var errorPlugins []string + for _, name := range o.names { + if found := findPlugin(plugins, name); found != nil { + if err := uninstallPlugin(found); err != nil { + errorPlugins = append(errorPlugins, fmt.Sprintf("Failed to uninstall plugin %s, got error (%v)", name, err)) + } else { + fmt.Fprintf(out, "Uninstalled plugin: %s\n", name) + } + } else { + errorPlugins = append(errorPlugins, fmt.Sprintf("Plugin: %s not found", name)) + } + } + if len(errorPlugins) > 0 { + return errors.Errorf(strings.Join(errorPlugins, "\n")) + } + return nil +} + +func uninstallPlugin(p *plugin.Plugin) error { + if err := os.RemoveAll(p.Dir); err != nil { + return err + } + return runHook(p, plugin.Delete) +} + +func findPlugin(plugins []*plugin.Plugin, name string) *plugin.Plugin { + for _, p := range plugins { + if p.Metadata.Name == name { + return p + } + } + return nil +} diff --git a/cmd/helm/plugin_update.go b/cmd/helm/plugin_update.go index d3778764d79..4515acdbb34 100644 --- a/cmd/helm/plugin_update.go +++ b/cmd/helm/plugin_update.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,76 +16,77 @@ limitations under the License. package main import ( - "errors" "fmt" "io" "path/filepath" "strings" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/plugin" - "k8s.io/helm/pkg/plugin/installer" - + "github.com/pkg/errors" "github.com/spf13/cobra" + + "helm.sh/helm/v3/pkg/plugin" + "helm.sh/helm/v3/pkg/plugin/installer" ) -type pluginUpdateCmd struct { +type pluginUpdateOptions struct { names []string - home helmpath.Home - out io.Writer } func newPluginUpdateCmd(out io.Writer) *cobra.Command { - pcmd := &pluginUpdateCmd{out: out} + o := &pluginUpdateOptions{} + cmd := &cobra.Command{ - Use: "update ...", - Short: "update one or more Helm plugins", + Use: "update ...", + Aliases: []string{"up"}, + Short: "update one or more Helm plugins", + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compListPlugins(toComplete, args), cobra.ShellCompDirectiveNoFileComp + }, PreRunE: func(cmd *cobra.Command, args []string) error { - return pcmd.complete(args) + return o.complete(args) }, RunE: func(cmd *cobra.Command, args []string) error { - return pcmd.run() + return o.run(out) }, } return cmd } -func (pcmd *pluginUpdateCmd) complete(args []string) error { +func (o *pluginUpdateOptions) complete(args []string) error { if len(args) == 0 { return errors.New("please provide plugin name to update") } - pcmd.names = args - pcmd.home = settings.Home + o.names = args return nil } -func (pcmd *pluginUpdateCmd) run() error { +func (o *pluginUpdateOptions) run(out io.Writer) error { installer.Debug = settings.Debug - debug("loading installed plugins from %s", settings.PluginDirs()) - plugins, err := findPlugins(settings.PluginDirs()) + debug("loading installed plugins from %s", settings.PluginsDirectory) + plugins, err := plugin.FindPlugins(settings.PluginsDirectory) if err != nil { return err } var errorPlugins []string - for _, name := range pcmd.names { + for _, name := range o.names { if found := findPlugin(plugins, name); found != nil { - if err := updatePlugin(found, pcmd.home); err != nil { + if err := updatePlugin(found); err != nil { errorPlugins = append(errorPlugins, fmt.Sprintf("Failed to update plugin %s, got error (%v)", name, err)) } else { - fmt.Fprintf(pcmd.out, "Updated plugin: %s\n", name) + fmt.Fprintf(out, "Updated plugin: %s\n", name) } } else { errorPlugins = append(errorPlugins, fmt.Sprintf("Plugin: %s not found", name)) } } if len(errorPlugins) > 0 { - return fmt.Errorf(strings.Join(errorPlugins, "\n")) + return errors.Errorf(strings.Join(errorPlugins, "\n")) } return nil } -func updatePlugin(p *plugin.Plugin, home helmpath.Home) error { +func updatePlugin(p *plugin.Plugin) error { exactLocation, err := filepath.EvalSymlinks(p.Dir) if err != nil { return err @@ -95,7 +96,7 @@ func updatePlugin(p *plugin.Plugin, home helmpath.Home) error { return err } - i, err := installer.FindSource(absExactLocation, home) + i, err := installer.FindSource(absExactLocation) if err != nil { return err } diff --git a/cmd/helm/printer.go b/cmd/helm/printer.go index ebb24bf7d80..7cf7bf994d7 100644 --- a/cmd/helm/printer.go +++ b/cmd/helm/printer.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,55 +17,10 @@ limitations under the License. package main import ( - "fmt" "io" "text/template" - "time" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/timeconv" ) -var printReleaseTemplate = `REVISION: {{.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}} -` - -func printRelease(out io.Writer, rel *release.Release) error { - if rel == nil { - return nil - } - - cfg, err := chartutil.CoalesceValues(rel.Chart, rel.Config) - if err != nil { - return err - } - cfgStr, err := cfg.YAML() - if err != nil { - return err - } - - data := map[string]interface{}{ - "Release": rel, - "ComputedValues": cfgStr, - "ReleaseDate": timeconv.Format(rel.Info.LastDeployed, time.ANSIC), - } - return tpl(printReleaseTemplate, data, out) -} - func tpl(t string, vals map[string]interface{}, out io.Writer) error { tt, err := template.New("_").Parse(t) if err != nil { @@ -73,10 +28,3 @@ func tpl(t string, vals map[string]interface{}, out io.Writer) error { } return tt.Execute(out, vals) } - -func debug(format string, args ...interface{}) { - if settings.Debug { - format = fmt.Sprintf("[debug] %s\n", format) - fmt.Printf(format, args...) - } -} diff --git a/cmd/helm/pull.go b/cmd/helm/pull.go new file mode 100644 index 00000000000..7711320f1dd --- /dev/null +++ b/cmd/helm/pull.go @@ -0,0 +1,105 @@ +/* +Copyright The Helm Authors. + +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" + "log" + "strings" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +const pullDesc = ` +Retrieve a package from a package repository, and download it locally. + +This is useful for fetching packages to inspect, modify, or repackage. It can +also be used to perform cryptographic verification of a chart without installing +the chart. + +There are options for unpacking the chart after download. This will create a +directory for the chart and uncompress into that directory. + +If the --verify flag is specified, the requested chart MUST have a provenance +file, and MUST pass the verification process. Failure in any part of this will +result in an error, and the chart will not be saved locally. +` + +func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewPullWithOpts(action.WithConfig(cfg)) + + cmd := &cobra.Command{ + Use: "pull [chart URL | repo/chartname] [...]", + Short: "download a chart from a repository and (optionally) unpack it in local directory", + Aliases: []string{"fetch"}, + Long: pullDesc, + Args: require.MinimumNArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compListCharts(toComplete, false) + }, + RunE: func(cmd *cobra.Command, args []string) error { + client.Settings = settings + if client.Version == "" && client.Devel { + debug("setting version to >0.0.0-0") + client.Version = ">0.0.0-0" + } + + if strings.HasPrefix(args[0], "oci://") { + if !FeatureGateOCI.IsEnabled() { + return FeatureGateOCI.Error() + } + } + + for i := 0; i < len(args); i++ { + output, err := client.Run(args[i]) + if err != nil { + return err + } + fmt.Fprint(out, output) + } + return nil + }, + } + + f := cmd.Flags() + f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") + f.BoolVar(&client.Untar, "untar", false, "if set to true, will untar the chart after downloading it") + f.BoolVar(&client.VerifyLater, "prov", false, "fetch the provenance file, but don't perform verification") + f.StringVar(&client.UntarDir, "untardir", ".", "if untar is specified, this flag specifies the name of the directory into which the chart is expanded") + f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and tardir are specified, tardir is appended to this") + addChartPathOptionsFlags(f, &client.ChartPathOptions) + + err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 1 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compVersionFlag(args[0], toComplete) + }) + + if err != nil { + log.Fatal(err) + } + + return cmd +} diff --git a/cmd/helm/pull_test.go b/cmd/helm/pull_test.go new file mode 100644 index 00000000000..51cdfdfa475 --- /dev/null +++ b/cmd/helm/pull_test.go @@ -0,0 +1,282 @@ +/* +Copyright The Helm Authors. + +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" + "os" + "path/filepath" + "testing" + + "helm.sh/helm/v3/pkg/repo/repotest" +) + +func TestPullCmd(t *testing.T) { + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() + + os.Setenv("HELM_EXPERIMENTAL_OCI", "1") + ociSrv, err := repotest.NewOCIServer(t, srv.Root()) + if err != nil { + t.Fatal(err) + } + ociSrv.Run(t) + + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + helmTestKeyOut := "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \n" + + "Using Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\n" + + "Chart Hash Verified: " + + // all flags will get "-d outdir" appended. + tests := []struct { + name string + args string + existFile string + existDir string + wantError bool + wantErrorMsg string + failExpect string + expectFile string + expectDir bool + expectVerify bool + expectSha string + }{ + { + name: "Basic chart fetch", + args: "test/signtest", + expectFile: "./signtest-0.1.0.tgz", + }, + { + name: "Chart fetch with version", + args: "test/signtest --version=0.1.0", + expectFile: "./signtest-0.1.0.tgz", + }, + { + name: "Fail chart fetch with non-existent version", + args: "test/signtest --version=99.1.0", + wantError: true, + failExpect: "no such chart", + }, + { + name: "Fail fetching non-existent chart", + args: "test/nosuchthing", + failExpect: "Failed to fetch", + wantError: true, + }, + { + name: "Fetch and verify", + args: "test/signtest --verify --keyring testdata/helm-test-key.pub", + expectFile: "./signtest-0.1.0.tgz", + expectVerify: true, + expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", + }, + { + name: "Fetch and fail verify", + args: "test/reqtest --verify --keyring testdata/helm-test-key.pub", + failExpect: "Failed to fetch provenance", + wantError: true, + }, + { + name: "Fetch and untar", + args: "test/signtest --untar --untardir signtest", + expectFile: "./signtest", + expectDir: true, + }, + { + name: "Fetch untar when file with same name existed", + args: "test/test1 --untar --untardir test1", + existFile: "test1", + wantError: true, + wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test1")), + }, + { + name: "Fetch untar when dir with same name existed", + args: "test/test2 --untar --untardir test2", + existDir: "test2", + wantError: true, + wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test2")), + }, + { + name: "Fetch, verify, untar", + args: "test/signtest --verify --keyring=testdata/helm-test-key.pub --untar --untardir signtest2", + expectFile: "./signtest2", + expectDir: true, + expectVerify: true, + expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", + }, + { + name: "Chart fetch using repo URL", + expectFile: "./signtest-0.1.0.tgz", + args: "signtest --repo " + srv.URL(), + }, + { + name: "Fail fetching non-existent chart on repo URL", + args: "someChart --repo " + srv.URL(), + failExpect: "Failed to fetch chart", + wantError: true, + }, + { + name: "Specific version chart fetch using repo URL", + expectFile: "./signtest-0.1.0.tgz", + args: "signtest --version=0.1.0 --repo " + srv.URL(), + }, + { + name: "Specific version chart fetch using repo URL", + args: "signtest --version=0.2.0 --repo " + srv.URL(), + failExpect: "Failed to fetch chart version", + wantError: true, + }, + { + name: "Fetch OCI Chart", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0", ociSrv.RegistryURL), + expectFile: "./oci-dependent-chart-0.1.0.tgz", + }, + { + name: "Fetch OCI Chart with untar", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar", ociSrv.RegistryURL), + expectFile: "./oci-dependent-chart", + expectDir: true, + }, + { + name: "Fetch OCI Chart with untar and untardir", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar --untardir ocitest2", ociSrv.RegistryURL), + expectFile: "./ocitest2", + expectDir: true, + }, + { + name: "OCI Fetch untar when dir with same name existed", + args: fmt.Sprintf("oci-test-chart oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar --untardir ocitest2 --untar --untardir ocitest2", ociSrv.RegistryURL), + wantError: true, + wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "ocitest2")), + }, + { + name: "Fail fetching non-existent OCI chart", + args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing --version 0.1.0", ociSrv.RegistryURL), + failExpect: "Failed to fetch", + wantError: true, + }, + { + name: "Fail fetching OCI chart without version specified", + args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing", ociSrv.RegistryURL), + wantErrorMsg: "Error: --version flag is explicitly required for OCI registries", + wantError: true, + }, + { + name: "Fail fetching OCI chart without version specified", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0", ociSrv.RegistryURL), + wantErrorMsg: "Error: --version flag is explicitly required for OCI registries", + wantError: true, + }, + { + name: "Fail fetching OCI chart without version specified", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0 --version 0.1.0", ociSrv.RegistryURL), + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outdir := srv.Root() + cmd := fmt.Sprintf("fetch %s -d '%s' --repository-config %s --repository-cache %s --registry-config %s", + tt.args, + outdir, + filepath.Join(outdir, "repositories.yaml"), + outdir, + filepath.Join(outdir, "config.json"), + ) + // Create file or Dir before helm pull --untar, see: https://github.com/helm/helm/issues/7182 + if tt.existFile != "" { + file := filepath.Join(outdir, tt.existFile) + _, err := os.Create(file) + if err != nil { + t.Fatal(err) + } + } + if tt.existDir != "" { + file := filepath.Join(outdir, tt.existDir) + err := os.Mkdir(file, 0755) + if err != nil { + t.Fatal(err) + } + } + _, out, err := executeActionCommand(cmd) + if err != nil { + if tt.wantError { + if tt.wantErrorMsg != "" && tt.wantErrorMsg == err.Error() { + t.Fatalf("Actual error %s, not equal to expected error %s", err, tt.wantErrorMsg) + } + return + } + t.Fatalf("%q reported error: %s", tt.name, err) + } + + if tt.expectVerify { + outString := helmTestKeyOut + tt.expectSha + "\n" + if out != outString { + t.Errorf("%q: expected verification output %q, got %q", tt.name, outString, out) + } + + } + + ef := filepath.Join(outdir, tt.expectFile) + fi, err := os.Stat(ef) + if err != nil { + t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err) + } + if fi.IsDir() != tt.expectDir { + t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir) + } + }) + } +} + +func TestPullVersionCompletion(t *testing.T) { + repoFile := "testdata/helmhome/helm/repositories.yaml" + repoCache := "testdata/helmhome/helm/repository" + + repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache) + + tests := []cmdTestCase{{ + name: "completion for pull version flag", + cmd: fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for pull version flag too few args", + cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for pull version flag too many args", + cmd: fmt.Sprintf("%s __complete pull testing/alpine badarg --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for pull version flag invalid chart", + cmd: fmt.Sprintf("%s __complete pull invalid/invalid --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }} + runTestCmd(t, tests) +} + +func TestPullFileCompletion(t *testing.T) { + checkFileCompletion(t, "pull", false) + checkFileCompletion(t, "pull repo/chart", false) +} diff --git a/cmd/helm/registry.go b/cmd/helm/registry.go new file mode 100644 index 00000000000..d13c308b2d0 --- /dev/null +++ b/cmd/helm/registry.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm Authors. +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" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/pkg/action" +) + +const registryHelp = ` +This command consists of multiple subcommands to interact with registries. +` + +func newRegistryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "registry", + Short: "login to or logout from a registry", + Long: registryHelp, + Hidden: !FeatureGateOCI.IsEnabled(), + PersistentPreRunE: checkOCIFeatureGate(), + } + cmd.AddCommand( + newRegistryLoginCmd(cfg, out), + newRegistryLogoutCmd(cfg, out), + ) + return cmd +} diff --git a/cmd/helm/registry_login.go b/cmd/helm/registry_login.go new file mode 100644 index 00000000000..43228f90a91 --- /dev/null +++ b/cmd/helm/registry_login.go @@ -0,0 +1,136 @@ +/* +Copyright The Helm Authors. + +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" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + "github.com/docker/docker/pkg/term" + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +const registryLoginDesc = ` +Authenticate to a remote registry. +` + +func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + var usernameOpt, passwordOpt string + var passwordFromStdinOpt, insecureOpt bool + + cmd := &cobra.Command{ + Use: "login [host]", + Short: "login to a registry", + Long: registryLoginDesc, + Args: require.MinimumNArgs(1), + Hidden: !FeatureGateOCI.IsEnabled(), + RunE: func(cmd *cobra.Command, args []string) error { + hostname := args[0] + + username, password, err := getUsernamePassword(usernameOpt, passwordOpt, passwordFromStdinOpt) + if err != nil { + return err + } + + return action.NewRegistryLogin(cfg).Run(out, hostname, username, password, insecureOpt) + }, + } + + f := cmd.Flags() + f.StringVarP(&usernameOpt, "username", "u", "", "registry username") + f.StringVarP(&passwordOpt, "password", "p", "", "registry password or identity token") + f.BoolVarP(&passwordFromStdinOpt, "password-stdin", "", false, "read password or identity token from stdin") + f.BoolVarP(&insecureOpt, "insecure", "", false, "allow connections to TLS registry without certs") + + return cmd +} + +// Adapted from https://github.com/deislabs/oras +func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) { + var err error + username := usernameOpt + password := passwordOpt + + if passwordFromStdinOpt { + passwordFromStdin, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return "", "", err + } + password = strings.TrimSuffix(string(passwordFromStdin), "\n") + password = strings.TrimSuffix(password, "\r") + } else if password == "" { + if username == "" { + username, err = readLine("Username: ", false) + if err != nil { + return "", "", err + } + username = strings.TrimSpace(username) + } + if username == "" { + password, err = readLine("Token: ", true) + if err != nil { + return "", "", err + } else if password == "" { + return "", "", errors.New("token required") + } + } else { + password, err = readLine("Password: ", true) + if err != nil { + return "", "", err + } else if password == "" { + return "", "", errors.New("password required") + } + } + } else { + warning("Using --password via the CLI is insecure. Use --password-stdin.") + } + + return username, password, nil +} + +// Copied/adapted from https://github.com/deislabs/oras +func readLine(prompt string, silent bool) (string, error) { + fmt.Print(prompt) + if silent { + fd := os.Stdin.Fd() + state, err := term.SaveState(fd) + if err != nil { + return "", err + } + term.DisableEcho(fd, state) + defer term.RestoreTerminal(fd, state) + } + + reader := bufio.NewReader(os.Stdin) + line, _, err := reader.ReadLine() + if err != nil { + return "", err + } + if silent { + fmt.Println() + } + + return string(line), nil +} diff --git a/cmd/helm/registry_logout.go b/cmd/helm/registry_logout.go new file mode 100644 index 00000000000..e7e1a24fea0 --- /dev/null +++ b/cmd/helm/registry_logout.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. + +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" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +const registryLogoutDesc = ` +Remove credentials stored for a remote registry. +` + +func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "logout [host]", + Short: "logout from a registry", + Long: registryLogoutDesc, + Args: require.MinimumNArgs(1), + Hidden: !FeatureGateOCI.IsEnabled(), + RunE: func(cmd *cobra.Command, args []string) error { + hostname := args[0] + return action.NewRegistryLogout(cfg).Run(out, hostname) + }, + } +} diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go index bdfa87a60f2..2637cbb9f20 100644 --- a/cmd/helm/release_testing.go +++ b/cmd/helm/release_testing.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,92 +19,79 @@ package main import ( "fmt" "io" + "regexp" + "strings" + "time" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" ) -const releaseTestDesc = ` +const releaseTestHelp = ` The test command runs the tests for a release. The argument this command takes is the name of a deployed release. The tests to be run are defined in the chart that was installed. ` -type releaseTestCmd struct { - name string - out io.Writer - client helm.Interface - timeout int64 - cleanup bool -} - -func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command { - rlsTest := &releaseTestCmd{ - out: out, - client: c, - } +func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewReleaseTesting(cfg) + var outfmt = output.Table + var outputLogs bool + var filter []string cmd := &cobra.Command{ - Use: "test [RELEASE]", - Short: "test a release", - Long: releaseTestDesc, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, - RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(len(args), "release name"); err != nil { - return err + Use: "test [RELEASE]", + Short: "run tests for a release", + Long: releaseTestHelp, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp } - - rlsTest.name = args[0] - rlsTest.client = ensureHelmClient(rlsTest.client) - return rlsTest.run() + return compListReleases(toComplete, args, cfg) }, - } - - f := cmd.Flags() - f.Int64Var(&rlsTest.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") - f.BoolVar(&rlsTest.cleanup, "cleanup", false, "delete test pods upon completion") - - return cmd -} - -func (t *releaseTestCmd) run() (err error) { - c, errc := t.client.RunReleaseTest( - t.name, - helm.ReleaseTestTimeout(t.timeout), - helm.ReleaseTestCleanup(t.cleanup), - ) - testErr := &testErr{} - - for { - select { - case err := <-errc: - if prettyError(err) == nil && testErr.failed > 0 { - return testErr.Error() + RunE: func(cmd *cobra.Command, args []string) error { + client.Namespace = settings.Namespace() + notName := regexp.MustCompile(`^!\s?name=`) + for _, f := range filter { + if strings.HasPrefix(f, "name=") { + client.Filters["name"] = append(client.Filters["name"], strings.TrimPrefix(f, "name=")) + } else if notName.MatchString(f) { + client.Filters["!name"] = append(client.Filters["!name"], notName.ReplaceAllLiteralString(f, "")) + } } - return prettyError(err) - case res, ok := <-c: - if !ok { - break + rel, runErr := client.Run(args[0]) + // We only return an error if we weren't even able to get the + // release, otherwise we keep going so we can print status and logs + // if requested + if runErr != nil && rel == nil { + return runErr } - if res.Status == release.TestRun_FAILURE { - testErr.failed++ + if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}); err != nil { + return err } - fmt.Fprintf(t.out, res.Msg+"\n") + if outputLogs { + // Print a newline to stdout to separate the output + fmt.Fprintln(out) + if err := client.GetPodLogs(out, rel); err != nil { + return err + } + } - } + return runErr + }, } -} - -type testErr struct { - failed int -} + f := cmd.Flags() + f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)") + f.StringSliceVar(&filter, "filter", []string{}, "specify tests by attribute (currently \"name\") using attribute=value syntax or '!attribute=value' to exclude a test (can specify multiple or separate values with commas: name=test1,name=test2)") -func (err *testErr) Error() error { - return fmt.Errorf("%v test(s) failed", err.failed) + return cmd } diff --git a/cmd/helm/release_testing_test.go b/cmd/helm/release_testing_test.go index b946746d016..680a9bd3e0e 100644 --- a/cmd/helm/release_testing_test.go +++ b/cmd/helm/release_testing_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,67 +17,14 @@ limitations under the License. package main import ( - "io" "testing" - - "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" ) -func TestReleaseTesting(t *testing.T) { - tests := []releaseCase{ - { - name: "basic test", - args: []string{"example-release"}, - flags: []string{}, - responses: map[string]release.TestRun_Status{"PASSED: green lights everywhere": release.TestRun_SUCCESS}, - err: false, - }, - { - name: "test failure", - args: []string{"example-fail"}, - flags: []string{}, - responses: map[string]release.TestRun_Status{"FAILURE: red lights everywhere": release.TestRun_FAILURE}, - err: true, - }, - { - name: "test unknown", - args: []string{"example-unknown"}, - flags: []string{}, - responses: map[string]release.TestRun_Status{"UNKNOWN: yellow lights everywhere": release.TestRun_UNKNOWN}, - err: false, - }, - { - name: "test error", - args: []string{"example-error"}, - flags: []string{}, - responses: map[string]release.TestRun_Status{"ERROR: yellow lights everywhere": release.TestRun_FAILURE}, - err: true, - }, - { - name: "test running", - args: []string{"example-running"}, - flags: []string{}, - responses: map[string]release.TestRun_Status{"RUNNING: things are happpeningggg": release.TestRun_RUNNING}, - err: false, - }, - { - name: "multiple tests example", - args: []string{"example-suite"}, - flags: []string{}, - responses: map[string]release.TestRun_Status{ - "RUNNING: things are happpeningggg": release.TestRun_RUNNING, - "PASSED: party time": release.TestRun_SUCCESS, - "RUNNING: things are happening again": release.TestRun_RUNNING, - "FAILURE: good thing u checked :)": release.TestRun_FAILURE, - "RUNNING: things are happpeningggg yet again": release.TestRun_RUNNING, - "PASSED: feel free to party again": release.TestRun_SUCCESS}, - err: true, - }, - } +func TestReleaseTestingCompletion(t *testing.T) { + checkReleaseCompletion(t, "test", false) +} - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newReleaseTestCmd(c, out) - }) +func TestReleaseTestingFileCompletion(t *testing.T) { + checkFileCompletion(t, "test", false) + checkFileCompletion(t, "test myrelease", false) } diff --git a/cmd/helm/repo.go b/cmd/helm/repo.go index 8acc762e278..ad6ceaa8fbd 100644 --- a/cmd/helm/repo.go +++ b/cmd/helm/repo.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,23 +18,26 @@ package main import ( "io" + "os" + "github.com/pkg/errors" "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" ) var repoHelm = ` This command consists of multiple subcommands to interact with chart repositories. It can be used to add, remove, list, and index chart repositories. -Example usage: - $ helm repo add [NAME] [REPO_URL] ` func newRepoCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "repo [FLAGS] add|remove|list|index|update [ARGS]", + Use: "repo add|remove|list|index|update [ARGS]", Short: "add, list, remove, update, and index chart repositories", Long: repoHelm, + Args: require.NoArgs, } cmd.AddCommand(newRepoAddCmd(out)) @@ -45,3 +48,7 @@ func newRepoCmd(out io.Writer) *cobra.Command { return cmd } + +func isNotExist(err error) bool { + return os.IsNotExist(errors.Cause(err)) +} diff --git a/cmd/helm/repo_add.go b/cmd/helm/repo_add.go index 77a64cc89a4..52cd020f5c5 100644 --- a/cmd/helm/repo_add.go +++ b/cmd/helm/repo_add.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,89 +17,159 @@ limitations under the License. package main import ( + "context" "fmt" "io" - + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" + + "github.com/gofrs/flock" + "github.com/pkg/errors" "github.com/spf13/cobra" + "golang.org/x/term" + "sigs.k8s.io/yaml" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/repo" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" ) -type repoAddCmd struct { - name string - url string - username string - password string - home helmpath.Home - noupdate bool +// Repositories that have been permanently deleted and no longer work +var deprecatedRepos = map[string]string{ + "//kubernetes-charts.storage.googleapis.com": "https://charts.helm.sh/stable", + "//kubernetes-charts-incubator.storage.googleapis.com": "https://charts.helm.sh/incubator", +} + +type repoAddOptions struct { + name string + url string + username string + password string + forceUpdate bool + allowDeprecatedRepos bool - certFile string - keyFile string - caFile string + certFile string + keyFile string + caFile string + insecureSkipTLSverify bool - out io.Writer + repoFile string + repoCache string + + // Deprecated, but cannot be removed until Helm 4 + deprecatedNoUpdate bool } func newRepoAddCmd(out io.Writer) *cobra.Command { - add := &repoAddCmd{out: out} + o := &repoAddOptions{} cmd := &cobra.Command{ - Use: "add [flags] [NAME] [URL]", - Short: "add a chart repository", + Use: "add [NAME] [URL]", + Short: "add a chart repository", + Args: require.ExactArgs(2), + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(len(args), "name for the chart repository", "the url of the chart repository"); err != nil { - return err - } - - add.name = args[0] - add.url = args[1] - add.home = settings.Home + o.name = args[0] + o.url = args[1] + o.repoFile = settings.RepositoryConfig + o.repoCache = settings.RepositoryCache - return add.run() + return o.run(out) }, } f := cmd.Flags() - f.StringVar(&add.username, "username", "", "chart repository username") - f.StringVar(&add.password, "password", "", "chart repository password") - f.BoolVar(&add.noupdate, "no-update", false, "raise error if repo is already registered") - f.StringVar(&add.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") - f.StringVar(&add.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") - f.StringVar(&add.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + f.StringVar(&o.username, "username", "", "chart repository username") + f.StringVar(&o.password, "password", "", "chart repository password") + f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists") + f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.") + f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository") + f.BoolVar(&o.allowDeprecatedRepos, "allow-deprecated-repos", false, "by default, this command will not allow adding official repos that have been permanently deleted. This disables that behavior") return cmd } -func (a *repoAddCmd) run() error { - if err := addRepository(a.name, a.url, a.username, a.password, a.home, a.certFile, a.keyFile, a.caFile, a.noupdate); err != nil { +func (o *repoAddOptions) run(out io.Writer) error { + // Block deprecated repos + if !o.allowDeprecatedRepos { + for oldURL, newURL := range deprecatedRepos { + if strings.Contains(o.url, oldURL) { + return fmt.Errorf("repo %q is no longer available; try %q instead", o.url, newURL) + } + } + } + + // Ensure the file directory exists as it is required for file locking + err := os.MkdirAll(filepath.Dir(o.repoFile), os.ModePerm) + if err != nil && !os.IsExist(err) { return err } - fmt.Fprintf(a.out, "%q has been added to your repositories\n", a.name) - return nil -} -func addRepository(name, url, username, password string, home helmpath.Home, certFile, keyFile, caFile string, noUpdate bool) error { - f, err := repo.LoadRepositoriesFile(home.RepositoryFile()) + // Acquire a file lock for process synchronization + fileLock := flock.New(strings.Replace(o.repoFile, filepath.Ext(o.repoFile), ".lock", 1)) + lockCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + locked, err := fileLock.TryLockContext(lockCtx, time.Second) + if err == nil && locked { + defer fileLock.Unlock() + } if err != nil { return err } - if noUpdate && f.Has(name) { - return fmt.Errorf("repository name (%s) already exists, please specify a different name", name) + b, err := ioutil.ReadFile(o.repoFile) + if err != nil && !os.IsNotExist(err) { + return err + } + + var f repo.File + if err := yaml.Unmarshal(b, &f); err != nil { + return err + } + + if o.username != "" && o.password == "" { + fd := int(os.Stdin.Fd()) + fmt.Fprint(out, "Password: ") + password, err := term.ReadPassword(fd) + fmt.Fprintln(out) + if err != nil { + return err + } + o.password = string(password) } - cif := home.CacheIndex(name) c := repo.Entry{ - Name: name, - Cache: cif, - URL: url, - Username: username, - Password: password, - CertFile: certFile, - KeyFile: keyFile, - CAFile: caFile, + Name: o.name, + URL: o.url, + Username: o.username, + Password: o.password, + CertFile: o.certFile, + KeyFile: o.keyFile, + CAFile: o.caFile, + InsecureSkipTLSverify: o.insecureSkipTLSverify, + } + + // If the repo exists do one of two things: + // 1. If the configuration for the name is the same continue without error + // 2. When the config is different require --force-update + if !o.forceUpdate && f.Has(o.name) { + existing := f.Get(o.name) + if c != *existing { + + // The input coming in for the name is different from what is already + // configured. Return an error. + return errors.Errorf("repository name (%s) already exists, please specify a different name", o.name) + } + + // The add is idempotent so do nothing + fmt.Fprintf(out, "%q already exists with the same configuration, skipping\n", o.name) + return nil } r, err := repo.NewChartRepository(&c, getter.All(settings)) @@ -107,11 +177,18 @@ func addRepository(name, url, username, password string, home helmpath.Home, cer return err } - if err := r.DownloadIndexFile(home.Cache()); err != nil { - return fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", url, err.Error()) + if o.repoCache != "" { + r.CachePath = o.repoCache + } + if _, err := r.DownloadIndexFile(); err != nil { + return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", o.url) } f.Update(&c) - return f.WriteFile(home.RepositoryFile(), 0644) + if err := f.WriteFile(o.repoFile, 0644); err != nil { + return err + } + fmt.Fprintf(out, "%q has been added to your repositories\n", o.name) + return nil } diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go index 157b1cc5bc0..f3bc5498553 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,87 +17,178 @@ limitations under the License. package main import ( - "io" + "fmt" + "io/ioutil" "os" + "path/filepath" + "sync" "testing" - "github.com/spf13/cobra" + "sigs.k8s.io/yaml" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/repo" - "k8s.io/helm/pkg/repo/repotest" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/helmpath/xdg" + "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v3/pkg/repo/repotest" ) -var testName = "test-name" - func TestRepoAddCmd(t *testing.T) { - srv, thome, err := repotest.NewTempServer("testdata/testserver/*.*") + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") if err != nil { t.Fatal(err) } + defer srv.Stop() - cleanup := resetEnv() - defer func() { - srv.Stop() - os.RemoveAll(thome.String()) - cleanup() - }() - if err := ensureTestHome(thome, t); err != nil { + // A second test server is setup to verify URL changing + srv2, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") + if err != nil { t.Fatal(err) } + defer srv2.Stop() - settings.Home = thome + tmpdir := ensure.TempDir(t) + repoFile := filepath.Join(tmpdir, "repositories.yaml") - tests := []releaseCase{ + tests := []cmdTestCase{ + { + name: "add a repository", + cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv.URL(), repoFile, tmpdir), + golden: "output/repo-add.txt", + }, + { + name: "add repository second time", + cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv.URL(), repoFile, tmpdir), + golden: "output/repo-add2.txt", + }, + { + name: "add repository different url", + cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv2.URL(), repoFile, tmpdir), + wantError: true, + }, { - name: "add a repository", - args: []string{testName, srv.URL()}, - expected: "\"" + testName + "\" has been added to your repositories", + name: "add repository second time", + cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s --force-update", srv2.URL(), repoFile, tmpdir), + golden: "output/repo-add.txt", }, } - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newRepoAddCmd(out) - }) + runTestCmd(t, tests) } func TestRepoAdd(t *testing.T) { - ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*") + ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") if err != nil { t.Fatal(err) } + defer ts.Stop() - cleanup := resetEnv() - hh := thome - defer func() { - ts.Stop() - os.RemoveAll(thome.String()) - cleanup() - }() - if err := ensureTestHome(hh, t); err != nil { - t.Fatal(err) - } + rootDir := ensure.TempDir(t) + repoFile := filepath.Join(rootDir, "repositories.yaml") - settings.Home = thome + const testRepoName = "test-name" - if err := addRepository(testName, ts.URL(), "", "", hh, "", "", "", true); err != nil { + o := &repoAddOptions{ + name: testRepoName, + url: ts.URL(), + forceUpdate: false, + deprecatedNoUpdate: true, + repoFile: repoFile, + } + os.Setenv(xdg.CacheHomeEnvVar, rootDir) + + if err := o.run(ioutil.Discard); err != nil { t.Error(err) } - f, err := repo.LoadRepositoriesFile(hh.RepositoryFile()) + f, err := repo.LoadFile(repoFile) if err != nil { - t.Error(err) + t.Fatal(err) + } + + if !f.Has(testRepoName) { + t.Errorf("%s was not successfully inserted into %s", testRepoName, repoFile) } - if !f.Has(testName) { - t.Errorf("%s was not successfully inserted into %s", testName, hh.RepositoryFile()) + idx := filepath.Join(helmpath.CachePath("repository"), helmpath.CacheIndexFile(testRepoName)) + if _, err := os.Stat(idx); os.IsNotExist(err) { + t.Errorf("Error cache index file was not created for repository %s", testRepoName) + } + idx = filepath.Join(helmpath.CachePath("repository"), helmpath.CacheChartsFile(testRepoName)) + if _, err := os.Stat(idx); os.IsNotExist(err) { + t.Errorf("Error cache charts file was not created for repository %s", testRepoName) } - if err := addRepository(testName, ts.URL(), "", "", hh, "", "", "", false); err != nil { + o.forceUpdate = true + + if err := o.run(ioutil.Discard); err != nil { t.Errorf("Repository was not updated: %s", err) } - if err := addRepository(testName, ts.URL(), "", "", hh, "", "", "", false); err != nil { + if err := o.run(ioutil.Discard); err != nil { t.Errorf("Duplicate repository name was added") } } + +func TestRepoAddConcurrentGoRoutines(t *testing.T) { + const testName = "test-name" + repoFile := filepath.Join(ensure.TempDir(t), "repositories.yaml") + repoAddConcurrent(t, testName, repoFile) +} + +func TestRepoAddConcurrentDirNotExist(t *testing.T) { + const testName = "test-name-2" + repoFile := filepath.Join(ensure.TempDir(t), "foo", "repositories.yaml") + repoAddConcurrent(t, testName, repoFile) +} + +func repoAddConcurrent(t *testing.T, testName, repoFile string) { + ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + defer ts.Stop() + + var wg sync.WaitGroup + wg.Add(3) + for i := 0; i < 3; i++ { + go func(name string) { + defer wg.Done() + o := &repoAddOptions{ + name: name, + url: ts.URL(), + deprecatedNoUpdate: true, + forceUpdate: false, + repoFile: repoFile, + } + if err := o.run(ioutil.Discard); err != nil { + t.Error(err) + } + }(fmt.Sprintf("%s-%d", testName, i)) + } + wg.Wait() + + b, err := ioutil.ReadFile(repoFile) + if err != nil { + t.Error(err) + } + + var f repo.File + if err := yaml.Unmarshal(b, &f); err != nil { + t.Error(err) + } + + var name string + for i := 0; i < 3; i++ { + name = fmt.Sprintf("%s-%d", testName, i) + if !f.Has(name) { + t.Errorf("%s was not successfully inserted into %s: %s", name, repoFile, f.Repositories[0]) + } + } +} + +func TestRepoAddFileCompletion(t *testing.T) { + checkFileCompletion(t, "repo add", false) + checkFileCompletion(t, "repo add reponame", false) + checkFileCompletion(t, "repo add reponame https://example.com", false) +} diff --git a/cmd/helm/repo_index.go b/cmd/helm/repo_index.go index 540057eb843..917acd442c1 100644 --- a/cmd/helm/repo_index.go +++ b/cmd/helm/repo_index.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,14 +17,15 @@ limitations under the License. package main import ( - "fmt" "io" "os" "path/filepath" + "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/repo" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/repo" ) const repoIndexDesc = ` @@ -38,39 +39,42 @@ flag. In this case, the charts found in the current directory will be merged into the existing index, with local charts taking priority over existing charts. ` -type repoIndexCmd struct { +type repoIndexOptions struct { dir string url string - out io.Writer merge string } func newRepoIndexCmd(out io.Writer) *cobra.Command { - index := &repoIndexCmd{out: out} + o := &repoIndexOptions{} cmd := &cobra.Command{ - Use: "index [flags] [DIR]", + Use: "index [DIR]", Short: "generate an index file given a directory containing packaged charts", Long: repoIndexDesc, - RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(len(args), "path to a directory"); err != nil { - return err + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + // Allow file completion when completing the argument for the directory + return nil, cobra.ShellCompDirectiveDefault } - - index.dir = args[0] - - return index.run() + // No more completions, so disable file completion + return nil, cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(cmd *cobra.Command, args []string) error { + o.dir = args[0] + return o.run(out) }, } f := cmd.Flags() - f.StringVar(&index.url, "url", "", "url of chart repository") - f.StringVar(&index.merge, "merge", "", "merge the generated index into the given index") + f.StringVar(&o.url, "url", "", "url of chart repository") + f.StringVar(&o.merge, "merge", "", "merge the generated index into the given index") return cmd } -func (i *repoIndexCmd) run() error { +func (i *repoIndexOptions) run(out io.Writer) error { path, err := filepath.Abs(i.dir) if err != nil { return err @@ -91,15 +95,15 @@ func index(dir, url, mergeTo string) error { var i2 *repo.IndexFile if _, err := os.Stat(mergeTo); os.IsNotExist(err) { i2 = repo.NewIndexFile() - i2.WriteFile(mergeTo, 0755) + i2.WriteFile(mergeTo, 0644) } else { i2, err = repo.LoadIndexFile(mergeTo) if err != nil { - return fmt.Errorf("Merge failed: %s", err) + return errors.Wrap(err, "merge failed") } } i.Merge(i2) } i.SortEntries() - return i.WriteFile(out, 0755) + return i.WriteFile(out, 0644) } diff --git a/cmd/helm/repo_index_test.go b/cmd/helm/repo_index_test.go index 4d6313f6c35..ae3390154bb 100644 --- a/cmd/helm/repo_index_test.go +++ b/cmd/helm/repo_index_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,21 +19,17 @@ package main import ( "bytes" "io" - "io/ioutil" "os" "path/filepath" "testing" - "k8s.io/helm/pkg/repo" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/repo" ) func TestRepoIndexCmd(t *testing.T) { - dir, err := ioutil.TempDir("", "helm-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := ensure.TempDir(t) comp := filepath.Join(dir, "compressedchart-0.1.0.tgz") if err := linkOrCopy("testdata/testcharts/compressedchart-0.1.0.tgz", comp); err != nil { @@ -123,7 +119,7 @@ func TestRepoIndexCmd(t *testing.T) { t.Error(err) } - _, err = repo.LoadIndexFile(destIndex) + index, err = repo.LoadIndexFile(destIndex) if err != nil { t.Fatal(err) } @@ -134,8 +130,8 @@ func TestRepoIndexCmd(t *testing.T) { } vs = index.Entries["compressedchart"] - if len(vs) != 3 { - t.Errorf("expected 3 versions, got %d: %#v", len(vs), vs) + if len(vs) != 1 { + t.Errorf("expected 1 versions, got %d: %#v", len(vs), vs) } expectedVersion = "0.3.0" @@ -169,3 +165,8 @@ func copyFile(dst, src string) error { return err } + +func TestRepoIndexFileCompletion(t *testing.T) { + checkFileCompletion(t, "repo index", true) + checkFileCompletion(t, "repo index mydir", false) +} diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go index 0f795b2b0fb..fc53ba75a12 100644 --- a/cmd/helm/repo_list.go +++ b/cmd/helm/repo_list.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,50 +17,123 @@ limitations under the License. package main import ( - "errors" - "fmt" "io" + "strings" "github.com/gosuri/uitable" + "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/repo" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/cli/output" + "helm.sh/helm/v3/pkg/repo" ) -type repoListCmd struct { - out io.Writer - home helmpath.Home -} - func newRepoListCmd(out io.Writer) *cobra.Command { - list := &repoListCmd{out: out} - + var outfmt output.Format cmd := &cobra.Command{ - Use: "list [flags]", - Short: "list chart repositories", + Use: "list", + Aliases: []string{"ls"}, + Short: "list chart repositories", + Args: require.NoArgs, + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { - list.home = settings.Home - return list.run() + f, err := repo.LoadFile(settings.RepositoryConfig) + if isNotExist(err) || (len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML)) { + return errors.New("no repositories to show") + } + + return outfmt.Write(out, &repoListWriter{f.Repositories}) }, } + bindOutputFlag(cmd, &outfmt) + return cmd } -func (a *repoListCmd) run() error { - f, err := repo.LoadRepositoriesFile(a.home.RepositoryFile()) - if err != nil { - return err - } - if len(f.Repositories) == 0 { - return errors.New("no repositories to show") - } +type repositoryElement struct { + Name string `json:"name"` + URL string `json:"url"` +} + +type repoListWriter struct { + repos []*repo.Entry +} + +func (r *repoListWriter) WriteTable(out io.Writer) error { table := uitable.New() table.AddRow("NAME", "URL") - for _, re := range f.Repositories { + for _, re := range r.repos { table.AddRow(re.Name, re.URL) } - fmt.Fprintln(a.out, table) + return output.EncodeTable(out, table) +} + +func (r *repoListWriter) WriteJSON(out io.Writer) error { + return r.encodeByFormat(out, output.JSON) +} + +func (r *repoListWriter) WriteYAML(out io.Writer) error { + return r.encodeByFormat(out, output.YAML) +} + +func (r *repoListWriter) encodeByFormat(out io.Writer, format output.Format) error { + // Initialize the array so no results returns an empty array instead of null + repolist := make([]repositoryElement, 0, len(r.repos)) + + for _, re := range r.repos { + repolist = append(repolist, repositoryElement{Name: re.Name, URL: re.URL}) + } + + switch format { + case output.JSON: + return output.EncodeJSON(out, repolist) + case output.YAML: + return output.EncodeYAML(out, repolist) + } + + // Because this is a non-exported function and only called internally by + // WriteJSON and WriteYAML, we shouldn't get invalid types return nil } + +// Returns all repos from repos, except those with names matching ignoredRepoNames +// Inspired by https://stackoverflow.com/a/28701031/893211 +func filterRepos(repos []*repo.Entry, ignoredRepoNames []string) []*repo.Entry { + // if ignoredRepoNames is nil, just return repo + if ignoredRepoNames == nil { + return repos + } + + filteredRepos := make([]*repo.Entry, 0) + + ignored := make(map[string]bool, len(ignoredRepoNames)) + for _, repoName := range ignoredRepoNames { + ignored[repoName] = true + } + + for _, repo := range repos { + if _, removed := ignored[repo.Name]; !removed { + filteredRepos = append(filteredRepos, repo) + } + } + + return filteredRepos +} + +// Provide dynamic auto-completion for repo names +func compListRepos(prefix string, ignoredRepoNames []string) []string { + var rNames []string + + f, err := repo.LoadFile(settings.RepositoryConfig) + if err == nil && len(f.Repositories) > 0 { + filteredRepos := filterRepos(f.Repositories, ignoredRepoNames) + for _, repo := range filteredRepos { + if strings.HasPrefix(repo.Name, prefix) { + rNames = append(rNames, repo.Name) + } + } + } + return rNames +} diff --git a/cmd/helm/repo_list_test.go b/cmd/helm/repo_list_test.go new file mode 100644 index 00000000000..90149ebdaa6 --- /dev/null +++ b/cmd/helm/repo_list_test.go @@ -0,0 +1,29 @@ +/* +Copyright The Helm Authors. + +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" +) + +func TestRepoListOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "repo list") +} + +func TestRepoListFileCompletion(t *testing.T) { + checkFileCompletion(t, "repo list", false) +} diff --git a/cmd/helm/repo_remove.go b/cmd/helm/repo_remove.go index 201ee9ca80e..e6e9cb681cd 100644 --- a/cmd/helm/repo_remove.go +++ b/cmd/helm/repo_remove.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,73 +20,77 @@ import ( "fmt" "io" "os" + "path/filepath" + "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/repo" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/repo" ) -type repoRemoveCmd struct { - out io.Writer - name string - home helmpath.Home +type repoRemoveOptions struct { + names []string + repoFile string + repoCache string } func newRepoRemoveCmd(out io.Writer) *cobra.Command { - remove := &repoRemoveCmd{out: out} + o := &repoRemoveOptions{} cmd := &cobra.Command{ - Use: "remove [flags] [NAME]", + Use: "remove [REPO1 [REPO2 ...]]", Aliases: []string{"rm"}, - Short: "remove a chart repository", + Short: "remove one or more chart repositories", + Args: require.MinimumNArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp + }, RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(len(args), "name of chart repository"); err != nil { - return err - } - remove.name = args[0] - remove.home = settings.Home - - return remove.run() + o.repoFile = settings.RepositoryConfig + o.repoCache = settings.RepositoryCache + o.names = args + return o.run(out) }, } - return cmd } -func (r *repoRemoveCmd) run() error { - return removeRepoLine(r.out, r.name, r.home) -} - -func removeRepoLine(out io.Writer, name string, home helmpath.Home) error { - repoFile := home.RepositoryFile() - r, err := repo.LoadRepositoriesFile(repoFile) - if err != nil { - return err +func (o *repoRemoveOptions) run(out io.Writer) error { + r, err := repo.LoadFile(o.repoFile) + if isNotExist(err) || len(r.Repositories) == 0 { + return errors.New("no repositories configured") } - if !r.Remove(name) { - return fmt.Errorf("no repo named %q found", name) - } - if err := r.WriteFile(repoFile, 0644); err != nil { - return err - } + for _, name := range o.names { + if !r.Remove(name) { + return errors.Errorf("no repo named %q found", name) + } + if err := r.WriteFile(o.repoFile, 0644); err != nil { + return err + } - if err := removeRepoCache(name, home); err != nil { - return err + if err := removeRepoCache(o.repoCache, name); err != nil { + return err + } + fmt.Fprintf(out, "%q has been removed from your repositories\n", name) } - fmt.Fprintf(out, "%q has been removed from your repositories\n", name) - return nil } -func removeRepoCache(name string, home helmpath.Home) error { - if _, err := os.Stat(home.CacheIndex(name)); err == nil { - err = os.Remove(home.CacheIndex(name)) - if err != nil { - return err - } +func removeRepoCache(root, name string) error { + idx := filepath.Join(root, helmpath.CacheChartsFile(name)) + if _, err := os.Stat(idx); err == nil { + os.Remove(idx) } - return nil + + idx = filepath.Join(root, helmpath.CacheIndexFile(name)) + if _, err := os.Stat(idx); os.IsNotExist(err) { + return nil + } else if err != nil { + return errors.Wrapf(err, "can't remove index file %s", idx) + } + return os.Remove(idx) } diff --git a/cmd/helm/repo_remove_test.go b/cmd/helm/repo_remove_test.go index 174a4449557..cb5c6e9ab0d 100644 --- a/cmd/helm/repo_remove_test.go +++ b/cmd/helm/repo_remove_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,63 +19,149 @@ package main import ( "bytes" "os" + "path/filepath" "strings" "testing" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/repo" - "k8s.io/helm/pkg/repo/repotest" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v3/pkg/repo/repotest" ) func TestRepoRemove(t *testing.T) { - ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*") + ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") if err != nil { t.Fatal(err) } + defer ts.Stop() - hh := helmpath.Home(thome) - cleanup := resetEnv() - defer func() { - ts.Stop() - os.RemoveAll(thome.String()) - cleanup() - }() - if err := ensureTestHome(hh, t); err != nil { - t.Fatal(err) - } + rootDir := ensure.TempDir(t) + repoFile := filepath.Join(rootDir, "repositories.yaml") - settings.Home = thome + const testRepoName = "test-name" b := bytes.NewBuffer(nil) - if err := removeRepoLine(b, testName, hh); err == nil { - t.Errorf("Expected error removing %s, but did not get one.", testName) + rmOpts := repoRemoveOptions{ + names: []string{testRepoName}, + repoFile: repoFile, + repoCache: rootDir, + } + + if err := rmOpts.run(os.Stderr); err == nil { + t.Errorf("Expected error removing %s, but did not get one.", testRepoName) + } + o := &repoAddOptions{ + name: testRepoName, + url: ts.URL(), + repoFile: repoFile, } - if err := addRepository(testName, ts.URL(), "", "", hh, "", "", "", true); err != nil { + + if err := o.run(os.Stderr); err != nil { t.Error(err) } - mf, _ := os.Create(hh.CacheIndex(testName)) - mf.Close() + cacheIndexFile, cacheChartsFile := createCacheFiles(rootDir, testRepoName) + // Reset the buffer before running repo remove b.Reset() - if err := removeRepoLine(b, testName, hh); err != nil { - t.Errorf("Error removing %s from repositories", testName) + + if err := rmOpts.run(b); err != nil { + t.Errorf("Error removing %s from repositories", testRepoName) } if !strings.Contains(b.String(), "has been removed") { t.Errorf("Unexpected output: %s", b.String()) } - if _, err := os.Stat(hh.CacheIndex(testName)); err == nil { - t.Errorf("Error cache file was not removed for repository %s", testName) - } + testCacheFiles(t, cacheIndexFile, cacheChartsFile, testRepoName) - f, err := repo.LoadRepositoriesFile(hh.RepositoryFile()) + f, err := repo.LoadFile(repoFile) if err != nil { t.Error(err) } - if f.Has(testName) { - t.Errorf("%s was not successfully removed from repositories list", testName) + if f.Has(testRepoName) { + t.Errorf("%s was not successfully removed from repositories list", testRepoName) + } + + // Test removal of multiple repos in one go + var testRepoNames = []string{"foo", "bar", "baz"} + cacheFiles := make(map[string][]string, len(testRepoNames)) + + // Add test repos + for _, repoName := range testRepoNames { + o := &repoAddOptions{ + name: repoName, + url: ts.URL(), + repoFile: repoFile, + } + + if err := o.run(os.Stderr); err != nil { + t.Error(err) + } + + cacheIndex, cacheChart := createCacheFiles(rootDir, repoName) + cacheFiles[repoName] = []string{cacheIndex, cacheChart} + + } + + // Create repo remove command + multiRmOpts := repoRemoveOptions{ + names: testRepoNames, + repoFile: repoFile, + repoCache: rootDir, } + + // Reset the buffer before running repo remove + b.Reset() + + // Run repo remove command + if err := multiRmOpts.run(b); err != nil { + t.Errorf("Error removing list of repos from repositories: %q", testRepoNames) + } + + // Check that stuff were removed + if !strings.Contains(b.String(), "has been removed") { + t.Errorf("Unexpected output: %s", b.String()) + } + + for _, repoName := range testRepoNames { + f, err := repo.LoadFile(repoFile) + if err != nil { + t.Error(err) + } + if f.Has(repoName) { + t.Errorf("%s was not successfully removed from repositories list", repoName) + } + cacheIndex := cacheFiles[repoName][0] + cacheChart := cacheFiles[repoName][1] + testCacheFiles(t, cacheIndex, cacheChart, repoName) + } +} + +func createCacheFiles(rootDir string, repoName string) (cacheIndexFile string, cacheChartsFile string) { + cacheIndexFile = filepath.Join(rootDir, helmpath.CacheIndexFile(repoName)) + mf, _ := os.Create(cacheIndexFile) + mf.Close() + + cacheChartsFile = filepath.Join(rootDir, helmpath.CacheChartsFile(repoName)) + mf, _ = os.Create(cacheChartsFile) + mf.Close() + + return cacheIndexFile, cacheChartsFile +} + +func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string, repoName string) { + if _, err := os.Stat(cacheIndexFile); err == nil { + t.Errorf("Error cache index file was not removed for repository %s", repoName) + } + if _, err := os.Stat(cacheChartsFile); err == nil { + t.Errorf("Error cache chart file was not removed for repository %s", repoName) + } +} + +func TestRepoRemoveFileCompletion(t *testing.T) { + checkFileCompletion(t, "repo remove", false) + checkFileCompletion(t, "repo remove repo1", false) } diff --git a/cmd/helm/repo_test.go b/cmd/helm/repo_test.go new file mode 100644 index 00000000000..2b0df7c4c93 --- /dev/null +++ b/cmd/helm/repo_test.go @@ -0,0 +1,25 @@ +/* +Copyright The Helm Authors. + +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" +) + +func TestRepoFileCompletion(t *testing.T) { + checkFileCompletion(t, "repo", false) +} diff --git a/cmd/helm/repo_update.go b/cmd/helm/repo_update.go index 51e5c08685a..23dca194a30 100644 --- a/cmd/helm/repo_update.go +++ b/cmd/helm/repo_update.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,87 +17,85 @@ limitations under the License. package main import ( - "errors" "fmt" "io" "sync" + "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/repo" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" ) const updateDesc = ` Update gets the latest information about charts from the respective chart repositories. Information is cached locally, where it is used by commands like 'helm search'. - -'helm update' is the deprecated form of 'helm repo update'. It will be removed in -future releases. ` var errNoRepositories = errors.New("no repositories found. You must add one before updating") -type repoUpdateCmd struct { - update func([]*repo.ChartRepository, io.Writer, helmpath.Home) - home helmpath.Home - out io.Writer +type repoUpdateOptions struct { + update func([]*repo.ChartRepository, io.Writer) + repoFile string + repoCache string } func newRepoUpdateCmd(out io.Writer) *cobra.Command { - u := &repoUpdateCmd{ - out: out, - update: updateCharts, - } + o := &repoUpdateOptions{update: updateCharts} + cmd := &cobra.Command{ - Use: "update", - Aliases: []string{"up"}, - Short: "update information of available charts locally from chart repositories", - Long: updateDesc, + Use: "update", + Aliases: []string{"up"}, + Short: "update information of available charts locally from chart repositories", + Long: updateDesc, + Args: require.NoArgs, + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { - u.home = settings.Home - return u.run() + o.repoFile = settings.RepositoryConfig + o.repoCache = settings.RepositoryCache + return o.run(out) }, } return cmd } -func (u *repoUpdateCmd) run() error { - f, err := repo.LoadRepositoriesFile(u.home.RepositoryFile()) - if err != nil { - return err - } - - if len(f.Repositories) == 0 { +func (o *repoUpdateOptions) run(out io.Writer) error { + f, err := repo.LoadFile(o.repoFile) + switch { + case isNotExist(err): + return errNoRepositories + case err != nil: + return errors.Wrapf(err, "failed loading file: %s", o.repoFile) + case len(f.Repositories) == 0: return errNoRepositories } + var repos []*repo.ChartRepository for _, cfg := range f.Repositories { r, err := repo.NewChartRepository(cfg, getter.All(settings)) if err != nil { return err } + if o.repoCache != "" { + r.CachePath = o.repoCache + } repos = append(repos, r) } - u.update(repos, u.out, u.home) + o.update(repos, out) return nil } -func updateCharts(repos []*repo.ChartRepository, out io.Writer, home helmpath.Home) { +func updateCharts(repos []*repo.ChartRepository, out io.Writer) { fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") var wg sync.WaitGroup for _, re := range repos { wg.Add(1) go func(re *repo.ChartRepository) { defer wg.Done() - if re.Config.Name == localRepository { - fmt.Fprintf(out, "...Skip %s chart repository\n", re.Config.Name) - return - } - err := re.DownloadIndexFile(home.Cache()) - if err != nil { + if _, err := re.DownloadIndexFile(); err != nil { fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err) } else { fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name) @@ -105,5 +103,5 @@ func updateCharts(repos []*repo.ChartRepository, out io.Writer, home helmpath.Ho }(re) } wg.Wait() - fmt.Fprintln(out, "Update Complete. ⎈ Happy Helming!⎈ ") + fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") } diff --git a/cmd/helm/repo_update_test.go b/cmd/helm/repo_update_test.go index 68f964f3280..83ef24349e6 100644 --- a/cmd/helm/repo_update_test.go +++ b/cmd/helm/repo_update_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -19,82 +19,86 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "os" + "path/filepath" "strings" "testing" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/repo" - "k8s.io/helm/pkg/repo/repotest" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v3/pkg/repo/repotest" ) func TestUpdateCmd(t *testing.T) { - thome, err := tempHelmHome(t) - if err != nil { - t.Fatal(err) - } - - cleanup := resetEnv() - defer func() { - os.RemoveAll(thome.String()) - cleanup() - }() - - settings.Home = thome - - out := bytes.NewBuffer(nil) + var out bytes.Buffer // Instead of using the HTTP updater, we provide our own for this test. // The TestUpdateCharts test verifies the HTTP behavior independently. - updater := func(repos []*repo.ChartRepository, out io.Writer, hh helmpath.Home) { + updater := func(repos []*repo.ChartRepository, out io.Writer) { for _, re := range repos { fmt.Fprintln(out, re.Config.Name) } } - uc := &repoUpdateCmd{ - update: updater, - home: helmpath.Home(thome), - out: out, + o := &repoUpdateOptions{ + update: updater, + repoFile: "testdata/repositories.yaml", } - if err := uc.run(); err != nil { + if err := o.run(&out); err != nil { t.Fatal(err) } - if got := out.String(); !strings.Contains(got, "charts") || !strings.Contains(got, "local") { - t.Errorf("Expected 'charts' and 'local' (in any order) got %q", got) + if got := out.String(); !strings.Contains(got, "charts") { + t.Errorf("Expected 'charts' got %q", got) } } -func TestUpdateCharts(t *testing.T) { - ts, thome, err := repotest.NewTempServer("testdata/testserver/*.*") +func TestUpdateCustomCacheCmd(t *testing.T) { + rootDir := ensure.TempDir(t) + cachePath := filepath.Join(rootDir, "updcustomcache") + os.Mkdir(cachePath, os.ModePerm) + defer os.RemoveAll(cachePath) + + ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") if err != nil { t.Fatal(err) } + defer ts.Stop() - hh := helmpath.Home(thome) - cleanup := resetEnv() - defer func() { - ts.Stop() - os.RemoveAll(thome.String()) - cleanup() - }() - if err := ensureTestHome(hh, t); err != nil { + o := &repoUpdateOptions{ + update: updateCharts, + repoFile: filepath.Join(ts.Root(), "repositories.yaml"), + repoCache: cachePath, + } + b := ioutil.Discard + if err := o.run(b); err != nil { t.Fatal(err) } + if _, err := os.Stat(filepath.Join(cachePath, "test-index.yaml")); err != nil { + t.Fatalf("error finding created index file in custom cache: %v", err) + } +} - settings.Home = thome +func TestUpdateCharts(t *testing.T) { + defer resetEnv()() + defer ensure.HelmHome(t)() + + ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + defer ts.Stop() r, err := repo.NewChartRepository(&repo.Entry{ - Name: "charts", - URL: ts.URL(), - Cache: hh.CacheIndex("charts"), + Name: "charts", + URL: ts.URL(), }, getter.All(settings)) if err != nil { t.Error(err) } b := bytes.NewBuffer(nil) - updateCharts([]*repo.ChartRepository{r}, b, hh) + updateCharts([]*repo.ChartRepository{r}, b) got := b.String() if strings.Contains(got, "Unable to get an update") { @@ -104,3 +108,7 @@ func TestUpdateCharts(t *testing.T) { t.Error("Update was not successful") } } + +func TestRepoUpdateFileCompletion(t *testing.T) { + checkFileCompletion(t, "repo update", false) +} diff --git a/cmd/helm/require/args.go b/cmd/helm/require/args.go new file mode 100644 index 00000000000..cfa8a01691d --- /dev/null +++ b/cmd/helm/require/args.go @@ -0,0 +1,88 @@ +/* +Copyright The Helm Authors. +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 require + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// NoArgs returns an error if any args are included. +func NoArgs(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + return errors.Errorf( + "%q accepts no arguments\n\nUsage: %s", + cmd.CommandPath(), + cmd.UseLine(), + ) + } + return nil +} + +// ExactArgs returns an error if there are not exactly n args. +func ExactArgs(n int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) != n { + return errors.Errorf( + "%q requires %d %s\n\nUsage: %s", + cmd.CommandPath(), + n, + pluralize("argument", n), + cmd.UseLine(), + ) + } + return nil + } +} + +// MaximumNArgs returns an error if there are more than N args. +func MaximumNArgs(n int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) > n { + return errors.Errorf( + "%q accepts at most %d %s\n\nUsage: %s", + cmd.CommandPath(), + n, + pluralize("argument", n), + cmd.UseLine(), + ) + } + return nil + } +} + +// MinimumNArgs returns an error if there is not at least N args. +func MinimumNArgs(n int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) < n { + return errors.Errorf( + "%q requires at least %d %s\n\nUsage: %s", + cmd.CommandPath(), + n, + pluralize("argument", n), + cmd.UseLine(), + ) + } + return nil + } +} + +func pluralize(word string, n int) string { + if n == 1 { + return word + } + return word + "s" +} diff --git a/cmd/helm/require/args_test.go b/cmd/helm/require/args_test.go new file mode 100644 index 00000000000..c8d5c31102a --- /dev/null +++ b/cmd/helm/require/args_test.go @@ -0,0 +1,91 @@ +/* +Copyright The Helm Authors. +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 require + +import ( + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/spf13/cobra" +) + +func TestArgs(t *testing.T) { + runTestCases(t, []testCase{{ + validateFunc: NoArgs, + }, { + args: []string{"one"}, + validateFunc: NoArgs, + wantError: `"root" accepts no arguments`, + }, { + args: []string{"one"}, + validateFunc: ExactArgs(1), + }, { + validateFunc: ExactArgs(1), + wantError: `"root" requires 1 argument`, + }, { + validateFunc: ExactArgs(2), + wantError: `"root" requires 2 arguments`, + }, { + args: []string{"one"}, + validateFunc: MaximumNArgs(1), + }, { + args: []string{"one", "two"}, + validateFunc: MaximumNArgs(1), + wantError: `"root" accepts at most 1 argument`, + }, { + validateFunc: MinimumNArgs(1), + wantError: `"root" requires at least 1 argument`, + }, { + args: []string{"one", "two"}, + validateFunc: MinimumNArgs(1), + }}) +} + +type testCase struct { + args []string + validateFunc cobra.PositionalArgs + wantError string +} + +func runTestCases(t *testing.T, testCases []testCase) { + for i, tc := range testCases { + t.Run(fmt.Sprint(i), func(t *testing.T) { + cmd := &cobra.Command{ + Use: "root", + Run: func(*cobra.Command, []string) {}, + Args: tc.validateFunc, + } + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + + err := cmd.Execute() + if tc.wantError == "" { + if err != nil { + t.Fatalf("unexpected error, got '%v'", err) + } + return + } + if !strings.Contains(err.Error(), tc.wantError) { + t.Fatalf("unexpected error \n\nWANT:\n%q\n\nGOT:\n%q\n", tc.wantError, err) + } + if !strings.Contains(err.Error(), "Usage:") { + t.Fatalf("unexpected error: want Usage string\n\nGOT:\n%q\n", err) + } + }) + } +} diff --git a/cmd/helm/reset.go b/cmd/helm/reset.go deleted file mode 100644 index 9d3e17e03cd..00000000000 --- a/cmd/helm/reset.go +++ /dev/null @@ -1,131 +0,0 @@ -/* -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" - - "github.com/spf13/cobra" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" - - "k8s.io/helm/cmd/helm/installer" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/proto/hapi/release" -) - -const resetDesc = ` -This command uninstalls Tiller (the Helm server-side component) from your -Kubernetes Cluster and optionally deletes local configuration in -$HELM_HOME (default ~/.helm/) -` - -type resetCmd struct { - force bool - removeHelmHome bool - namespace string - out io.Writer - home helmpath.Home - client helm.Interface - kubeClient internalclientset.Interface -} - -func newResetCmd(client helm.Interface, out io.Writer) *cobra.Command { - d := &resetCmd{ - out: out, - client: client, - } - - cmd := &cobra.Command{ - Use: "reset", - Short: "uninstalls Tiller from a cluster", - Long: resetDesc, - PreRunE: func(cmd *cobra.Command, args []string) error { - if err := setupConnection(); !d.force && err != nil { - return err - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 0 { - return errors.New("This command does not accept arguments") - } - - d.namespace = settings.TillerNamespace - d.home = settings.Home - d.client = ensureHelmClient(d.client) - - return d.run() - }, - } - - f := cmd.Flags() - f.BoolVarP(&d.force, "force", "f", false, "forces Tiller uninstall even if there are releases installed, or if Tiller is not in ready state. Releases are not deleted.)") - f.BoolVar(&d.removeHelmHome, "remove-helm-home", false, "if set deletes $HELM_HOME") - - return cmd -} - -// runReset uninstalls tiller from Kubernetes Cluster and deletes local config -func (d *resetCmd) run() error { - if d.kubeClient == nil { - c, err := getInternalKubeClient(settings.KubeContext) - if err != nil { - return fmt.Errorf("could not get kubernetes client: %s", err) - } - d.kubeClient = c - } - - res, err := d.client.ListReleases( - helm.ReleaseListStatuses([]release.Status_Code{release.Status_DEPLOYED}), - ) - if !d.force && err != nil { - return prettyError(err) - } - - if !d.force && res != nil && len(res.Releases) > 0 { - return fmt.Errorf("there are still %d deployed releases (Tip: use --force to remove Tiller. Releases will not be deleted.)", len(res.Releases)) - } - - if err := installer.Uninstall(d.kubeClient, &installer.Options{Namespace: d.namespace}); err != nil { - return fmt.Errorf("error unstalling Tiller: %s", err) - } - - if d.removeHelmHome { - if err := deleteDirectories(d.home, d.out); err != nil { - return err - } - } - - fmt.Fprintln(d.out, "Tiller (the Helm server-side component) has been uninstalled from your Kubernetes Cluster.") - return nil -} - -// deleteDirectories deletes $HELM_HOME -func deleteDirectories(home helmpath.Home, out io.Writer) error { - if _, err := os.Stat(home.String()); err == nil { - fmt.Fprintf(out, "Deleting %s \n", home.String()) - if err := os.RemoveAll(home.String()); err != nil { - return fmt.Errorf("Could not remove %s: %s", home.String(), err) - } - } - - return nil -} diff --git a/cmd/helm/reset_test.go b/cmd/helm/reset_test.go deleted file mode 100644 index ae6a0003609..00000000000 --- a/cmd/helm/reset_test.go +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "bytes" - "io/ioutil" - "os" - "strings" - "testing" - - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/proto/hapi/release" -) - -type resetCase struct { - name string - err bool - resp []*release.Release - removeHelmHome bool - force bool - expectedActions int - expectedOutput string -} - -func TestResetCmd(t *testing.T) { - - verifyResetCmd(t, resetCase{ - name: "test reset command", - expectedActions: 3, - expectedOutput: "Tiller (the Helm server-side component) has been uninstalled from your Kubernetes Cluster.", - }) -} - -func TestResetCmd_removeHelmHome(t *testing.T) { - verifyResetCmd(t, resetCase{ - name: "test reset command - remove helm home", - removeHelmHome: true, - expectedActions: 3, - expectedOutput: "Tiller (the Helm server-side component) has been uninstalled from your Kubernetes Cluster.", - }) -} - -func TestReset_deployedReleases(t *testing.T) { - verifyResetCmd(t, resetCase{ - name: "test reset command - deployed releases", - resp: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), - }, - err: true, - expectedOutput: "there are still 1 deployed releases (Tip: use --force to remove Tiller. Releases will not be deleted.)", - }) -} - -func TestReset_forceFlag(t *testing.T) { - verifyResetCmd(t, resetCase{ - name: "test reset command - force flag", - force: true, - resp: []*release.Release{ - helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", StatusCode: release.Status_DEPLOYED}), - }, - expectedActions: 3, - expectedOutput: "Tiller (the Helm server-side component) has been uninstalled from your Kubernetes Cluster.", - }) -} - -func verifyResetCmd(t *testing.T, tc resetCase) { - home, err := ioutil.TempDir("", "helm_home") - if err != nil { - t.Fatal(err) - } - defer os.Remove(home) - - var buf bytes.Buffer - c := &helm.FakeClient{ - Rels: tc.resp, - } - fc := fake.NewSimpleClientset() - cmd := &resetCmd{ - removeHelmHome: tc.removeHelmHome, - force: tc.force, - out: &buf, - home: helmpath.Home(home), - client: c, - kubeClient: fc, - namespace: core.NamespaceDefault, - } - - err = cmd.run() - if !tc.err && err != nil { - t.Errorf("unexpected error: %v", err) - } - - got := buf.String() - if tc.err { - got = err.Error() - } - - actions := fc.Actions() - if tc.expectedActions > 0 && len(actions) != tc.expectedActions { - t.Errorf("Expected %d actions, got %d", tc.expectedActions, len(actions)) - } - if !strings.Contains(got, tc.expectedOutput) { - t.Errorf("expected %q, got %q", tc.expectedOutput, got) - } - _, err = os.Stat(home) - if !tc.removeHelmHome && err != nil { - t.Errorf("Helm home directory %s does not exists", home) - } - if tc.removeHelmHome && err == nil { - t.Errorf("Helm home directory %s exists", home) - } -} diff --git a/cmd/helm/rollback.go b/cmd/helm/rollback.go index 889b6ae2860..ea4b75cb1d4 100644 --- a/cmd/helm/rollback.go +++ b/cmd/helm/rollback.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,88 +20,71 @@ import ( "fmt" "io" "strconv" + "time" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" ) const rollbackDesc = ` This command rolls back a release to a previous revision. The first argument of the rollback command is the name of a release, and the -second is a revision (version) number. To see revision numbers, run -'helm history RELEASE'. +second is a revision (version) number. If this argument is omitted, it will +roll back to the previous release. + +To see revision numbers, run 'helm history RELEASE'. ` -type rollbackCmd struct { - name string - revision int32 - dryRun bool - recreate bool - force bool - disableHooks bool - out io.Writer - client helm.Interface - timeout int64 - wait bool -} - -func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command { - rollback := &rollbackCmd{ - out: out, - client: c, - } +func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewRollback(cfg) cmd := &cobra.Command{ - Use: "rollback [flags] [RELEASE] [REVISION]", - Short: "roll back a release to a previous revision", - Long: rollbackDesc, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, - RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(len(args), "release name", "revision number"); err != nil { - return err + Use: "rollback [REVISION]", + Short: "roll back a release to a previous revision", + Long: rollbackDesc, + Args: require.MinimumNArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + return compListReleases(toComplete, args, cfg) } - rollback.name = args[0] + if len(args) == 1 { + return compListRevisions(toComplete, cfg, args[0]) + } - v64, err := strconv.ParseInt(args[1], 10, 32) - if err != nil { - return fmt.Errorf("invalid revision number '%q': %s", args[1], err) + return nil, cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 1 { + ver, err := strconv.Atoi(args[1]) + if err != nil { + return fmt.Errorf("could not convert revision to a number: %v", err) + } + client.Version = ver } - rollback.revision = int32(v64) - rollback.client = ensureHelmClient(rollback.client) - return rollback.run() + if err := client.Run(args[0]); err != nil { + return err + } + + fmt.Fprintf(out, "Rollback was a success! Happy Helming!\n") + return nil }, } f := cmd.Flags() - f.BoolVar(&rollback.dryRun, "dry-run", false, "simulate a rollback") - f.BoolVar(&rollback.recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") - f.BoolVar(&rollback.force, "force", false, "force resource update through delete/recreate if needed") - f.BoolVar(&rollback.disableHooks, "no-hooks", false, "prevent hooks from running during rollback") - f.Int64Var(&rollback.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") - f.BoolVar(&rollback.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") + f.BoolVar(&client.DryRun, "dry-run", false, "simulate a rollback") + f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") + f.BoolVar(&client.Force, "force", false, "force resource update through delete/recreate if needed") + f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during rollback") + f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout") + f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout") + f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails") + f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit") return cmd } - -func (r *rollbackCmd) run() error { - _, err := r.client.RollbackRelease( - r.name, - helm.RollbackDryRun(r.dryRun), - helm.RollbackRecreate(r.recreate), - helm.RollbackForce(r.force), - helm.RollbackDisableHooks(r.disableHooks), - helm.RollbackVersion(r.revision), - helm.RollbackTimeout(r.timeout), - helm.RollbackWait(r.wait)) - if err != nil { - return prettyError(err) - } - - fmt.Fprintf(r.out, "Rollback was a success! Happy Helming!\n") - - return nil -} diff --git a/cmd/helm/rollback_test.go b/cmd/helm/rollback_test.go index f1479b2eb2c..9ca92155715 100644 --- a/cmd/helm/rollback_test.go +++ b/cmd/helm/rollback_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,45 +17,101 @@ limitations under the License. package main import ( - "io" "testing" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/release" ) func TestRollbackCmd(t *testing.T) { - - tests := []releaseCase{ - { - name: "rollback a release", - args: []string{"funny-honey", "1"}, - expected: "Rollback was a success! Happy Helming!", - }, - { - name: "rollback a release with timeout", - args: []string{"funny-honey", "1"}, - flags: []string{"--timeout", "120"}, - expected: "Rollback was a success! Happy Helming!", - }, + rels := []*release.Release{ { - name: "rollback a release with wait", - args: []string{"funny-honey", "1"}, - flags: []string{"--wait"}, - expected: "Rollback was a success! Happy Helming!", + Name: "funny-honey", + Info: &release.Info{Status: release.StatusSuperseded}, + Chart: &chart.Chart{}, + Version: 1, }, { - name: "rollback a release without revision", - args: []string{"funny-honey"}, - err: true, + Name: "funny-honey", + Info: &release.Info{Status: release.StatusDeployed}, + Chart: &chart.Chart{}, + Version: 2, }, } - cmd := func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newRollbackCmd(c, out) + tests := []cmdTestCase{{ + name: "rollback a release", + cmd: "rollback funny-honey 1", + golden: "output/rollback.txt", + rels: rels, + }, { + name: "rollback a release with timeout", + cmd: "rollback funny-honey 1 --timeout 120s", + golden: "output/rollback-timeout.txt", + rels: rels, + }, { + name: "rollback a release with wait", + cmd: "rollback funny-honey 1 --wait", + golden: "output/rollback-wait.txt", + rels: rels, + }, { + name: "rollback a release with wait-for-jobs", + cmd: "rollback funny-honey 1 --wait --wait-for-jobs", + golden: "output/rollback-wait-for-jobs.txt", + rels: rels, + }, { + name: "rollback a release without revision", + cmd: "rollback funny-honey", + golden: "output/rollback-no-revision.txt", + rels: rels, + }, { + name: "rollback a release without release name", + cmd: "rollback", + golden: "output/rollback-no-args.txt", + rels: rels, + wantError: true, + }} + runTestCmd(t, tests) +} + +func TestRollbackRevisionCompletion(t *testing.T) { + mk := func(name string, vers int, status release.Status) *release.Release { + return release.Mock(&release.MockReleaseOptions{ + Name: name, + Version: vers, + Status: status, + }) } - runReleaseCases(t, tests, cmd) + releases := []*release.Release{ + mk("musketeers", 11, release.StatusDeployed), + mk("musketeers", 10, release.StatusSuperseded), + mk("musketeers", 9, release.StatusSuperseded), + mk("musketeers", 8, release.StatusSuperseded), + mk("carabins", 1, release.StatusSuperseded), + } + + tests := []cmdTestCase{{ + name: "completion for release parameter", + cmd: "__complete rollback ''", + rels: releases, + golden: "output/rollback-comp.txt", + }, { + name: "completion for revision parameter", + cmd: "__complete rollback musketeers ''", + rels: releases, + golden: "output/revision-comp.txt", + }, { + name: "completion for with too many args", + cmd: "__complete rollback musketeers 11 ''", + rels: releases, + golden: "output/rollback-wrong-args-comp.txt", + }} + runTestCmd(t, tests) +} +func TestRollbackFileCompletion(t *testing.T) { + checkFileCompletion(t, "rollback", false) + checkFileCompletion(t, "rollback myrelease", false) + checkFileCompletion(t, "rollback myrelease 1", false) } diff --git a/cmd/helm/root.go b/cmd/helm/root.go new file mode 100644 index 00000000000..285c800215b --- /dev/null +++ b/cmd/helm/root.go @@ -0,0 +1,264 @@ +/* +Copyright The Helm Authors. + +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 "helm.sh/helm/v3/cmd/helm" + +import ( + "context" + "fmt" + "io" + "log" + "os" + "strings" + + "github.com/spf13/cobra" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" + + "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/repo" +) + +var globalUsage = `The Kubernetes package manager + +Common actions for Helm: + +- helm search: search for charts +- helm pull: download a chart to your local directory to view +- helm install: upload the chart to Kubernetes +- helm list: list releases of charts + +Environment variables: + +| Name | Description | +|------------------------------------|-----------------------------------------------------------------------------------| +| $HELM_CACHE_HOME | set an alternative location for storing cached files. | +| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. | +| $HELM_DATA_HOME | set an alternative location for storing Helm data. | +| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode | +| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, postgres | +| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. | +| $HELM_MAX_HISTORY | set the maximum number of helm release history. | +| $HELM_NAMESPACE | set the namespace used for the helm operations. | +| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. | +| $HELM_PLUGINS | set the path to the plugins directory | +| $HELM_REGISTRY_CONFIG | set the path to the registry config file. | +| $HELM_REPOSITORY_CACHE | set the path to the repository cache directory | +| $HELM_REPOSITORY_CONFIG | set the path to the repositories file. | +| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") | +| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication | +| $HELM_KUBECAFILE | set the Kubernetes certificate authority file. | +| $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. | +| $HELM_KUBEASUSER | set the Username to impersonate for the operation. | +| $HELM_KUBECONTEXT | set the name of the kubeconfig context. | +| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. | + +Helm stores cache, configuration, and data based on the following configuration order: + +- If a HELM_*_HOME environment variable is set, it will be used +- Otherwise, on systems supporting the XDG base directory specification, the XDG variables will be used +- When no other location is set a default location will be used based on the operating system + +By default, the default directories depend on the Operating System. The defaults are listed below: + +| Operating System | Cache Path | Configuration Path | Data Path | +|------------------|---------------------------|--------------------------------|-------------------------| +| Linux | $HOME/.cache/helm | $HOME/.config/helm | $HOME/.local/share/helm | +| macOS | $HOME/Library/Caches/helm | $HOME/Library/Preferences/helm | $HOME/Library/helm | +| Windows | %TEMP%\helm | %APPDATA%\helm | %APPDATA%\helm | +` + +func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "helm", + Short: "The Helm package manager for Kubernetes.", + Long: globalUsage, + SilenceUsage: true, + } + flags := cmd.PersistentFlags() + + settings.AddFlags(flags) + addKlogFlags(flags) + + // Setup shell completion for the namespace flag + err := cmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if client, err := actionConfig.KubernetesClientSet(); err == nil { + // Choose a long enough timeout that the user notices something is not working + // but short enough that the user is not made to wait very long + to := int64(3) + cobra.CompDebugln(fmt.Sprintf("About to call kube client for namespaces with timeout of: %d", to), settings.Debug) + + nsNames := []string{} + if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil { + for _, ns := range namespaces.Items { + if strings.HasPrefix(ns.Name, toComplete) { + nsNames = append(nsNames, ns.Name) + } + } + return nsNames, cobra.ShellCompDirectiveNoFileComp + } + } + return nil, cobra.ShellCompDirectiveDefault + }) + + if err != nil { + log.Fatal(err) + } + + // Setup shell completion for the kube-context flag + err = cmd.RegisterFlagCompletionFunc("kube-context", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cobra.CompDebugln("About to get the different kube-contexts", settings.Debug) + + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if len(settings.KubeConfig) > 0 { + loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: settings.KubeConfig} + } + if config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + loadingRules, + &clientcmd.ConfigOverrides{}).RawConfig(); err == nil { + comps := []string{} + for name, context := range config.Contexts { + if strings.HasPrefix(name, toComplete) { + comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster)) + } + } + return comps, cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) + + if err != nil { + log.Fatal(err) + } + + // We can safely ignore any errors that flags.Parse encounters since + // those errors will be caught later during the call to cmd.Execution. + // This call is required to gather configuration information prior to + // execution. + flags.ParseErrorsWhitelist.UnknownFlags = true + flags.Parse(args) + + registryClient, err := registry.NewClient( + registry.ClientOptDebug(settings.Debug), + registry.ClientOptWriter(out), + registry.ClientOptCredentialsFile(settings.RegistryConfig), + ) + if err != nil { + return nil, err + } + actionConfig.RegistryClient = registryClient + + // Add subcommands + cmd.AddCommand( + // chart commands + newCreateCmd(out), + newDependencyCmd(actionConfig, out), + newPullCmd(actionConfig, out), + newShowCmd(out), + newLintCmd(out), + newPackageCmd(out), + newRepoCmd(out), + newSearchCmd(out), + newVerifyCmd(out), + + // release commands + newGetCmd(actionConfig, out), + newHistoryCmd(actionConfig, out), + newInstallCmd(actionConfig, out), + newListCmd(actionConfig, out), + newReleaseTestCmd(actionConfig, out), + newRollbackCmd(actionConfig, out), + newStatusCmd(actionConfig, out), + newTemplateCmd(actionConfig, out), + newUninstallCmd(actionConfig, out), + newUpgradeCmd(actionConfig, out), + + newCompletionCmd(out), + newEnvCmd(out), + newPluginCmd(out), + newVersionCmd(out), + + // Hidden documentation generator command: 'helm docs' + newDocsCmd(out), + ) + + // Add *experimental* subcommands + cmd.AddCommand( + newRegistryCmd(actionConfig, out), + newChartCmd(actionConfig, out), + ) + + // Find and add plugins + loadPlugins(cmd, out) + + // Check permissions on critical files + checkPerms() + + // Check for expired repositories + checkForExpiredRepos(settings.RepositoryConfig) + + return cmd, nil +} + +func checkForExpiredRepos(repofile string) { + + expiredRepos := []struct { + name string + old string + new string + }{ + { + name: "stable", + old: "kubernetes-charts.storage.googleapis.com", + new: "https://charts.helm.sh/stable", + }, + { + name: "incubator", + old: "kubernetes-charts-incubator.storage.googleapis.com", + new: "https://charts.helm.sh/incubator", + }, + } + + // parse repo file. + // Ignore the error because it is okay for a repo file to be unparseable at this + // stage. Later checks will trap the error and respond accordingly. + repoFile, err := repo.LoadFile(repofile) + if err != nil { + return + } + + for _, exp := range expiredRepos { + r := repoFile.Get(exp.name) + if r == nil { + return + } + + if url := r.URL; strings.Contains(url, exp.old) { + fmt.Fprintf( + os.Stderr, + "WARNING: %q is deprecated for %q and will be deleted Nov. 13, 2020.\nWARNING: You should switch to %q via:\nWARNING: helm repo add %q %q --force-update\n", + exp.old, + exp.name, + exp.new, + exp.name, + exp.new, + ) + } + } + +} diff --git a/cmd/helm/root_test.go b/cmd/helm/root_test.go new file mode 100644 index 00000000000..075544971f8 --- /dev/null +++ b/cmd/helm/root_test.go @@ -0,0 +1,131 @@ +/* +Copyright The Helm Authors. + +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 ( + "os" + "path/filepath" + "testing" + + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/helmpath/xdg" +) + +func TestRootCmd(t *testing.T) { + defer resetEnv()() + + tests := []struct { + name, args, cachePath, configPath, dataPath string + envvars map[string]string + }{ + { + name: "defaults", + args: "env", + }, + { + name: "with $XDG_CACHE_HOME set", + args: "env", + envvars: map[string]string{xdg.CacheHomeEnvVar: "/bar"}, + cachePath: "/bar/helm", + }, + { + name: "with $XDG_CONFIG_HOME set", + args: "env", + envvars: map[string]string{xdg.ConfigHomeEnvVar: "/bar"}, + configPath: "/bar/helm", + }, + { + name: "with $XDG_DATA_HOME set", + args: "env", + envvars: map[string]string{xdg.DataHomeEnvVar: "/bar"}, + dataPath: "/bar/helm", + }, + { + name: "with $HELM_CACHE_HOME set", + args: "env", + envvars: map[string]string{helmpath.CacheHomeEnvVar: "/foo/helm"}, + cachePath: "/foo/helm", + }, + { + name: "with $HELM_CONFIG_HOME set", + args: "env", + envvars: map[string]string{helmpath.ConfigHomeEnvVar: "/foo/helm"}, + configPath: "/foo/helm", + }, + { + name: "with $HELM_DATA_HOME set", + args: "env", + envvars: map[string]string{helmpath.DataHomeEnvVar: "/foo/helm"}, + dataPath: "/foo/helm", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer ensure.HelmHome(t)() + + for k, v := range tt.envvars { + os.Setenv(k, v) + } + + if _, _, err := executeActionCommand(tt.args); err != nil { + t.Fatalf("unexpected error: %s", err) + } + + // NOTE(bacongobbler): we need to check here after calling ensure.HelmHome so we + // load the proper paths after XDG_*_HOME is set + if tt.cachePath == "" { + tt.cachePath = filepath.Join(os.Getenv(xdg.CacheHomeEnvVar), "helm") + } + + if tt.configPath == "" { + tt.configPath = filepath.Join(os.Getenv(xdg.ConfigHomeEnvVar), "helm") + } + + if tt.dataPath == "" { + tt.dataPath = filepath.Join(os.Getenv(xdg.DataHomeEnvVar), "helm") + } + + if helmpath.CachePath() != tt.cachePath { + t.Errorf("expected cache path %q, got %q", tt.cachePath, helmpath.CachePath()) + } + if helmpath.ConfigPath() != tt.configPath { + t.Errorf("expected config path %q, got %q", tt.configPath, helmpath.ConfigPath()) + } + if helmpath.DataPath() != tt.dataPath { + t.Errorf("expected data path %q, got %q", tt.dataPath, helmpath.DataPath()) + } + }) + } +} + +func TestUnknownSubCmd(t *testing.T) { + _, _, err := executeActionCommand("foobar") + + if err == nil || err.Error() != `unknown command "foobar" for "helm"` { + t.Errorf("Expect unknown command error, got %q", err) + } +} + +// Need the release of Cobra following 1.0 to be able to disable +// file completion on the root command. Until then, we cannot +// because it would break 'helm help ' +// +// func TestRootFileCompletion(t *testing.T) { +// checkFileCompletion(t, "", false) +// } diff --git a/cmd/helm/root_unix.go b/cmd/helm/root_unix.go new file mode 100644 index 00000000000..3df801e4c85 --- /dev/null +++ b/cmd/helm/root_unix.go @@ -0,0 +1,58 @@ +// +build !windows + +/* +Copyright The Helm Authors. + +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 ( + "os" + "os/user" + "path/filepath" +) + +func checkPerms() { + // This function MUST NOT FAIL, as it is just a check for a common permissions problem. + // If for some reason the function hits a stopping condition, it may panic. But only if + // we can be sure that it is panicking because Helm cannot proceed. + + kc := settings.KubeConfig + if kc == "" { + kc = os.Getenv("KUBECONFIG") + } + if kc == "" { + u, err := user.Current() + if err != nil { + // No idea where to find KubeConfig, so return silently. Many helm commands + // can proceed happily without a KUBECONFIG, so this is not a fatal error. + return + } + kc = filepath.Join(u.HomeDir, ".kube", "config") + } + fi, err := os.Stat(kc) + if err != nil { + // DO NOT error if no KubeConfig is found. Not all commands require one. + return + } + + perm := fi.Mode().Perm() + if perm&0040 > 0 { + warning("Kubernetes configuration file is group-readable. This is insecure. Location: %s", kc) + } + if perm&0004 > 0 { + warning("Kubernetes configuration file is world-readable. This is insecure. Location: %s", kc) + } +} diff --git a/cmd/helm/root_unix_test.go b/cmd/helm/root_unix_test.go new file mode 100644 index 00000000000..c62776c2a04 --- /dev/null +++ b/cmd/helm/root_unix_test.go @@ -0,0 +1,87 @@ +// +build !windows + +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" +) + +func checkPermsStderr() (string, error) { + r, w, err := os.Pipe() + if err != nil { + return "", err + } + + stderr := os.Stderr + os.Stderr = w + defer func() { + os.Stderr = stderr + }() + + checkPerms() + w.Close() + + var text bytes.Buffer + io.Copy(&text, r) + return text.String(), nil +} + +func TestCheckPerms(t *testing.T) { + tdir, err := ioutil.TempDir("", "helmtest") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tdir) + tfile := filepath.Join(tdir, "testconfig") + fh, err := os.OpenFile(tfile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0440) + if err != nil { + t.Errorf("Failed to create temp file: %s", err) + } + + tconfig := settings.KubeConfig + settings.KubeConfig = tfile + defer func() { settings.KubeConfig = tconfig }() + + text, err := checkPermsStderr() + if err != nil { + t.Fatalf("could not read from stderr: %s", err) + } + expectPrefix := "WARNING: Kubernetes configuration file is group-readable. This is insecure. Location:" + if !strings.HasPrefix(text, expectPrefix) { + t.Errorf("Expected to get a warning for group perms. Got %q", text) + } + + if err := fh.Chmod(0404); err != nil { + t.Errorf("Could not change mode on file: %s", err) + } + text, err = checkPermsStderr() + if err != nil { + t.Fatalf("could not read from stderr: %s", err) + } + expectPrefix = "WARNING: Kubernetes configuration file is world-readable. This is insecure. Location:" + if !strings.HasPrefix(text, expectPrefix) { + t.Errorf("Expected to get a warning for world perms. Got %q", text) + } +} diff --git a/cmd/helm/root_windows.go b/cmd/helm/root_windows.go new file mode 100644 index 00000000000..7b5000f4fe3 --- /dev/null +++ b/cmd/helm/root_windows.go @@ -0,0 +1,22 @@ +/* +Copyright The Helm Authors. + +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 + +func checkPerms() { + // Not yet implemented on Windows. If you know how to do a comprehensive perms + // check on Windows, contributions welcomed! +} diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 845bfd0be8b..6c62d5d2ef1 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,146 +17,27 @@ limitations under the License. package main import ( - "fmt" "io" - "strings" - "github.com/Masterminds/semver" - "github.com/gosuri/uitable" "github.com/spf13/cobra" - - "k8s.io/helm/cmd/helm/search" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/repo" ) const searchDesc = ` -Search reads through all of the repositories configured on the system, and -looks for matches. - -Repositories are managed with 'helm repo' commands. +Search provides the ability to search for Helm charts in the various places +they can be stored including the Artifact Hub and repositories you have added. +Use search subcommands to search different locations for charts. ` -// searchMaxScore suggests that any score higher than this is not considered a match. -const searchMaxScore = 25 - -type searchCmd struct { - out io.Writer - helmhome helmpath.Home - - versions bool - regexp bool - version string -} - func newSearchCmd(out io.Writer) *cobra.Command { - sc := &searchCmd{out: out} cmd := &cobra.Command{ Use: "search [keyword]", Short: "search for a keyword in charts", Long: searchDesc, - RunE: func(cmd *cobra.Command, args []string) error { - sc.helmhome = settings.Home - return sc.run(args) - }, } - f := cmd.Flags() - f.BoolVarP(&sc.regexp, "regexp", "r", false, "use regular expressions for searching") - f.BoolVarP(&sc.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line") - f.StringVarP(&sc.version, "version", "v", "", "search using semantic versioning constraints") + cmd.AddCommand(newSearchHubCmd(out)) + cmd.AddCommand(newSearchRepoCmd(out)) return cmd } - -func (s *searchCmd) run(args []string) error { - index, err := s.buildIndex() - if err != nil { - return err - } - - var res []*search.Result - if len(args) == 0 { - res = index.All() - } else { - q := strings.Join(args, " ") - res, err = index.Search(q, searchMaxScore, s.regexp) - if err != nil { - return err - } - } - - search.SortScore(res) - data, err := s.applyConstraint(res) - if err != nil { - return err - } - - fmt.Fprintln(s.out, s.formatSearchResults(data)) - - return nil -} - -func (s *searchCmd) applyConstraint(res []*search.Result) ([]*search.Result, error) { - if len(s.version) == 0 { - return res, nil - } - - constraint, err := semver.NewConstraint(s.version) - if err != nil { - return res, fmt.Errorf("an invalid version/constraint format: %s", err) - } - - data := res[:0] - foundNames := map[string]bool{} - for _, r := range res { - if _, found := foundNames[r.Name]; found { - continue - } - v, err := semver.NewVersion(r.Chart.Version) - if err != nil || constraint.Check(v) { - data = append(data, r) - if !s.versions { - foundNames[r.Name] = true // If user hasn't requested all versions, only show the latest that matches - } - } - } - - return data, nil -} - -func (s *searchCmd) formatSearchResults(res []*search.Result) string { - if len(res) == 0 { - return "No results found" - } - table := uitable.New() - table.MaxColWidth = 50 - table.AddRow("NAME", "CHART VERSION", "APP VERSION", "DESCRIPTION") - for _, r := range res { - table.AddRow(r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description) - } - return table.String() -} - -func (s *searchCmd) buildIndex() (*search.Index, error) { - // Load the repositories.yaml - rf, err := repo.LoadRepositoriesFile(s.helmhome.RepositoryFile()) - if err != nil { - return nil, err - } - - i := search.NewIndex() - for _, re := range rf.Repositories { - n := re.Name - f := s.helmhome.CacheIndex(n) - ind, err := repo.LoadIndexFile(f) - if err != nil { - fmt.Fprintf(s.out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n) - continue - } - - i.AddRepo(n, ind, s.versions || len(s.version) > 0) - } - return i, nil -} diff --git a/cmd/helm/search/search.go b/cmd/helm/search/search.go index 6c4cb4aa48b..fc7f30596b7 100644 --- a/cmd/helm/search/search.go +++ b/cmd/helm/search/search.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,14 +23,14 @@ to find matches. package search import ( - "errors" "path" "regexp" "sort" "strings" - "github.com/Masterminds/semver" - "k8s.io/helm/pkg/repo" + "github.com/Masterminds/semver/v3" + + "helm.sh/helm/v3/pkg/repo" ) // Result is a search result. @@ -51,7 +51,7 @@ type Index struct { const sep = "\v" -// NewIndex creats a new Index. +// NewIndex creates a new Index. func NewIndex() *Index { return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}} } @@ -177,15 +177,6 @@ func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { return buf, nil } -// Chart returns the ChartVersion for a particular name. -func (i *Index) Chart(name string) (*repo.ChartVersion, error) { - c, ok := i.charts[name] - if !ok { - return nil, errors.New("no such chart") - } - return c, nil -} - // SortScore does an in-place sort of the results. // // Lowest scores are highest on the list. Matching scores are subsorted alphabetically. diff --git a/cmd/helm/search/search_test.go b/cmd/helm/search/search_test.go index 574f55448a4..9c1859d7700 100644 --- a/cmd/helm/search/search_test.go +++ b/cmd/helm/search/search_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,8 +20,8 @@ import ( "strings" "testing" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/repo" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/repo" ) func TestSortScore(t *testing.T) { @@ -236,36 +236,38 @@ func TestSearchByName(t *testing.T) { i := loadTestIndex(t, false) for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { - charts, err := i.Search(tt.query, 100, tt.regexp) - if err != nil { - if tt.fail { - if !strings.Contains(err.Error(), tt.failMsg) { - t.Fatalf("%s: Unexpected error message: %s", tt.name, err) + charts, err := i.Search(tt.query, 100, tt.regexp) + if err != nil { + if tt.fail { + if !strings.Contains(err.Error(), tt.failMsg) { + t.Fatalf("Unexpected error message: %s", err) + } + return } - continue + t.Fatalf("%s: %s", tt.name, err) } - t.Fatalf("%s: %s", tt.name, err) - } - // Give us predictably ordered results. - SortScore(charts) + // Give us predictably ordered results. + SortScore(charts) - l := len(charts) - if l != len(tt.expect) { - t.Fatalf("%s: Expected %d result, got %d", tt.name, len(tt.expect), l) - } - // For empty result sets, just keep going. - if l == 0 { - continue - } + l := len(charts) + if l != len(tt.expect) { + t.Fatalf("Expected %d result, got %d", len(tt.expect), l) + } + // For empty result sets, just keep going. + if l == 0 { + return + } - for i, got := range charts { - ex := tt.expect[i] - if got.Name != ex.Name { - t.Errorf("%s[%d]: Expected name %q, got %q", tt.name, i, ex.Name, got.Name) + for i, got := range charts { + ex := tt.expect[i] + if got.Name != ex.Name { + t.Errorf("[%d]: Expected name %q, got %q", i, ex.Name, got.Name) + } } - } + }) } } diff --git a/cmd/helm/search_hub.go b/cmd/helm/search_hub.go new file mode 100644 index 00000000000..82b55578820 --- /dev/null +++ b/cmd/helm/search_hub.go @@ -0,0 +1,165 @@ +/* +Copyright The Helm Authors. + +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" + "strings" + + "github.com/gosuri/uitable" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "helm.sh/helm/v3/internal/monocular" + "helm.sh/helm/v3/pkg/cli/output" +) + +const searchHubDesc = ` +Search for Helm charts in the Artifact Hub or your own hub instance. + +Artifact Hub is a web-based application that enables finding, installing, and +publishing packages and configurations for CNCF projects, including publicly +available distributed charts Helm charts. It is a Cloud Native Computing +Foundation sandbox project. You can browse the hub at https://artifacthub.io/ + +The [KEYWORD] argument accepts either a keyword string, or quoted string of rich +query options. For rich query options documentation, see +https://artifacthub.github.io/hub/api/?urls.primaryName=Monocular%20compatible%20search%20API#/Monocular/get_api_chartsvc_v1_charts_search + +Previous versions of Helm used an instance of Monocular as the default +'endpoint', so for backwards compatibility Artifact Hub is compatible with the +Monocular search API. Similarly, when setting the 'endpoint' flag, the specified +endpoint must also be implement a Monocular compatible search API endpoint. +Note that when specifying a Monocular instance as the 'endpoint', rich queries +are not supported. For API details, see https://github.com/helm/monocular +` + +type searchHubOptions struct { + searchEndpoint string + maxColWidth uint + outputFormat output.Format +} + +func newSearchHubCmd(out io.Writer) *cobra.Command { + o := &searchHubOptions{} + + cmd := &cobra.Command{ + Use: "hub [KEYWORD]", + Short: "search for charts in the Artifact Hub or your own hub instance", + Long: searchHubDesc, + RunE: func(cmd *cobra.Command, args []string) error { + return o.run(out, args) + }, + } + + f := cmd.Flags() + f.StringVar(&o.searchEndpoint, "endpoint", "https://hub.helm.sh", "Hub instance to query for charts") + f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table") + bindOutputFlag(cmd, &o.outputFormat) + + return cmd +} + +func (o *searchHubOptions) run(out io.Writer, args []string) error { + c, err := monocular.New(o.searchEndpoint) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("unable to create connection to %q", o.searchEndpoint)) + } + + q := strings.Join(args, " ") + results, err := c.Search(q) + if err != nil { + debug("%s", err) + return fmt.Errorf("unable to perform search against %q", o.searchEndpoint) + } + + return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth)) +} + +type hubChartElement struct { + URL string `json:"url"` + Version string `json:"version"` + AppVersion string `json:"app_version"` + Description string `json:"description"` +} + +type hubSearchWriter struct { + elements []hubChartElement + columnWidth uint +} + +func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint) *hubSearchWriter { + var elements []hubChartElement + for _, r := range results { + // Backwards compatibility for Monocular + url := endpoint + "/charts/" + r.ID + + // Check for artifactHub compatibility + if r.ArtifactHub.PackageURL != "" { + url = r.ArtifactHub.PackageURL + } + + elements = append(elements, hubChartElement{url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description}) + } + return &hubSearchWriter{elements, columnWidth} +} + +func (h *hubSearchWriter) WriteTable(out io.Writer) error { + if len(h.elements) == 0 { + _, err := out.Write([]byte("No results found\n")) + if err != nil { + return fmt.Errorf("unable to write results: %s", err) + } + return nil + } + table := uitable.New() + table.MaxColWidth = h.columnWidth + table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION") + for _, r := range h.elements { + table.AddRow(r.URL, r.Version, r.AppVersion, r.Description) + } + return output.EncodeTable(out, table) +} + +func (h *hubSearchWriter) WriteJSON(out io.Writer) error { + return h.encodeByFormat(out, output.JSON) +} + +func (h *hubSearchWriter) WriteYAML(out io.Writer) error { + return h.encodeByFormat(out, output.YAML) +} + +func (h *hubSearchWriter) encodeByFormat(out io.Writer, format output.Format) error { + // Initialize the array so no results returns an empty array instead of null + chartList := make([]hubChartElement, 0, len(h.elements)) + + for _, r := range h.elements { + chartList = append(chartList, hubChartElement{r.URL, r.Version, r.AppVersion, r.Description}) + } + + switch format { + case output.JSON: + return output.EncodeJSON(out, chartList) + case output.YAML: + return output.EncodeYAML(out, chartList) + } + + // Because this is a non-exported function and only called internally by + // WriteJSON and WriteYAML, we shouldn't get invalid types + return nil +} diff --git a/cmd/helm/search_hub_test.go b/cmd/helm/search_hub_test.go new file mode 100644 index 00000000000..ae51b6a3e62 --- /dev/null +++ b/cmd/helm/search_hub_test.go @@ -0,0 +1,60 @@ +/* +Copyright The Helm Authors. + +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" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSearchHubCmd(t *testing.T) { + + // Setup a mock search service + var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}` + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, searchResult) + })) + defer ts.Close() + + // The expected output has the URL to the mocked search service in it + var expected = fmt.Sprintf(`URL CHART VERSION APP VERSION DESCRIPTION +%s/charts/stable/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend +%s/charts/bitnami/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend +`, ts.URL, ts.URL) + + testcmd := "search hub --endpoint " + ts.URL + " maria" + storage := storageFixture() + _, out, err := executeActionCommandC(storage, testcmd) + if err != nil { + t.Errorf("unexpected error, %s", err) + } + if out != expected { + t.Error("expected and actual output did not match") + t.Log(out) + t.Log(expected) + } +} + +func TestSearchHubOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "search hub") +} + +func TestSearchHubFileCompletion(t *testing.T) { + checkFileCompletion(t, "search hub", true) // File completion may be useful when inputting a keyword +} diff --git a/cmd/helm/search_repo.go b/cmd/helm/search_repo.go new file mode 100644 index 00000000000..ba692a2e7c3 --- /dev/null +++ b/cmd/helm/search_repo.go @@ -0,0 +1,374 @@ +/* +Copyright The Helm Authors. + +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" + "io/ioutil" + "path/filepath" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/gosuri/uitable" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/search" + "helm.sh/helm/v3/pkg/cli/output" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/repo" +) + +const searchRepoDesc = ` +Search reads through all of the repositories configured on the system, and +looks for matches. Search of these repositories uses the metadata stored on +the system. + +It will display the latest stable versions of the charts found. If you +specify the --devel flag, the output will include pre-release versions. +If you want to search using a version constraint, use --version. + +Examples: + + # Search for stable release versions matching the keyword "nginx" + $ helm search repo nginx + + # Search for release versions matching the keyword "nginx", including pre-release versions + $ helm search repo nginx --devel + + # Search for the latest stable release for nginx-ingress with a major version of 1 + $ helm search repo nginx-ingress --version ^1.0.0 + +Repositories are managed with 'helm repo' commands. +` + +// searchMaxScore suggests that any score higher than this is not considered a match. +const searchMaxScore = 25 + +type searchRepoOptions struct { + versions bool + regexp bool + devel bool + version string + maxColWidth uint + repoFile string + repoCacheDir string + outputFormat output.Format +} + +func newSearchRepoCmd(out io.Writer) *cobra.Command { + o := &searchRepoOptions{} + + cmd := &cobra.Command{ + Use: "repo [keyword]", + Short: "search repositories for a keyword in charts", + Long: searchRepoDesc, + RunE: func(cmd *cobra.Command, args []string) error { + o.repoFile = settings.RepositoryConfig + o.repoCacheDir = settings.RepositoryCache + return o.run(out, args) + }, + } + + f := cmd.Flags() + f.BoolVarP(&o.regexp, "regexp", "r", false, "use regular expressions for searching repositories you have added") + f.BoolVarP(&o.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line, for repositories you have added") + f.BoolVar(&o.devel, "devel", false, "use development versions (alpha, beta, and release candidate releases), too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") + f.StringVar(&o.version, "version", "", "search using semantic versioning constraints on repositories you have added") + f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table") + bindOutputFlag(cmd, &o.outputFormat) + + return cmd +} + +func (o *searchRepoOptions) run(out io.Writer, args []string) error { + o.setupSearchedVersion() + + index, err := o.buildIndex() + if err != nil { + return err + } + + var res []*search.Result + if len(args) == 0 { + res = index.All() + } else { + q := strings.Join(args, " ") + res, err = index.Search(q, searchMaxScore, o.regexp) + if err != nil { + return err + } + } + + search.SortScore(res) + data, err := o.applyConstraint(res) + if err != nil { + return err + } + + return o.outputFormat.Write(out, &repoSearchWriter{data, o.maxColWidth}) +} + +func (o *searchRepoOptions) setupSearchedVersion() { + debug("Original chart version: %q", o.version) + + if o.version != "" { + return + } + + if o.devel { // search for releases and prereleases (alpha, beta, and release candidate releases). + debug("setting version to >0.0.0-0") + o.version = ">0.0.0-0" + } else { // search only for stable releases, prerelease versions will be skip + debug("setting version to >0.0.0") + o.version = ">0.0.0" + } +} + +func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Result, error) { + if o.version == "" { + return res, nil + } + + constraint, err := semver.NewConstraint(o.version) + if err != nil { + return res, errors.Wrap(err, "an invalid version/constraint format") + } + + data := res[:0] + foundNames := map[string]bool{} + for _, r := range res { + // if not returning all versions and already have found a result, + // you're done! + if !o.versions && foundNames[r.Name] { + continue + } + v, err := semver.NewVersion(r.Chart.Version) + if err != nil { + continue + } + if constraint.Check(v) { + data = append(data, r) + foundNames[r.Name] = true + } + } + + return data, nil +} + +func (o *searchRepoOptions) buildIndex() (*search.Index, error) { + // Load the repositories.yaml + rf, err := repo.LoadFile(o.repoFile) + if isNotExist(err) || len(rf.Repositories) == 0 { + return nil, errors.New("no repositories configured") + } + + i := search.NewIndex() + for _, re := range rf.Repositories { + n := re.Name + f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n)) + ind, err := repo.LoadIndexFile(f) + if err != nil { + warning("Repo %q is corrupt or missing. Try 'helm repo update'.", n) + warning("%s", err) + continue + } + + i.AddRepo(n, ind, o.versions || len(o.version) > 0) + } + return i, nil +} + +type repoChartElement struct { + Name string `json:"name"` + Version string `json:"version"` + AppVersion string `json:"app_version"` + Description string `json:"description"` +} + +type repoSearchWriter struct { + results []*search.Result + columnWidth uint +} + +func (r *repoSearchWriter) WriteTable(out io.Writer) error { + if len(r.results) == 0 { + _, err := out.Write([]byte("No results found\n")) + if err != nil { + return fmt.Errorf("unable to write results: %s", err) + } + return nil + } + table := uitable.New() + table.MaxColWidth = r.columnWidth + table.AddRow("NAME", "CHART VERSION", "APP VERSION", "DESCRIPTION") + for _, r := range r.results { + table.AddRow(r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description) + } + return output.EncodeTable(out, table) +} + +func (r *repoSearchWriter) WriteJSON(out io.Writer) error { + return r.encodeByFormat(out, output.JSON) +} + +func (r *repoSearchWriter) WriteYAML(out io.Writer) error { + return r.encodeByFormat(out, output.YAML) +} + +func (r *repoSearchWriter) encodeByFormat(out io.Writer, format output.Format) error { + // Initialize the array so no results returns an empty array instead of null + chartList := make([]repoChartElement, 0, len(r.results)) + + for _, r := range r.results { + chartList = append(chartList, repoChartElement{r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description}) + } + + switch format { + case output.JSON: + return output.EncodeJSON(out, chartList) + case output.YAML: + return output.EncodeYAML(out, chartList) + } + + // Because this is a non-exported function and only called internally by + // WriteJSON and WriteYAML, we shouldn't get invalid types + return nil +} + +// Provides the list of charts that are part of the specified repo, and that starts with 'prefix'. +func compListChartsOfRepo(repoName string, prefix string) []string { + var charts []string + + path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName)) + content, err := ioutil.ReadFile(path) + if err == nil { + scanner := bufio.NewScanner(bytes.NewReader(content)) + for scanner.Scan() { + fullName := fmt.Sprintf("%s/%s", repoName, scanner.Text()) + if strings.HasPrefix(fullName, prefix) { + charts = append(charts, fullName) + } + } + return charts + } + + if isNotExist(err) { + // If there is no cached charts file, fallback to the full index file. + // This is much slower but can happen after the caching feature is first + // installed but before the user does a 'helm repo update' to generate the + // first cached charts file. + path = filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName)) + if indexFile, err := repo.LoadIndexFile(path); err == nil { + for name := range indexFile.Entries { + fullName := fmt.Sprintf("%s/%s", repoName, name) + if strings.HasPrefix(fullName, prefix) { + charts = append(charts, fullName) + } + } + return charts + } + } + + return []string{} +} + +// Provide dynamic auto-completion for commands that operate on charts (e.g., helm show) +// When true, the includeFiles argument indicates that completion should include local files (e.g., local charts) +func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.ShellCompDirective) { + cobra.CompDebugln(fmt.Sprintf("compListCharts with toComplete %s", toComplete), settings.Debug) + + noSpace := false + noFile := false + var completions []string + + // First check completions for repos + repos := compListRepos("", nil) + for _, repo := range repos { + repoWithSlash := fmt.Sprintf("%s/", repo) + if strings.HasPrefix(toComplete, repoWithSlash) { + // Must complete with charts within the specified repo + completions = append(completions, compListChartsOfRepo(repo, toComplete)...) + noSpace = false + break + } else if strings.HasPrefix(repo, toComplete) { + // Must complete the repo name + completions = append(completions, repoWithSlash) + noSpace = true + } + } + cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug) + + // Now handle completions for url prefixes + for _, url := range []string{"https://", "http://", "file://"} { + if strings.HasPrefix(toComplete, url) { + // The user already put in the full url prefix; we don't have + // anything to add, but make sure the shell does not default + // to file completion since we could be returning an empty array. + noFile = true + noSpace = true + } else if strings.HasPrefix(url, toComplete) { + // We are completing a url prefix + completions = append(completions, url) + noSpace = true + } + } + cobra.CompDebugln(fmt.Sprintf("Completions after urls: %v", completions), settings.Debug) + + // Finally, provide file completion if we need to. + // We only do this if: + // 1- There are other completions found (if there are no completions, + // the shell will do file completion itself) + // 2- If there is some input from the user (or else we will end up + // listing the entire content of the current directory which will + // be too many choices for the user to find the real repos) + if includeFiles && len(completions) > 0 && len(toComplete) > 0 { + if files, err := ioutil.ReadDir("."); err == nil { + for _, file := range files { + if strings.HasPrefix(file.Name(), toComplete) { + // We are completing a file prefix + completions = append(completions, file.Name()) + } + } + } + } + cobra.CompDebugln(fmt.Sprintf("Completions after files: %v", completions), settings.Debug) + + // If the user didn't provide any input to completion, + // we provide a hint that a path can also be used + if includeFiles && len(toComplete) == 0 { + completions = append(completions, "./", "/") + } + cobra.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions), settings.Debug) + + directive := cobra.ShellCompDirectiveDefault + if noFile { + directive = directive | cobra.ShellCompDirectiveNoFileComp + } + if noSpace { + directive = directive | cobra.ShellCompDirectiveNoSpace + } + if !includeFiles { + // If we should not include files in the completions, + // we should disable file completion + directive = directive | cobra.ShellCompDirectiveNoFileComp + } + return completions, directive +} diff --git a/cmd/helm/search_repo_test.go b/cmd/helm/search_repo_test.go new file mode 100644 index 00000000000..58ba3a7151b --- /dev/null +++ b/cmd/helm/search_repo_test.go @@ -0,0 +1,93 @@ +/* +Copyright The Helm Authors. + +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" +) + +func TestSearchRepositoriesCmd(t *testing.T) { + repoFile := "testdata/helmhome/helm/repositories.yaml" + repoCache := "testdata/helmhome/helm/repository" + + tests := []cmdTestCase{{ + name: "search for 'alpine', expect one match with latest stable version", + cmd: "search repo alpine", + golden: "output/search-multiple-stable-release.txt", + }, { + name: "search for 'alpine', expect one match with newest development version", + cmd: "search repo alpine --devel", + golden: "output/search-multiple-devel-release.txt", + }, { + name: "search for 'alpine' with versions, expect three matches", + cmd: "search repo alpine --versions", + golden: "output/search-multiple-versions.txt", + }, { + name: "search for 'alpine' with version constraint, expect one match with version 0.1.0", + cmd: "search repo alpine --version '>= 0.1, < 0.2'", + golden: "output/search-constraint.txt", + }, { + name: "search for 'alpine' with version constraint, expect one match with version 0.1.0", + cmd: "search repo alpine --versions --version '>= 0.1, < 0.2'", + golden: "output/search-versions-constraint.txt", + }, { + name: "search for 'alpine' with version constraint, expect one match with version 0.2.0", + cmd: "search repo alpine --version '>= 0.1'", + golden: "output/search-constraint-single.txt", + }, { + name: "search for 'alpine' with version constraint and --versions, expect two matches", + cmd: "search repo alpine --versions --version '>= 0.1'", + golden: "output/search-multiple-versions-constraints.txt", + }, { + name: "search for 'syzygy', expect no matches", + cmd: "search repo syzygy", + golden: "output/search-not-found.txt", + }, { + name: "search for 'alp[a-z]+', expect two matches", + cmd: "search repo alp[a-z]+ --regexp", + golden: "output/search-regex.txt", + }, { + name: "search for 'alp[', expect failure to compile regexp", + cmd: "search repo alp[ --regexp", + wantError: true, + }, { + name: "search for 'maria', expect valid json output", + cmd: "search repo maria --output json", + golden: "output/search-output-json.txt", + }, { + name: "search for 'alpine', expect valid yaml output", + cmd: "search repo alpine --output yaml", + golden: "output/search-output-yaml.txt", + }} + + settings.Debug = true + defer func() { settings.Debug = false }() + + for i := range tests { + tests[i].cmd += " --repository-config " + repoFile + tests[i].cmd += " --repository-cache " + repoCache + } + runTestCmd(t, tests) +} + +func TestSearchRepoOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "search repo") +} + +func TestSearchRepoFileCompletion(t *testing.T) { + checkFileCompletion(t, "search repo", true) // File completion may be useful when inputting a keyword +} diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go index 734f752f5ff..6cf845b06c3 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,82 +16,8 @@ limitations under the License. package main -import ( - "io" - "testing" +import "testing" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" -) - -func TestSearchCmd(t *testing.T) { - tests := []releaseCase{ - { - name: "search for 'maria', expect one match", - args: []string{"maria"}, - expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/mariadb\t0.3.0 \t \tChart for MariaDB", - }, - { - name: "search for 'alpine', expect two matches", - args: []string{"alpine"}, - expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \t2.3.4 \tDeploy a basic Alpine Linux pod", - }, - { - name: "search for 'alpine' with versions, expect three matches", - args: []string{"alpine"}, - flags: []string{"--versions"}, - expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \t2.3.4 \tDeploy a basic Alpine Linux pod\ntesting/alpine\t0.1.0 \t1.2.3 \tDeploy a basic Alpine Linux pod", - }, - { - name: "search for 'alpine' with version constraint, expect one match with version 0.1.0", - args: []string{"alpine"}, - flags: []string{"--version", ">= 0.1, < 0.2"}, - expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \t1.2.3 \tDeploy a basic Alpine Linux pod", - }, - { - name: "search for 'alpine' with version constraint, expect one match with version 0.1.0", - args: []string{"alpine"}, - flags: []string{"--versions", "--version", ">= 0.1, < 0.2"}, - expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \t1.2.3 \tDeploy a basic Alpine Linux pod", - }, - { - name: "search for 'alpine' with version constraint, expect one match with version 0.2.0", - args: []string{"alpine"}, - flags: []string{"--version", ">= 0.1"}, - expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \t2.3.4 \tDeploy a basic Alpine Linux pod", - }, - { - name: "search for 'alpine' with version constraint and --versions, expect two matches", - args: []string{"alpine"}, - flags: []string{"--versions", "--version", ">= 0.1"}, - expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \t2.3.4 \tDeploy a basic Alpine Linux pod\ntesting/alpine\t0.1.0 \t1.2.3 \tDeploy a basic Alpine Linux pod", - }, - { - name: "search for 'syzygy', expect no matches", - args: []string{"syzygy"}, - expected: "No results found", - }, - { - name: "search for 'alp[a-z]+', expect two matches", - args: []string{"alp[a-z]+"}, - flags: []string{"--regexp"}, - expected: "NAME \tCHART VERSION\tAPP VERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \t2.3.4 \tDeploy a basic Alpine Linux pod", - }, - { - name: "search for 'alp[', expect failure to compile regexp", - args: []string{"alp["}, - flags: []string{"--regexp"}, - err: true, - }, - } - - cleanup := resetEnv() - defer cleanup() - - settings.Home = "testdata/helmhome" - - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newSearchCmd(out) - }) +func TestSearchFileCompletion(t *testing.T) { + checkFileCompletion(t, "search", false) } diff --git a/cmd/helm/serve.go b/cmd/helm/serve.go deleted file mode 100644 index 21ae36da146..00000000000 --- a/cmd/helm/serve.go +++ /dev/null @@ -1,102 +0,0 @@ -/* -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" - "os" - "path/filepath" - - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/repo" -) - -const serveDesc = ` -This command starts a local chart repository server that serves charts from a local directory. - -The new server will provide HTTP access to a repository. By default, it will -scan all of the charts in '$HELM_HOME/repository/local' and serve those over -the local IPv4 TCP port (default '127.0.0.1:8879'). - -This command is intended to be used for educational and testing purposes only. -It is best to rely on a dedicated web server or a cloud-hosted solution like -Google Cloud Storage for production use. - -See https://github.com/kubernetes/helm/blob/master/docs/chart_repository.md#hosting-chart-repositories -for more information on hosting chart repositories in a production setting. -` - -type serveCmd struct { - out io.Writer - url string - address string - repoPath string -} - -func newServeCmd(out io.Writer) *cobra.Command { - srv := &serveCmd{out: out} - cmd := &cobra.Command{ - Use: "serve", - Short: "start a local http web server", - Long: serveDesc, - PreRunE: func(cmd *cobra.Command, args []string) error { - return srv.complete() - }, - RunE: func(cmd *cobra.Command, args []string) error { - return srv.run() - }, - } - - f := cmd.Flags() - f.StringVar(&srv.repoPath, "repo-path", "", "local directory path from which to serve charts") - f.StringVar(&srv.address, "address", "127.0.0.1:8879", "address to listen on") - f.StringVar(&srv.url, "url", "", "external URL of chart repository") - - return cmd -} - -func (s *serveCmd) complete() error { - if s.repoPath == "" { - s.repoPath = settings.Home.LocalRepository() - } - return nil -} - -func (s *serveCmd) run() error { - repoPath, err := filepath.Abs(s.repoPath) - if err != nil { - return err - } - if _, err := os.Stat(repoPath); os.IsNotExist(err) { - return err - } - - fmt.Fprintln(s.out, "Regenerating index. This may take a moment.") - if len(s.url) > 0 { - err = index(repoPath, s.url, "") - } else { - err = index(repoPath, "http://"+s.address, "") - } - if err != nil { - return err - } - - fmt.Fprintf(s.out, "Now serving you on %s\n", s.address) - return repo.StartLocalRepo(repoPath, s.address) -} diff --git a/cmd/helm/show.go b/cmd/helm/show.go new file mode 100644 index 00000000000..888d2d3f3ec --- /dev/null +++ b/cmd/helm/show.go @@ -0,0 +1,184 @@ +/* +Copyright The Helm Authors. + +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" + "log" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +const showDesc = ` +This command consists of multiple subcommands to display information about a chart +` + +const showAllDesc = ` +This command inspects a chart (directory, file, or URL) and displays all its content +(values.yaml, Charts.yaml, README) +` + +const showValuesDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the values.yaml file +` + +const showChartDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the Charts.yaml file +` + +const readmeChartDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the README file +` + +func newShowCmd(out io.Writer) *cobra.Command { + client := action.NewShow(action.ShowAll) + + showCommand := &cobra.Command{ + Use: "show", + Short: "show information of a chart", + Aliases: []string{"inspect"}, + Long: showDesc, + Args: require.NoArgs, + ValidArgsFunction: noCompletions, // Disable file completion + } + + // Function providing dynamic auto-completion + validArgsFunc := func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compListCharts(toComplete, true) + } + + all := &cobra.Command{ + Use: "all [CHART]", + Short: "show all information of the chart", + Long: showAllDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: validArgsFunc, + RunE: func(cmd *cobra.Command, args []string) error { + client.OutputFormat = action.ShowAll + output, err := runShow(args, client) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + valuesSubCmd := &cobra.Command{ + Use: "values [CHART]", + Short: "show the chart's values", + Long: showValuesDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: validArgsFunc, + RunE: func(cmd *cobra.Command, args []string) error { + client.OutputFormat = action.ShowValues + output, err := runShow(args, client) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + chartSubCmd := &cobra.Command{ + Use: "chart [CHART]", + Short: "show the chart's definition", + Long: showChartDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: validArgsFunc, + RunE: func(cmd *cobra.Command, args []string) error { + client.OutputFormat = action.ShowChart + output, err := runShow(args, client) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + readmeSubCmd := &cobra.Command{ + Use: "readme [CHART]", + Short: "show the chart's README", + Long: readmeChartDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: validArgsFunc, + RunE: func(cmd *cobra.Command, args []string) error { + client.OutputFormat = action.ShowReadme + output, err := runShow(args, client) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd} + for _, subCmd := range cmds { + addShowFlags(subCmd, client) + showCommand.AddCommand(subCmd) + } + + return showCommand +} + +func addShowFlags(subCmd *cobra.Command, client *action.Show) { + f := subCmd.Flags() + + f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") + if subCmd.Name() == "values" { + f.StringVar(&client.JSONPathTemplate, "jsonpath", "", "supply a JSONPath expression to filter the output") + } + addChartPathOptionsFlags(f, &client.ChartPathOptions) + + err := subCmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 1 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compVersionFlag(args[0], toComplete) + }) + + if err != nil { + log.Fatal(err) + } +} + +func runShow(args []string, client *action.Show) (string, error) { + debug("Original chart version: %q", client.Version) + if client.Version == "" && client.Devel { + debug("setting version to >0.0.0-0") + client.Version = ">0.0.0-0" + } + + cp, err := client.ChartPathOptions.LocateChart(args[0], settings) + if err != nil { + return "", err + } + return client.Run(cp) +} diff --git a/cmd/helm/show_test.go b/cmd/helm/show_test.go new file mode 100644 index 00000000000..9781a3de44b --- /dev/null +++ b/cmd/helm/show_test.go @@ -0,0 +1,147 @@ +/* +Copyright The Helm Authors. + +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" + "path/filepath" + "strings" + "testing" + + "helm.sh/helm/v3/pkg/repo/repotest" +) + +func TestShowPreReleaseChart(t *testing.T) { + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() + + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + args string + flags string + fail bool + expectedErr string + }{ + { + name: "show pre-release chart", + args: "test/pre-release-chart", + fail: true, + expectedErr: "failed to download \"test/pre-release-chart\"", + }, + { + name: "show pre-release chart", + args: "test/pre-release-chart", + fail: true, + flags: "--version 1.0.0", + expectedErr: "failed to download \"test/pre-release-chart\" at version \"1.0.0\"", + }, + { + name: "show pre-release chart with 'devel' flag", + args: "test/pre-release-chart", + flags: "--devel", + fail: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outdir := srv.Root() + cmd := fmt.Sprintf("show all '%s' %s --repository-config %s --repository-cache %s", + tt.args, + tt.flags, + filepath.Join(outdir, "repositories.yaml"), + outdir, + ) + //_, out, err := executeActionCommand(cmd) + _, _, err := executeActionCommand(cmd) + if err != nil { + if tt.fail { + if !strings.Contains(err.Error(), tt.expectedErr) { + t.Errorf("%q expected error: %s, got: %s", tt.name, tt.expectedErr, err.Error()) + } + return + } + t.Errorf("%q reported error: %s", tt.name, err) + } + }) + } +} + +func TestShowVersionCompletion(t *testing.T) { + repoFile := "testdata/helmhome/helm/repositories.yaml" + repoCache := "testdata/helmhome/helm/repository" + + repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache) + + tests := []cmdTestCase{{ + name: "completion for show version flag", + cmd: fmt.Sprintf("%s __complete show chart testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for show version flag too few args", + cmd: fmt.Sprintf("%s __complete show chart --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for show version flag too many args", + cmd: fmt.Sprintf("%s __complete show chart testing/alpine badarg --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for show version flag invalid chart", + cmd: fmt.Sprintf("%s __complete show chart invalid/invalid --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for show version flag with all", + cmd: fmt.Sprintf("%s __complete show all testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for show version flag with readme", + cmd: fmt.Sprintf("%s __complete show readme testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for show version flag with values", + cmd: fmt.Sprintf("%s __complete show values testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }} + runTestCmd(t, tests) +} + +func TestShowFileCompletion(t *testing.T) { + checkFileCompletion(t, "show", false) +} + +func TestShowAllFileCompletion(t *testing.T) { + checkFileCompletion(t, "show all", true) +} + +func TestShowChartFileCompletion(t *testing.T) { + checkFileCompletion(t, "show chart", true) +} + +func TestShowReadmeFileCompletion(t *testing.T) { + checkFileCompletion(t, "show readme", true) +} + +func TestShowValuesFileCompletion(t *testing.T) { + checkFileCompletion(t, "show values", true) +} diff --git a/cmd/helm/status.go b/cmd/helm/status.go index b73b6f56e38..6085251d57b 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,141 +17,178 @@ limitations under the License. package main import ( - "encoding/json" "fmt" "io" - "regexp" - "text/tabwriter" + "log" + "strings" + "time" - "github.com/ghodss/yaml" - "github.com/gosuri/uitable" - "github.com/gosuri/uitable/util/strutil" "github.com/spf13/cobra" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/timeconv" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli/output" + "helm.sh/helm/v3/pkg/release" ) +// NOTE: Keep the list of statuses up-to-date with pkg/release/status.go. var statusHelp = ` This command shows the status of a named release. The status consists of: - last deployment time - k8s namespace in which the release lives -- state of the release (can be: UNKNOWN, DEPLOYED, DELETED, SUPERSEDED, FAILED or DELETING) +- state of the release (can be: unknown, deployed, uninstalled, superseded, failed, uninstalling, pending-install, pending-upgrade or pending-rollback) +- revision of the release +- description of the release (can be completion message or error message, need to enable --show-desc) - list of resources that this release consists of, sorted by kind - details on last test suite run, if applicable - additional notes provided by the chart ` -type statusCmd struct { - release string - out io.Writer - client helm.Interface - version int32 - outfmt string -} - -func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { - status := &statusCmd{ - out: out, - client: client, - } +func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewStatus(cfg) + var outfmt output.Format cmd := &cobra.Command{ - Use: "status [flags] RELEASE_NAME", - Short: "displays the status of the named release", - Long: statusHelp, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errReleaseRequired + Use: "status RELEASE_NAME", + Short: "display the status of the named release", + Long: statusHelp, + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp } - status.release = args[0] - if status.client == nil { - status.client = newClient() + return compListReleases(toComplete, args, cfg) + }, + RunE: func(cmd *cobra.Command, args []string) error { + rel, err := client.Run(args[0]) + if err != nil { + return err } - return status.run() + + // strip chart metadata from the output + rel.Chart = nil + + return outfmt.Write(out, &statusPrinter{rel, false, client.ShowDescription}) }, } - cmd.PersistentFlags().Int32Var(&status.version, "revision", 0, "if set, display the status of the named release with revision") - cmd.PersistentFlags().StringVarP(&status.outfmt, "output", "o", "", "output the status in the specified format (json or yaml)") + f := cmd.Flags() - return cmd -} + f.IntVar(&client.Version, "revision", 0, "if set, display the status of the named release with revision") + + err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 1 { + return compListRevisions(toComplete, cfg, args[0]) + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) -func (s *statusCmd) run() error { - res, err := s.client.ReleaseStatus(s.release, helm.StatusReleaseVersion(s.version)) if err != nil { - return prettyError(err) + log.Fatal(err) } - switch s.outfmt { - case "": - PrintStatus(s.out, res) + bindOutputFlag(cmd, &outfmt) + f.BoolVar(&client.ShowDescription, "show-desc", false, "if set, display the description message of the named release") + + return cmd +} + +type statusPrinter struct { + release *release.Release + debug bool + showDescription bool +} + +func (s statusPrinter) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, s.release) +} + +func (s statusPrinter) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, s.release) +} + +func (s statusPrinter) WriteTable(out io.Writer) error { + if s.release == nil { return nil - case "json": - data, err := json.Marshal(res) - if err != nil { - return fmt.Errorf("Failed to Marshal JSON output: %s", err) + } + fmt.Fprintf(out, "NAME: %s\n", s.release.Name) + if !s.release.Info.LastDeployed.IsZero() { + fmt.Fprintf(out, "LAST DEPLOYED: %s\n", s.release.Info.LastDeployed.Format(time.ANSIC)) + } + fmt.Fprintf(out, "NAMESPACE: %s\n", s.release.Namespace) + fmt.Fprintf(out, "STATUS: %s\n", s.release.Info.Status.String()) + fmt.Fprintf(out, "REVISION: %d\n", s.release.Version) + if s.showDescription { + fmt.Fprintf(out, "DESCRIPTION: %s\n", s.release.Info.Description) + } + + executions := executionsByHookEvent(s.release) + if tests, ok := executions[release.HookTest]; !ok || len(tests) == 0 { + fmt.Fprintln(out, "TEST SUITE: None") + } else { + for _, h := range tests { + // Don't print anything if hook has not been initiated + if h.LastRun.StartedAt.IsZero() { + continue + } + fmt.Fprintf(out, "TEST SUITE: %s\n%s\n%s\n%s\n", + h.Name, + fmt.Sprintf("Last Started: %s", h.LastRun.StartedAt.Format(time.ANSIC)), + fmt.Sprintf("Last Completed: %s", h.LastRun.CompletedAt.Format(time.ANSIC)), + fmt.Sprintf("Phase: %s", h.LastRun.Phase), + ) } - s.out.Write(data) - return nil - case "yaml": - data, err := yaml.Marshal(res) + } + + if s.debug { + fmt.Fprintln(out, "USER-SUPPLIED VALUES:") + err := output.EncodeYAML(out, s.release.Config) if err != nil { - return fmt.Errorf("Failed to Marshal YAML output: %s", err) + return err } - s.out.Write(data) - return nil - } + // Print an extra newline + fmt.Fprintln(out) - return fmt.Errorf("Unknown output format %q", s.outfmt) -} + cfg, err := chartutil.CoalesceValues(s.release.Chart, s.release.Config) + if err != nil { + return err + } -// PrintStatus prints out the status of a release. Shared because also used by -// install / upgrade -func PrintStatus(out io.Writer, res *services.GetReleaseStatusResponse) { - if res.Info.LastDeployed != nil { - fmt.Fprintf(out, "LAST DEPLOYED: %s\n", timeconv.String(res.Info.LastDeployed)) - } - fmt.Fprintf(out, "NAMESPACE: %s\n", res.Namespace) - fmt.Fprintf(out, "STATUS: %s\n", res.Info.Status.Code) - fmt.Fprintf(out, "\n") - if len(res.Info.Status.Resources) > 0 { - re := regexp.MustCompile(" +") - - w := tabwriter.NewWriter(out, 0, 0, 2, ' ', tabwriter.TabIndent) - fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(res.Info.Status.Resources, "\t")) - w.Flush() + fmt.Fprintln(out, "COMPUTED VALUES:") + err = output.EncodeYAML(out, cfg.AsMap()) + if err != nil { + return err + } + // Print an extra newline + fmt.Fprintln(out) } - if res.Info.Status.LastTestSuiteRun != nil { - lastRun := res.Info.Status.LastTestSuiteRun - fmt.Fprintf(out, "TEST SUITE:\n%s\n%s\n\n%s\n", - fmt.Sprintf("Last Started: %s", timeconv.String(lastRun.StartedAt)), - fmt.Sprintf("Last Completed: %s", timeconv.String(lastRun.CompletedAt)), - formatTestResults(lastRun.Results)) + + if strings.EqualFold(s.release.Info.Description, "Dry run complete") || s.debug { + fmt.Fprintln(out, "HOOKS:") + for _, h := range s.release.Hooks { + fmt.Fprintf(out, "---\n# Source: %s\n%s\n", h.Path, h.Manifest) + } + fmt.Fprintf(out, "MANIFEST:\n%s\n", s.release.Manifest) } - if len(res.Info.Status.Notes) > 0 { - fmt.Fprintf(out, "NOTES:\n%s\n", res.Info.Status.Notes) + if len(s.release.Info.Notes) > 0 { + fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes)) } + return nil } -func formatTestResults(results []*release.TestRun) string { - tbl := uitable.New() - tbl.MaxColWidth = 50 - tbl.AddRow("TEST", "STATUS", "INFO", "STARTED", "COMPLETED") - for i := 0; i < len(results); i++ { - r := results[i] - n := r.Name - s := strutil.PadRight(r.Status.String(), 10, ' ') - i := r.Info - ts := timeconv.String(r.StartedAt) - tc := timeconv.String(r.CompletedAt) - tbl.AddRow(n, s, i, ts, tc) +func executionsByHookEvent(rel *release.Release) map[release.HookEvent][]*release.Hook { + result := make(map[release.HookEvent][]*release.Hook) + for _, h := range rel.Hooks { + for _, e := range h.Events { + executions, ok := result[e] + if !ok { + executions = []*release.Hook{} + } + result[e] = append(executions, h) + } } - return tbl.String() + return result } diff --git a/cmd/helm/status_test.go b/cmd/helm/status_test.go index 616b027f392..7f305d56bae 100644 --- a/cmd/helm/status_test.go +++ b/cmd/helm/status_test.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,135 +17,186 @@ limitations under the License. package main import ( - "fmt" - "io" "testing" + "time" - "github.com/golang/protobuf/ptypes/timestamp" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/timeconv" -) - -var ( - date = timestamp.Timestamp{Seconds: 242085845, Nanos: 0} - dateString = timeconv.String(&date) + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" ) func TestStatusCmd(t *testing.T) { - tests := []releaseCase{ - { - name: "get status of a deployed release", - args: []string{"flummoxed-chickadee"}, - expected: outputWithStatus("DEPLOYED\n\n"), - rels: []*release.Release{ - releaseMockWithStatus(&release.Status{ - Code: release.Status_DEPLOYED, - }), + releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { + info.LastDeployed = helmtime.Unix(1452902400, 0).UTC() + return []*release.Release{{ + Name: "flummoxed-chickadee", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }} + } + + tests := []cmdTestCase{{ + name: "get status of a deployed release", + cmd: "status flummoxed-chickadee", + golden: "output/status.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + }), + }, { + name: "get status of a deployed release, with desc", + cmd: "status --show-desc flummoxed-chickadee", + golden: "output/status-with-desc.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + Description: "Mock description", + }), + }, { + name: "get status of a deployed release with notes", + cmd: "status flummoxed-chickadee", + golden: "output/status-with-notes.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + Notes: "release notes", + }), + }, { + name: "get status of a deployed release with notes in json", + cmd: "status flummoxed-chickadee -o json", + golden: "output/status.json", + rels: releasesMockWithStatus(&release.Info{ + Status: release.StatusDeployed, + Notes: "release notes", + }), + }, { + name: "get status of a deployed release with test suite", + cmd: "status flummoxed-chickadee", + golden: "output/status-with-test-suite.txt", + rels: releasesMockWithStatus( + &release.Info{ + Status: release.StatusDeployed, }, - }, - { - name: "get status of a deployed release with notes", - args: []string{"flummoxed-chickadee"}, - expected: outputWithStatus("DEPLOYED\n\nNOTES:\nrelease notes\n"), - rels: []*release.Release{ - releaseMockWithStatus(&release.Status{ - Code: release.Status_DEPLOYED, - Notes: "release notes", - }), + &release.Hook{ + Name: "never-run-test", + Events: []release.HookEvent{release.HookTest}, }, - }, - { - name: "get status of a deployed release with notes in json", - args: []string{"flummoxed-chickadee"}, - flags: []string{"-o", "json"}, - expected: `{"name":"flummoxed-chickadee","info":{"status":{"code":1,"notes":"release notes"},"first_deployed":{"seconds":242085845},"last_deployed":{"seconds":242085845}}}`, - rels: []*release.Release{ - releaseMockWithStatus(&release.Status{ - Code: release.Status_DEPLOYED, - Notes: "release notes", - }), + &release.Hook{ + Name: "passing-test", + Events: []release.HookEvent{release.HookTest}, + LastRun: release.HookExecution{ + StartedAt: mustParseTime("2006-01-02T15:04:05Z"), + CompletedAt: mustParseTime("2006-01-02T15:04:07Z"), + Phase: release.HookPhaseSucceeded, + }, }, - }, - { - name: "get status of a deployed release with resources", - args: []string{"flummoxed-chickadee"}, - expected: outputWithStatus("DEPLOYED\n\nRESOURCES:\nresource A\nresource B\n\n"), - rels: []*release.Release{ - releaseMockWithStatus(&release.Status{ - Code: release.Status_DEPLOYED, - Resources: "resource A\nresource B\n", - }), + &release.Hook{ + Name: "failing-test", + Events: []release.HookEvent{release.HookTest}, + LastRun: release.HookExecution{ + StartedAt: mustParseTime("2006-01-02T15:10:05Z"), + CompletedAt: mustParseTime("2006-01-02T15:10:07Z"), + Phase: release.HookPhaseFailed, + }, }, - }, - { - name: "get status of a deployed release with resources in YAML", - args: []string{"flummoxed-chickadee"}, - flags: []string{"-o", "yaml"}, - expected: "info:\n (.*)first_deployed:\n (.*)seconds: 242085845\n (.*)last_deployed:\n (.*)seconds: 242085845\n (.*)status:\n code: 1\n (.*)resources: |\n (.*)resource A\n (.*)resource B\nname: flummoxed-chickadee\n", - rels: []*release.Release{ - releaseMockWithStatus(&release.Status{ - Code: release.Status_DEPLOYED, - Resources: "resource A\nresource B\n", - }), + &release.Hook{ + Name: "passing-pre-install", + Events: []release.HookEvent{release.HookPreInstall}, + LastRun: release.HookExecution{ + StartedAt: mustParseTime("2006-01-02T15:00:05Z"), + CompletedAt: mustParseTime("2006-01-02T15:00:07Z"), + Phase: release.HookPhaseSucceeded, + }, }, - }, + ), + }} + runTestCmd(t, tests) +} + +func mustParseTime(t string) helmtime.Time { + res, _ := helmtime.Parse(time.RFC3339, t) + return res +} + +func TestStatusCompletion(t *testing.T) { + rels := []*release.Release{ { - name: "get status of a deployed release with test suite", - args: []string{"flummoxed-chickadee"}, - expected: outputWithStatus( - fmt.Sprintf("DEPLOYED\n\nTEST SUITE:\nLast Started: %s\nLast Completed: %s\n\n", dateString, dateString) + - "TEST \tSTATUS (.*)\tINFO (.*)\tSTARTED (.*)\tCOMPLETED (.*)\n" + - fmt.Sprintf("test run 1\tSUCCESS (.*)\textra info\t%s\t%s\n", dateString, dateString) + - fmt.Sprintf("test run 2\tFAILURE (.*)\t (.*)\t%s\t%s\n", dateString, dateString)), - rels: []*release.Release{ - releaseMockWithStatus(&release.Status{ - Code: release.Status_DEPLOYED, - LastTestSuiteRun: &release.TestSuite{ - StartedAt: &date, - CompletedAt: &date, - Results: []*release.TestRun{ - { - Name: "test run 1", - Status: release.TestRun_SUCCESS, - Info: "extra info", - StartedAt: &date, - CompletedAt: &date, - }, - { - Name: "test run 2", - Status: release.TestRun_FAILURE, - StartedAt: &date, - CompletedAt: &date, - }, - }, - }, - }), + Name: "athos", + Namespace: "default", + Info: &release.Info{ + Status: release.StatusDeployed, }, - }, - } + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "Athos-chart", + Version: "1.2.3", + }, + }, + }, { + Name: "porthos", + Namespace: "default", + Info: &release.Info{ + Status: release.StatusFailed, + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "Porthos-chart", + Version: "111.222.333", + }, + }, + }, { + Name: "aramis", + Namespace: "default", + Info: &release.Info{ + Status: release.StatusUninstalled, + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "Aramis-chart", + Version: "0.0.0", + }, + }, + }, { + Name: "dartagnan", + Namespace: "gascony", + Info: &release.Info{ + Status: release.StatusUnknown, + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "Dartagnan-chart", + Version: "1.2.3-prerelease", + }, + }, + }} - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newStatusCmd(c, out) - }) + tests := []cmdTestCase{{ + name: "completion for status", + cmd: "__complete status a", + golden: "output/status-comp.txt", + rels: rels, + }, { + name: "completion for status with too many arguments", + cmd: "__complete status dartagnan ''", + golden: "output/status-wrong-args-comp.txt", + rels: rels, + }, { + name: "completion for status with global flag", + cmd: "__complete status --debug a", + golden: "output/status-comp.txt", + rels: rels, + }} + runTestCmd(t, tests) +} +func TestStatusRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "status") } -func outputWithStatus(status string) string { - return fmt.Sprintf("LAST DEPLOYED: %s\nNAMESPACE: \nSTATUS: %s", - dateString, - status) +func TestStatusOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "status") } -func releaseMockWithStatus(status *release.Status) *release.Release { - return &release.Release{ - Name: "flummoxed-chickadee", - Info: &release.Info{ - FirstDeployed: &date, - LastDeployed: &date, - Status: status, - }, - } +func TestStatusFileCompletion(t *testing.T) { + checkFileCompletion(t, "status", false) + checkFileCompletion(t, "status myrelease", false) } diff --git a/cmd/helm/template.go b/cmd/helm/template.go index c04bc2dc8f8..1110771f0c9 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,278 +17,182 @@ limitations under the License. package main import ( - "errors" + "bytes" "fmt" "io" "os" "path" "path/filepath" "regexp" + "sort" "strings" - "time" - "github.com/Masterminds/semver" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/engine" - "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" - tversion "k8s.io/helm/pkg/version" -) + "helm.sh/helm/v3/pkg/release" -const defaultDirectoryPermission = 0755 - -var ( - whitespaceRegex = regexp.MustCompile(`^\s*$`) + "github.com/spf13/cobra" - // defaultKubeVersion is the default value of --kube-version flag - defaultKubeVersion = fmt.Sprintf("%s.%s", chartutil.DefaultKubeVersion.Major, chartutil.DefaultKubeVersion.Minor) + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/releaseutil" ) 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 +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. ` -type templateCmd struct { - namespace string - valueFiles valueFiles - chartPath string - out io.Writer - values []string - stringValues []string - nameTemplate string - showNotes bool - releaseName string - renderFiles []string - kubeVersion string - outputDir string -} - -func newTemplateCmd(out io.Writer) *cobra.Command { - - t := &templateCmd{ - out: out, - } +func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + var validate bool + var includeCrds bool + var skipTests bool + client := action.NewInstall(cfg) + valueOpts := &values.Options{} + var extraAPIs []string + var showFiles []string cmd := &cobra.Command{ - Use: "template [flags] CHART", - Short: fmt.Sprintf("locally render templates"), + Use: "template [NAME] [CHART]", + Short: "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.StringArrayVar(&t.stringValues, "set-string", []string{}, "set STRING 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") - f.StringVar(&t.kubeVersion, "kube-version", defaultKubeVersion, "kubernetes version used as Capabilities.KubeVersion.Major/Minor") - f.StringVar(&t.outputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout") - - 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(filepath.Join(t.chartPath, f)) - if err != nil { - return fmt.Errorf("could not resolve template path: %s", err) + Args: require.MinimumNArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compInstall(args, toComplete, client) + }, + RunE: func(_ *cobra.Command, args []string) error { + client.DryRun = true + client.ReleaseName = "RELEASE-NAME" + client.Replace = true // Skip the name check + client.ClientOnly = !validate + client.APIVersions = chartutil.VersionSet(extraAPIs) + client.IncludeCRDs = includeCrds + rel, err := runInstall(args, client, valueOpts, out) + + if err != nil && !settings.Debug { + if rel != nil { + return fmt.Errorf("%w\n\nUse --debug flag to render out invalid YAML", 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) + return err } - } - } - // verify that output-dir exists if provided - if t.outputDir != "" { - _, err = os.Stat(t.outputDir) - if os.IsNotExist(err) { - return fmt.Errorf("output-dir '%s' does not exist", t.outputDir) - } - } + // We ignore a potential error here because, when the --debug flag was specified, + // we always want to print the YAML, even if it is not valid. The error is still returned afterwards. + if rel != nil { + var manifests bytes.Buffer + fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest)) + if !client.DisableHooks { + fileWritten := make(map[string]bool) + for _, m := range rel.Hooks { + if skipTests && isTestHook(m) { + continue + } + if client.OutputDir == "" { + fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest) + } else { + newDir := client.OutputDir + if client.UseReleaseName { + newDir = filepath.Join(client.OutputDir, client.ReleaseName) + } + err = writeToFile(newDir, m.Path, m.Manifest, fileWritten[m.Path]) + if err != nil { + return err + } + fileWritten[m.Path] = true + } + + } + } - if t.namespace == "" { - t.namespace = defaultNamespace() - } - // get combined values and create config - rawVals, err := vals(t.valueFiles, t.values, t.stringValues) - if err != nil { - return err - } - config := &chart.Config{Raw: string(rawVals), Values: map[string]*chart.Value{}} + // if we have a list of files to render, then check that each of the + // provided files exists in the chart. + if len(showFiles) > 0 { + // This is necessary to ensure consistent manifest ordering when using --show-only + // with globs or directory names. + splitManifests := releaseutil.SplitManifests(manifests.String()) + manifestsKeys := make([]string, 0, len(splitManifests)) + for k := range splitManifests { + manifestsKeys = append(manifestsKeys, k) + } + sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys)) + + manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)") + var manifestsToRender []string + for _, f := range showFiles { + missing := true + // Use linux-style filepath separators to unify user's input path + f = filepath.ToSlash(f) + for _, manifestKey := range manifestsKeys { + manifest := splitManifests[manifestKey] + submatch := manifestNameRegex.FindStringSubmatch(manifest) + if len(submatch) == 0 { + continue + } + manifestName := submatch[1] + // manifest.Name is rendered using linux-style filepath separators on Windows as + // well as macOS/linux. + manifestPathSplit := strings.Split(manifestName, "/") + // manifest.Path is connected using linux-style filepath separators on Windows as + // well as macOS/linux + manifestPath := strings.Join(manifestPathSplit, "/") + + // if the filepath provided matches a manifest path in the + // chart, render that manifest + if matched, _ := filepath.Match(f, manifestPath); !matched { + continue + } + manifestsToRender = append(manifestsToRender, manifest) + missing = false + } + if missing { + return fmt.Errorf("could not find template %s in chart", f) + } + } + for _, m := range manifestsToRender { + fmt.Fprintf(out, "---\n%s\n", m) + } + } else { + fmt.Fprintf(out, "%s", manifests.String()) + } + } - // 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() - - caps := &chartutil.Capabilities{ - APIVersions: chartutil.DefaultVersionSet, - KubeVersion: chartutil.DefaultKubeVersion, - TillerVersion: tversion.GetVersionProto(), - } - - // kubernetes version - kv, err := semver.NewVersion(t.kubeVersion) - if err != nil { - return fmt.Errorf("could not parse a kubernetes version: %v", err) - } - caps.KubeVersion.Major = fmt.Sprint(kv.Major()) - caps.KubeVersion.Minor = fmt.Sprint(kv.Minor()) - caps.KubeVersion.GitVersion = fmt.Sprintf("v%d.%d.0", kv.Major(), kv.Minor()) - - vals, err := chartutil.ToRenderValuesCaps(c, config, options, caps) - 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, string(os.PathSeparator)) - dd := d[1:] - an := filepath.Join(t.chartPath, strings.Join(dd, string(os.PathSeparator))) - - 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) - } + f := cmd.Flags() + addInstallFlags(cmd, f, client, valueOpts) + f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates") + f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout") + f.BoolVar(&validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install") + f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output") + f.BoolVar(&skipTests, "skip-tests", false, "skip tests from templated output") + f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall") + f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions") + f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.") + bindPostRenderFlag(cmd, &client.PostRenderer) - for _, m := range tiller.SortByKind(listManifests) { - if len(t.renderFiles) > 0 && !in(m.Name, rf) { - continue - } - data := m.Content - b := filepath.Base(m.Name) - if !t.showNotes && b == "NOTES.txt" { - continue - } - if strings.HasPrefix(b, "_") { - continue - } + return cmd +} - if t.outputDir != "" { - // blank template after execution - if whitespaceRegex.MatchString(data) { - continue - } - err = writeToFile(t.outputDir, m.Name, data) - if err != nil { - return err - } - continue +func isTestHook(h *release.Hook) bool { + for _, e := range h.Events { + if e == release.HookTest { + return true } - fmt.Printf("---\n# Source: %s\n", m.Name) - fmt.Println(data) } - return nil + return false } -// write the to / -func writeToFile(outputDir string, name string, data string) error { +// The following functions (writeToFile, createOrOpenFile, and ensureDirectoryForFile) +// are copied from the actions package. This is part of a change to correct a +// bug introduced by #8156. As part of the todo to refactor renderResources +// this duplicate code should be removed. It is added here so that the API +// surface area is as minimally impacted as possible in fixing the issue. +func writeToFile(outputDir string, name string, data string, append bool) error { outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator)) err := ensureDirectoryForFile(outfileName) @@ -296,14 +200,14 @@ func writeToFile(outputDir string, name string, data string) error { return err } - f, err := os.Create(outfileName) + f, err := createOrOpenFile(outfileName, append) if err != nil { return err } defer f.Close() - _, err = f.WriteString(fmt.Sprintf("##---\n# Source: %s\n%s", name, data)) + _, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data)) if err != nil { return err @@ -313,7 +217,13 @@ func writeToFile(outputDir string, name string, data string) error { return nil } -// check if the directory exists to create file. creates if don't exists +func createOrOpenFile(filename string, append bool) (*os.File, error) { + if append { + return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600) + } + return os.Create(filename) +} + func ensureDirectoryForFile(file string) error { baseDir := path.Dir(file) _, err := os.Stat(baseDir) @@ -321,5 +231,5 @@ func ensureDirectoryForFile(file string) error { return err } - return os.MkdirAll(baseDir, defaultDirectoryPermission) + return os.MkdirAll(baseDir, 0755) } diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go index eefa46774d0..9e6a0c43414 100644 --- a/cmd/helm/template_test.go +++ b/cmd/helm/template_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,151 +17,152 @@ limitations under the License. package main import ( - "bufio" - "bytes" "fmt" - "io" - "os" "path/filepath" - "strings" "testing" ) -var chartPath = "./../../pkg/chartutil/testdata/subpop/charts/subchart1" +var chartPath = "testdata/testcharts/subchart" 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 - }{ + tests := []cmdTestCase{ + { + name: "check name", + cmd: fmt.Sprintf("template '%s'", chartPath), + golden: "output/template.txt", + }, + { + name: "check set name", + cmd: fmt.Sprintf("template '%s' --set service.name=apache", chartPath), + golden: "output/template-set.txt", + }, + { + name: "check values files", + cmd: fmt.Sprintf("template '%s' --values '%s'", chartPath, filepath.Join(chartPath, "/charts/subchartA/values.yaml")), + golden: "output/template-values-files.txt", + }, + { + name: "check name template", + cmd: fmt.Sprintf(`template '%s' --name-template='foobar-{{ b64enc "abc" }}-baz'`, chartPath), + golden: "output/template-name-template.txt", + }, { - 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 no args", + cmd: "template", + wantError: true, + golden: "output/template-no-args.txt", }, { - 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 library chart", + cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/lib-chart"), + wantError: true, + golden: "output/template-lib-chart.txt", }, { - 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 chart bad type", + cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-bad-type"), + wantError: true, + golden: "output/install-chart-bad-type.txt", }, { - 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 chart with dependency which is an app chart acting as a library chart", + cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-with-template-lib-dep"), + golden: "output/template-chart-with-template-lib-dep.txt", }, { - name: "check_namespace", - desc: "verify --namespace", - args: []string{chartPath, "--namespace", "test"}, - expectKey: "subchart1/templates/service.yaml", - expectValue: "namespace: \"test\"", + name: "check chart with dependency which is an app chart archive acting as a library chart", + cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-with-template-lib-archive-dep"), + golden: "output/template-chart-with-template-lib-archive-dep.txt", }, { - name: "check_release_name", - desc: "verify --release exists", - args: []string{chartPath, "--name", "test"}, - expectKey: "subchart1/templates/service.yaml", - expectValue: "release-name: \"test\"", + name: "check kube api versions", + cmd: fmt.Sprintf("template --api-versions helm.k8s.io/test '%s'", chartPath), + golden: "output/template-with-api-version.txt", }, { - name: "check_notes", - desc: "verify --notes shows notes", - args: []string{chartPath, "--notes", "true"}, - expectKey: "subchart1/templates/NOTES.txt", - expectValue: "Sample notes for subchart1", + name: "template with CRDs", + cmd: fmt.Sprintf("template '%s' --include-crds", chartPath), + golden: "output/template-with-crds.txt", }, { - 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: "template with show-only one", + cmd: fmt.Sprintf("template '%s' --show-only templates/service.yaml", chartPath), + golden: "output/template-show-only-one.txt", }, { - 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\"", + name: "template with show-only multiple", + cmd: fmt.Sprintf("template '%s' --show-only templates/service.yaml --show-only charts/subcharta/templates/service.yaml", chartPath), + golden: "output/template-show-only-multiple.txt", }, { - name: "check_kube_version", - desc: "verify --kube-version overrides the kubernetes version", - args: []string{chartPath, "--kube-version", "1.6"}, - expectKey: "subchart1/templates/service.yaml", - expectValue: "kube-version/major: \"1\"\n kube-version/minor: \"6\"\n kube-version/gitversion: \"v1.6.0\"", + name: "template with show-only glob", + cmd: fmt.Sprintf("template '%s' --show-only templates/subdir/role*", chartPath), + golden: "output/template-show-only-glob.txt", + // Repeat to ensure manifest ordering regressions are caught + repeat: 10, + }, + { + name: "sorted output of manifests (order of filenames, then order of objects within each YAML file)", + cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/object-order"), + golden: "output/object-order.txt", + // Helm previously used random file order. Repeat the test so we + // don't accidentally get the expected result. + repeat: 10, + }, + { + name: "chart with template with invalid yaml", + cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-with-template-with-invalid-yaml"), + wantError: true, + golden: "output/template-with-invalid-yaml.txt", + }, + { + name: "chart with template with invalid yaml (--debug)", + cmd: fmt.Sprintf("template '%s' --debug", "testdata/testcharts/chart-with-template-with-invalid-yaml"), + wantError: true, + golden: "output/template-with-invalid-yaml-debug.txt", + }, + { + name: "template skip-tests", + cmd: fmt.Sprintf(`template '%s' --skip-tests`, chartPath), + golden: "output/template-skip-tests.txt", }, } + runTestCmd(t, tests) +} - 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) { - 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() - }) - } +func TestTemplateVersionCompletion(t *testing.T) { + repoFile := "testdata/helmhome/helm/repositories.yaml" + repoCache := "testdata/helmhome/helm/repository" + + repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache) + + tests := []cmdTestCase{{ + name: "completion for template version flag with release name", + cmd: fmt.Sprintf("%s __complete template releasename testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for template version flag with generate-name", + cmd: fmt.Sprintf("%s __complete template --generate-name testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for template version flag too few args", + cmd: fmt.Sprintf("%s __complete template testing/alpine --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for template version flag too many args", + cmd: fmt.Sprintf("%s __complete template releasename testing/alpine badarg --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for template version flag invalid chart", + cmd: fmt.Sprintf("%s __complete template releasename invalid/invalid --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }} + runTestCmd(t, tests) +} + +func TestTemplateFileCompletion(t *testing.T) { + checkFileCompletion(t, "template", false) + checkFileCompletion(t, "template --generate-name", true) + checkFileCompletion(t, "template myname", true) + checkFileCompletion(t, "template myname mychart", false) } diff --git a/cmd/helm/testdata/helmhome/plugins/args/args.sh b/cmd/helm/testdata/helmhome/helm/plugins/args/args.sh similarity index 100% rename from cmd/helm/testdata/helmhome/plugins/args/args.sh rename to cmd/helm/testdata/helmhome/helm/plugins/args/args.sh diff --git a/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete b/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete new file mode 100755 index 00000000000..2b00c22817c --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +echo "plugin.complete was called" +echo "Namespace: ${HELM_NAMESPACE:-NO_NS}" +echo "Num args received: ${#}" +echo "Args received: ${@}" + +# Final printout is the optional completion directive of the form : +if [ "$HELM_NAMESPACE" = "default" ]; then + echo ":4" +else + echo ":2" +fi diff --git a/cmd/helm/testdata/helmhome/plugins/args/plugin.yaml b/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.yaml similarity index 100% rename from cmd/helm/testdata/helmhome/plugins/args/plugin.yaml rename to cmd/helm/testdata/helmhome/helm/plugins/args/plugin.yaml diff --git a/cmd/helm/testdata/helmhome/repository/local/index.yaml b/cmd/helm/testdata/helmhome/helm/plugins/echo/completion.yaml similarity index 100% rename from cmd/helm/testdata/helmhome/repository/local/index.yaml rename to cmd/helm/testdata/helmhome/helm/plugins/echo/completion.yaml diff --git a/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete b/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete new file mode 100755 index 00000000000..6bc73d130c5 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +echo "echo plugin.complete was called" +echo "Namespace: ${HELM_NAMESPACE:-NO_NS}" +echo "Num args received: ${#}" +echo "Args received: ${@}" + +# Final printout is the optional completion directive of the form : +if [ "$HELM_NAMESPACE" = "default" ]; then + # Output an invalid directive, which should be ignored + echo ":2222" +# else + # Don't include the directive, to test it is really optional +fi diff --git a/cmd/helm/testdata/helmhome/plugins/echo/plugin.yaml b/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.yaml similarity index 100% rename from cmd/helm/testdata/helmhome/plugins/echo/plugin.yaml rename to cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.yaml diff --git a/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml new file mode 100644 index 00000000000..e479a0503d6 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml @@ -0,0 +1,13 @@ +name: env +commands: + - name: list + flags: + - a + - all + - log + - name: remove + validArgs: + - all + - one +flags: +- global diff --git a/cmd/helm/testdata/helmhome/helm/plugins/env/plugin.yaml b/cmd/helm/testdata/helmhome/helm/plugins/env/plugin.yaml new file mode 100644 index 00000000000..52cb7a84855 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/env/plugin.yaml @@ -0,0 +1,4 @@ +name: env +usage: "env stuff" +description: "show the env" +command: "echo $HELM_PLUGIN_NAME" diff --git a/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml new file mode 100644 index 00000000000..e5bf440f6cd --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml @@ -0,0 +1,5 @@ +commands: + - name: code + flags: + - a + - b diff --git a/cmd/helm/testdata/helmhome/helm/plugins/exitwith/exitwith.sh b/cmd/helm/testdata/helmhome/helm/plugins/exitwith/exitwith.sh new file mode 100755 index 00000000000..ec84696575e --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/exitwith/exitwith.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exit $* diff --git a/cmd/helm/testdata/helmhome/helm/plugins/exitwith/plugin.yaml b/cmd/helm/testdata/helmhome/helm/plugins/exitwith/plugin.yaml new file mode 100644 index 00000000000..5691d1712aa --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/exitwith/plugin.yaml @@ -0,0 +1,4 @@ +name: exitwith +usage: "exitwith code" +description: "This exits with the specified exit code" +command: "$HELM_PLUGIN_DIR/exitwith.sh" diff --git a/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml new file mode 100644 index 00000000000..e0b161c69ba --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml @@ -0,0 +1,19 @@ +name: wrongname +commands: + - name: empty + - name: full + commands: + - name: more + validArgs: + - one + - two + flags: + - b + - ball + - name: less + flags: + - a + - all +flags: +- z +- q diff --git a/cmd/helm/testdata/helmhome/helm/plugins/fullenv/fullenv.sh b/cmd/helm/testdata/helmhome/helm/plugins/fullenv/fullenv.sh new file mode 100755 index 00000000000..2efad9b3c87 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/fullenv/fullenv.sh @@ -0,0 +1,7 @@ +#!/bin/sh +echo $HELM_PLUGIN_NAME +echo $HELM_PLUGIN_DIR +echo $HELM_PLUGINS +echo $HELM_REPOSITORY_CONFIG +echo $HELM_REPOSITORY_CACHE +echo $HELM_BIN diff --git a/cmd/helm/testdata/helmhome/plugins/fullenv/plugin.yaml b/cmd/helm/testdata/helmhome/helm/plugins/fullenv/plugin.yaml similarity index 100% rename from cmd/helm/testdata/helmhome/plugins/fullenv/plugin.yaml rename to cmd/helm/testdata/helmhome/helm/plugins/fullenv/plugin.yaml diff --git a/cmd/helm/testdata/helmhome/repository/repositories.yaml b/cmd/helm/testdata/helmhome/helm/repositories.yaml similarity index 100% rename from cmd/helm/testdata/helmhome/repository/repositories.yaml rename to cmd/helm/testdata/helmhome/helm/repositories.yaml diff --git a/pkg/chartutil/testdata/frobnitz_backslash/ignore/me.txt b/cmd/helm/testdata/helmhome/helm/repository/test-name-charts.txt old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/ignore/me.txt rename to cmd/helm/testdata/helmhome/helm/repository/test-name-charts.txt diff --git a/cmd/helm/testdata/helmhome/helm/repository/test-name-index.yaml b/cmd/helm/testdata/helmhome/helm/repository/test-name-index.yaml new file mode 100644 index 00000000000..d5ab620ad49 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/repository/test-name-index.yaml @@ -0,0 +1,3 @@ +apiVersion: v1 +entries: {} +generated: "2020-09-09T19:50:50.198347916-04:00" diff --git a/cmd/helm/testdata/helmhome/helm/repository/testing-index.yaml b/cmd/helm/testdata/helmhome/helm/repository/testing-index.yaml new file mode 100644 index 00000000000..91e4d463f1b --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/repository/testing-index.yaml @@ -0,0 +1,66 @@ +apiVersion: v1 +entries: + alpine: + - name: alpine + url: https://charts.helm.sh/stable/alpine-0.1.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + created: "2018-06-27T10:00:18.230700509Z" + deprecated: true + home: https://helm.sh/helm + sources: + - https://github.com/helm/helm + version: 0.1.0 + appVersion: 1.2.3 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + icon: "" + apiVersion: v2 + - name: alpine + url: https://charts.helm.sh/stable/alpine-0.2.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + created: "2018-07-09T11:34:37.797864902Z" + home: https://helm.sh/helm + sources: + - https://github.com/helm/helm + version: 0.2.0 + appVersion: 2.3.4 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + icon: "" + apiVersion: v2 + - name: alpine + url: https://charts.helm.sh/stable/alpine-0.3.0-rc.1.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + created: "2020-11-12T08:44:58.872726222Z" + home: https://helm.sh/helm + sources: + - https://github.com/helm/helm + version: 0.3.0-rc.1 + appVersion: 3.0.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + icon: "" + apiVersion: v2 + mariadb: + - name: mariadb + url: https://charts.helm.sh/stable/mariadb-0.3.0.tgz + checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 + created: "2018-04-23T08:20:27.160959131Z" + home: https://mariadb.org + sources: + - https://github.com/bitnami/bitnami-docker-mariadb + version: 0.3.0 + description: Chart for MariaDB + keywords: + - mariadb + - mysql + - database + - sql + maintainers: + - name: Bitnami + email: containers@bitnami.com + icon: "" + apiVersion: v2 diff --git a/cmd/helm/testdata/helmhome/plugins/env/plugin.yaml b/cmd/helm/testdata/helmhome/plugins/env/plugin.yaml deleted file mode 100644 index c8ae403502e..00000000000 --- a/cmd/helm/testdata/helmhome/plugins/env/plugin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: env -usage: "env stuff" -description: "show the env" -command: "echo $HELM_HOME" diff --git a/cmd/helm/testdata/helmhome/plugins/fullenv/fullenv.sh b/cmd/helm/testdata/helmhome/plugins/fullenv/fullenv.sh deleted file mode 100755 index 518f492e901..00000000000 --- a/cmd/helm/testdata/helmhome/plugins/fullenv/fullenv.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -echo $HELM_PLUGIN_NAME -echo $HELM_PLUGIN_DIR -echo $HELM_PLUGIN -echo $HELM_HOME -echo $HELM_PATH_REPOSITORY -echo $HELM_PATH_REPOSITORY_FILE -echo $HELM_PATH_CACHE -echo $HELM_PATH_LOCAL_REPOSITORY -echo $HELM_BIN diff --git a/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml b/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml deleted file mode 100644 index 96c98c38c4b..00000000000 --- a/cmd/helm/testdata/helmhome/repository/cache/testing-index.yaml +++ /dev/null @@ -1,48 +0,0 @@ -apiVersion: v1 -entries: - alpine: - - name: alpine - url: https://kubernetes-charts.storage.googleapis.com/alpine-0.1.0.tgz - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 0.1.0 - appVersion: 1.2.3 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" - - name: alpine - url: https://kubernetes-charts.storage.googleapis.com/alpine-0.2.0.tgz - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 0.2.0 - appVersion: 2.3.4 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" - mariadb: - - name: mariadb - url: https://kubernetes-charts.storage.googleapis.com/mariadb-0.3.0.tgz - checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 - home: https://mariadb.org - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - version: 0.3.0 - description: Chart for MariaDB - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - name: Bitnami - email: containers@bitnami.com - engine: gotpl - icon: "" diff --git a/cmd/helm/testdata/output/chart-with-subchart-update.txt b/cmd/helm/testdata/output/chart-with-subchart-update.txt new file mode 100644 index 00000000000..a4135c78251 --- /dev/null +++ b/cmd/helm/testdata/output/chart-with-subchart-update.txt @@ -0,0 +1,8 @@ +NAME: updeps +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None +NOTES: +PARENT NOTES diff --git a/cmd/helm/testdata/output/dependency-list-archive.txt b/cmd/helm/testdata/output/dependency-list-archive.txt new file mode 100644 index 00000000000..ffd4542b048 --- /dev/null +++ b/cmd/helm/testdata/output/dependency-list-archive.txt @@ -0,0 +1,5 @@ +NAME VERSION REPOSITORY STATUS +reqsubchart 0.1.0 https://example.com/charts unpacked +reqsubchart2 0.2.0 https://example.com/charts unpacked +reqsubchart3 >=0.1.0 https://example.com/charts unpacked + diff --git a/cmd/helm/testdata/output/dependency-list-no-chart-linux.txt b/cmd/helm/testdata/output/dependency-list-no-chart-linux.txt new file mode 100644 index 00000000000..8fab8f8eb79 --- /dev/null +++ b/cmd/helm/testdata/output/dependency-list-no-chart-linux.txt @@ -0,0 +1 @@ +Error: stat /no/such/chart: no such file or directory diff --git a/cmd/helm/testdata/output/dependency-list-no-requirements-linux.txt b/cmd/helm/testdata/output/dependency-list-no-requirements-linux.txt new file mode 100644 index 00000000000..35fe1d2e374 --- /dev/null +++ b/cmd/helm/testdata/output/dependency-list-no-requirements-linux.txt @@ -0,0 +1 @@ +WARNING: no dependencies at testdata/testcharts/alpine/charts diff --git a/cmd/helm/testdata/output/dependency-list.txt b/cmd/helm/testdata/output/dependency-list.txt new file mode 100644 index 00000000000..b57c21a2182 --- /dev/null +++ b/cmd/helm/testdata/output/dependency-list.txt @@ -0,0 +1,5 @@ +NAME VERSION REPOSITORY STATUS +reqsubchart 0.1.0 https://example.com/charts unpacked +reqsubchart2 0.2.0 https://example.com/charts unpacked +reqsubchart3 >=0.1.0 https://example.com/charts ok + diff --git a/cmd/helm/testdata/output/deprecated-chart.txt b/cmd/helm/testdata/output/deprecated-chart.txt new file mode 100644 index 00000000000..039d6aef6a2 --- /dev/null +++ b/cmd/helm/testdata/output/deprecated-chart.txt @@ -0,0 +1,6 @@ +NAME: aeneas +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/docs-type-comp.txt b/cmd/helm/testdata/output/docs-type-comp.txt new file mode 100644 index 00000000000..69494f87d18 --- /dev/null +++ b/cmd/helm/testdata/output/docs-type-comp.txt @@ -0,0 +1,5 @@ +bash +man +markdown +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/docs-type-filtered-comp.txt b/cmd/helm/testdata/output/docs-type-filtered-comp.txt new file mode 100644 index 00000000000..55104f32e6d --- /dev/null +++ b/cmd/helm/testdata/output/docs-type-filtered-comp.txt @@ -0,0 +1,3 @@ +markdown +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/empty_default_comp.txt b/cmd/helm/testdata/output/empty_default_comp.txt new file mode 100644 index 00000000000..879d50d0ea9 --- /dev/null +++ b/cmd/helm/testdata/output/empty_default_comp.txt @@ -0,0 +1,2 @@ +:0 +Completion ended with directive: ShellCompDirectiveDefault diff --git a/cmd/helm/testdata/output/empty_nofile_comp.txt b/cmd/helm/testdata/output/empty_nofile_comp.txt new file mode 100644 index 00000000000..8d9fad576c0 --- /dev/null +++ b/cmd/helm/testdata/output/empty_nofile_comp.txt @@ -0,0 +1,2 @@ +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/env-comp.txt b/cmd/helm/testdata/output/env-comp.txt new file mode 100644 index 00000000000..b7befd69e00 --- /dev/null +++ b/cmd/helm/testdata/output/env-comp.txt @@ -0,0 +1,19 @@ +HELM_BIN +HELM_CACHE_HOME +HELM_CONFIG_HOME +HELM_DATA_HOME +HELM_DEBUG +HELM_KUBEAPISERVER +HELM_KUBEASGROUPS +HELM_KUBEASUSER +HELM_KUBECAFILE +HELM_KUBECONTEXT +HELM_KUBETOKEN +HELM_MAX_HISTORY +HELM_NAMESPACE +HELM_PLUGINS +HELM_REGISTRY_CONFIG +HELM_REPOSITORY_CACHE +HELM_REPOSITORY_CONFIG +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/get-all-no-args.txt b/cmd/helm/testdata/output/get-all-no-args.txt new file mode 100644 index 00000000000..cc3fc2ad13b --- /dev/null +++ b/cmd/helm/testdata/output/get-all-no-args.txt @@ -0,0 +1,3 @@ +Error: "helm get all" requires 1 argument + +Usage: helm get all RELEASE_NAME [flags] diff --git a/cmd/helm/testdata/output/get-hooks-no-args.txt b/cmd/helm/testdata/output/get-hooks-no-args.txt new file mode 100644 index 00000000000..2911fdb88b2 --- /dev/null +++ b/cmd/helm/testdata/output/get-hooks-no-args.txt @@ -0,0 +1,3 @@ +Error: "helm get hooks" requires 1 argument + +Usage: helm get hooks RELEASE_NAME [flags] diff --git a/cmd/helm/testdata/output/get-hooks.txt b/cmd/helm/testdata/output/get-hooks.txt new file mode 100644 index 00000000000..81e87b1f106 --- /dev/null +++ b/cmd/helm/testdata/output/get-hooks.txt @@ -0,0 +1,8 @@ +--- +# Source: pre-install-hook.yaml +apiVersion: v1 +kind: Job +metadata: + annotations: + "helm.sh/hook": pre-install + diff --git a/cmd/helm/testdata/output/get-manifest-no-args.txt b/cmd/helm/testdata/output/get-manifest-no-args.txt new file mode 100644 index 00000000000..df7aa5b04a0 --- /dev/null +++ b/cmd/helm/testdata/output/get-manifest-no-args.txt @@ -0,0 +1,3 @@ +Error: "helm get manifest" requires 1 argument + +Usage: helm get manifest RELEASE_NAME [flags] diff --git a/cmd/helm/testdata/output/get-manifest.txt b/cmd/helm/testdata/output/get-manifest.txt new file mode 100644 index 00000000000..88937e089cf --- /dev/null +++ b/cmd/helm/testdata/output/get-manifest.txt @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: Secret +metadata: + name: fixture + diff --git a/cmd/helm/testdata/output/get-notes-no-args.txt b/cmd/helm/testdata/output/get-notes-no-args.txt new file mode 100644 index 00000000000..1a0c20caafb --- /dev/null +++ b/cmd/helm/testdata/output/get-notes-no-args.txt @@ -0,0 +1,3 @@ +Error: "helm get notes" requires 1 argument + +Usage: helm get notes RELEASE_NAME [flags] diff --git a/cmd/helm/testdata/output/get-notes.txt b/cmd/helm/testdata/output/get-notes.txt new file mode 100644 index 00000000000..e710c780109 --- /dev/null +++ b/cmd/helm/testdata/output/get-notes.txt @@ -0,0 +1,2 @@ +NOTES: +Some mock release notes! diff --git a/cmd/helm/testdata/output/get-release-template.txt b/cmd/helm/testdata/output/get-release-template.txt new file mode 100644 index 00000000000..02d44fb012d --- /dev/null +++ b/cmd/helm/testdata/output/get-release-template.txt @@ -0,0 +1 @@ +0.1.0-beta.1 \ No newline at end of file diff --git a/cmd/helm/testdata/output/get-release.txt b/cmd/helm/testdata/output/get-release.txt new file mode 100644 index 00000000000..f6c3b57eb7e --- /dev/null +++ b/cmd/helm/testdata/output/get-release.txt @@ -0,0 +1,29 @@ +NAME: thomas-guide +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None +USER-SUPPLIED VALUES: +name: value + +COMPUTED VALUES: +name: value + +HOOKS: +--- +# Source: pre-install-hook.yaml +apiVersion: v1 +kind: Job +metadata: + annotations: + "helm.sh/hook": pre-install + +MANIFEST: +apiVersion: v1 +kind: Secret +metadata: + name: fixture + +NOTES: +Some mock release notes! diff --git a/cmd/helm/testdata/output/get-values-all.txt b/cmd/helm/testdata/output/get-values-all.txt new file mode 100644 index 00000000000..b7e9696bcbf --- /dev/null +++ b/cmd/helm/testdata/output/get-values-all.txt @@ -0,0 +1,2 @@ +COMPUTED VALUES: +name: value diff --git a/cmd/helm/testdata/output/get-values-args.txt b/cmd/helm/testdata/output/get-values-args.txt new file mode 100644 index 00000000000..c8a65e7f305 --- /dev/null +++ b/cmd/helm/testdata/output/get-values-args.txt @@ -0,0 +1,3 @@ +Error: "helm get values" requires 1 argument + +Usage: helm get values RELEASE_NAME [flags] diff --git a/cmd/helm/testdata/output/get-values.txt b/cmd/helm/testdata/output/get-values.txt new file mode 100644 index 00000000000..b7d146b1599 --- /dev/null +++ b/cmd/helm/testdata/output/get-values.txt @@ -0,0 +1,2 @@ +USER-SUPPLIED VALUES: +name: value diff --git a/cmd/helm/testdata/output/history-limit.txt b/cmd/helm/testdata/output/history-limit.txt new file mode 100644 index 00000000000..aee0fadb275 --- /dev/null +++ b/cmd/helm/testdata/output/history-limit.txt @@ -0,0 +1,3 @@ +REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION +3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock diff --git a/cmd/helm/testdata/output/history.json b/cmd/helm/testdata/output/history.json new file mode 100644 index 00000000000..35311d3ce1d --- /dev/null +++ b/cmd/helm/testdata/output/history.json @@ -0,0 +1 @@ +[{"revision":3,"updated":"1977-09-02T22:04:05Z","status":"superseded","chart":"foo-0.1.0-beta.1","app_version":"1.0","description":"Release mock"},{"revision":4,"updated":"1977-09-02T22:04:05Z","status":"deployed","chart":"foo-0.1.0-beta.1","app_version":"1.0","description":"Release mock"}] diff --git a/cmd/helm/testdata/output/history.txt b/cmd/helm/testdata/output/history.txt new file mode 100644 index 00000000000..2a5d69c1144 --- /dev/null +++ b/cmd/helm/testdata/output/history.txt @@ -0,0 +1,5 @@ +REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION +1 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +2 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +3 Fri Sep 2 22:04:05 1977 superseded foo-0.1.0-beta.1 1.0 Release mock +4 Fri Sep 2 22:04:05 1977 deployed foo-0.1.0-beta.1 1.0 Release mock diff --git a/cmd/helm/testdata/output/history.yaml b/cmd/helm/testdata/output/history.yaml new file mode 100644 index 00000000000..b7ae03be78a --- /dev/null +++ b/cmd/helm/testdata/output/history.yaml @@ -0,0 +1,12 @@ +- app_version: "1.0" + chart: foo-0.1.0-beta.1 + description: Release mock + revision: 3 + status: superseded + updated: "1977-09-02T22:04:05Z" +- app_version: "1.0" + chart: foo-0.1.0-beta.1 + description: Release mock + revision: 4 + status: deployed + updated: "1977-09-02T22:04:05Z" diff --git a/cmd/helm/testdata/output/install-and-replace.txt b/cmd/helm/testdata/output/install-and-replace.txt new file mode 100644 index 00000000000..039d6aef6a2 --- /dev/null +++ b/cmd/helm/testdata/output/install-and-replace.txt @@ -0,0 +1,6 @@ +NAME: aeneas +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/install-chart-bad-type.txt b/cmd/helm/testdata/output/install-chart-bad-type.txt new file mode 100644 index 00000000000..d8a3bf275ac --- /dev/null +++ b/cmd/helm/testdata/output/install-chart-bad-type.txt @@ -0,0 +1 @@ +Error: validation: chart.metadata.type must be application or library diff --git a/cmd/helm/testdata/output/install-name-template.txt b/cmd/helm/testdata/output/install-name-template.txt new file mode 100644 index 00000000000..67e06d92bc5 --- /dev/null +++ b/cmd/helm/testdata/output/install-name-template.txt @@ -0,0 +1,6 @@ +NAME: FOOBAR +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/install-no-args.txt b/cmd/helm/testdata/output/install-no-args.txt new file mode 100644 index 00000000000..47f010ab810 --- /dev/null +++ b/cmd/helm/testdata/output/install-no-args.txt @@ -0,0 +1,3 @@ +Error: "helm install" requires at least 1 argument + +Usage: helm install [NAME] [CHART] [flags] diff --git a/cmd/helm/testdata/output/install-no-hooks.txt b/cmd/helm/testdata/output/install-no-hooks.txt new file mode 100644 index 00000000000..039d6aef6a2 --- /dev/null +++ b/cmd/helm/testdata/output/install-no-hooks.txt @@ -0,0 +1,6 @@ +NAME: aeneas +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/install-with-multiple-values-files.txt b/cmd/helm/testdata/output/install-with-multiple-values-files.txt new file mode 100644 index 00000000000..406e522a99d --- /dev/null +++ b/cmd/helm/testdata/output/install-with-multiple-values-files.txt @@ -0,0 +1,6 @@ +NAME: virgil +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/install-with-multiple-values.txt b/cmd/helm/testdata/output/install-with-multiple-values.txt new file mode 100644 index 00000000000..406e522a99d --- /dev/null +++ b/cmd/helm/testdata/output/install-with-multiple-values.txt @@ -0,0 +1,6 @@ +NAME: virgil +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/install-with-timeout.txt b/cmd/helm/testdata/output/install-with-timeout.txt new file mode 100644 index 00000000000..19952e3c2be --- /dev/null +++ b/cmd/helm/testdata/output/install-with-timeout.txt @@ -0,0 +1,6 @@ +NAME: foobar +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/install-with-values-file.txt b/cmd/helm/testdata/output/install-with-values-file.txt new file mode 100644 index 00000000000..406e522a99d --- /dev/null +++ b/cmd/helm/testdata/output/install-with-values-file.txt @@ -0,0 +1,6 @@ +NAME: virgil +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/install-with-values.txt b/cmd/helm/testdata/output/install-with-values.txt new file mode 100644 index 00000000000..406e522a99d --- /dev/null +++ b/cmd/helm/testdata/output/install-with-values.txt @@ -0,0 +1,6 @@ +NAME: virgil +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/install-with-wait-for-jobs.txt b/cmd/helm/testdata/output/install-with-wait-for-jobs.txt new file mode 100644 index 00000000000..7ce22d4ec94 --- /dev/null +++ b/cmd/helm/testdata/output/install-with-wait-for-jobs.txt @@ -0,0 +1,6 @@ +NAME: apollo +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/install-with-wait.txt b/cmd/helm/testdata/output/install-with-wait.txt new file mode 100644 index 00000000000..7ce22d4ec94 --- /dev/null +++ b/cmd/helm/testdata/output/install-with-wait.txt @@ -0,0 +1,6 @@ +NAME: apollo +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/install.txt b/cmd/helm/testdata/output/install.txt new file mode 100644 index 00000000000..039d6aef6a2 --- /dev/null +++ b/cmd/helm/testdata/output/install.txt @@ -0,0 +1,6 @@ +NAME: aeneas +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt b/cmd/helm/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt new file mode 100644 index 00000000000..e77aa387fd9 --- /dev/null +++ b/cmd/helm/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt @@ -0,0 +1,20 @@ +==> Linting testdata/testcharts/chart-with-bad-subcharts +[INFO] Chart.yaml: icon is recommended +[WARNING] templates/: directory not found +[ERROR] : unable to load chart + error unpacking bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required + +==> Linting testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart +[ERROR] Chart.yaml: name is required +[ERROR] Chart.yaml: apiVersion is required. The value must be either "v1" or "v2" +[ERROR] Chart.yaml: version is required +[INFO] Chart.yaml: icon is recommended +[WARNING] templates/: directory not found +[ERROR] : unable to load chart + validation: chart.metadata.name is required + +==> Linting testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart +[INFO] Chart.yaml: icon is recommended +[WARNING] templates/: directory not found + +Error: 3 chart(s) linted, 2 chart(s) failed diff --git a/cmd/helm/testdata/output/lint-chart-with-bad-subcharts.txt b/cmd/helm/testdata/output/lint-chart-with-bad-subcharts.txt new file mode 100644 index 00000000000..265e555f7bd --- /dev/null +++ b/cmd/helm/testdata/output/lint-chart-with-bad-subcharts.txt @@ -0,0 +1,7 @@ +==> Linting testdata/testcharts/chart-with-bad-subcharts +[INFO] Chart.yaml: icon is recommended +[WARNING] templates/: directory not found +[ERROR] : unable to load chart + error unpacking bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required + +Error: 1 chart(s) linted, 1 chart(s) failed diff --git a/cmd/helm/testdata/output/list-all.txt b/cmd/helm/testdata/output/list-all.txt new file mode 100644 index 00000000000..ef6d44cd56e --- /dev/null +++ b/cmd/helm/testdata/output/list-all.txt @@ -0,0 +1,9 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 +gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 +groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 +thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-date-reversed.txt b/cmd/helm/testdata/output/list-date-reversed.txt new file mode 100644 index 00000000000..8b4e71a38e1 --- /dev/null +++ b/cmd/helm/testdata/output/list-date-reversed.txt @@ -0,0 +1,5 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-date.txt b/cmd/helm/testdata/output/list-date.txt new file mode 100644 index 00000000000..3d2b27ad8b8 --- /dev/null +++ b/cmd/helm/testdata/output/list-date.txt @@ -0,0 +1,5 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-failed.txt b/cmd/helm/testdata/output/list-failed.txt new file mode 100644 index 00000000000..a8ec3e1326f --- /dev/null +++ b/cmd/helm/testdata/output/list-failed.txt @@ -0,0 +1,2 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-filter.txt b/cmd/helm/testdata/output/list-filter.txt new file mode 100644 index 00000000000..0a820922b70 --- /dev/null +++ b/cmd/helm/testdata/output/list-filter.txt @@ -0,0 +1,5 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-max.txt b/cmd/helm/testdata/output/list-max.txt new file mode 100644 index 00000000000..a909322b42e --- /dev/null +++ b/cmd/helm/testdata/output/list-max.txt @@ -0,0 +1,2 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-namespace.txt b/cmd/helm/testdata/output/list-namespace.txt new file mode 100644 index 00000000000..9382327d610 --- /dev/null +++ b/cmd/helm/testdata/output/list-namespace.txt @@ -0,0 +1,2 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +starlord milano 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-offset.txt b/cmd/helm/testdata/output/list-offset.txt new file mode 100644 index 00000000000..36e963ca5b4 --- /dev/null +++ b/cmd/helm/testdata/output/list-offset.txt @@ -0,0 +1,4 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-pending.txt b/cmd/helm/testdata/output/list-pending.txt new file mode 100644 index 00000000000..f3d7aa03b42 --- /dev/null +++ b/cmd/helm/testdata/output/list-pending.txt @@ -0,0 +1,2 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-reverse.txt b/cmd/helm/testdata/output/list-reverse.txt new file mode 100644 index 00000000000..da178b2c350 --- /dev/null +++ b/cmd/helm/testdata/output/list-reverse.txt @@ -0,0 +1,5 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-short-json.txt b/cmd/helm/testdata/output/list-short-json.txt new file mode 100644 index 00000000000..acbf1e44dbd --- /dev/null +++ b/cmd/helm/testdata/output/list-short-json.txt @@ -0,0 +1 @@ +["hummingbird","iguana","rocket","starlord"] diff --git a/cmd/helm/testdata/output/list-short-yaml.txt b/cmd/helm/testdata/output/list-short-yaml.txt new file mode 100644 index 00000000000..86fb3d67083 --- /dev/null +++ b/cmd/helm/testdata/output/list-short-yaml.txt @@ -0,0 +1,4 @@ +- hummingbird +- iguana +- rocket +- starlord diff --git a/cmd/helm/testdata/output/list-short.txt b/cmd/helm/testdata/output/list-short.txt new file mode 100644 index 00000000000..0a63be990a1 --- /dev/null +++ b/cmd/helm/testdata/output/list-short.txt @@ -0,0 +1,4 @@ +hummingbird +iguana +rocket +starlord diff --git a/cmd/helm/testdata/output/list-superseded.txt b/cmd/helm/testdata/output/list-superseded.txt new file mode 100644 index 00000000000..50b4358749e --- /dev/null +++ b/cmd/helm/testdata/output/list-superseded.txt @@ -0,0 +1,3 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 +starlord default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-uninstalled.txt b/cmd/helm/testdata/output/list-uninstalled.txt new file mode 100644 index 00000000000..430cf32fbf4 --- /dev/null +++ b/cmd/helm/testdata/output/list-uninstalled.txt @@ -0,0 +1,2 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list-uninstalling.txt b/cmd/helm/testdata/output/list-uninstalling.txt new file mode 100644 index 00000000000..9228963917a --- /dev/null +++ b/cmd/helm/testdata/output/list-uninstalling.txt @@ -0,0 +1,2 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/list.txt b/cmd/helm/testdata/output/list.txt new file mode 100644 index 00000000000..0a820922b70 --- /dev/null +++ b/cmd/helm/testdata/output/list.txt @@ -0,0 +1,5 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/cmd/helm/testdata/output/object-order.txt b/cmd/helm/testdata/output/object-order.txt new file mode 100644 index 00000000000..307f928f2e7 --- /dev/null +++ b/cmd/helm/testdata/output/object-order.txt @@ -0,0 +1,191 @@ +--- +# Source: object-order/templates/01-a.yml +# 1 +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: first +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/01-a.yml +# 2 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: second +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/01-a.yml +# 3 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: third +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/02-b.yml +# 5 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fifth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/02-b.yml +# 7 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: seventh +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/02-b.yml +# 8 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: eighth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/02-b.yml +# 9 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: ninth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/02-b.yml +# 10 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: tenth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/02-b.yml +# 11 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: eleventh +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/02-b.yml +# 12 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: twelfth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/02-b.yml +# 13 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: thirteenth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/02-b.yml +# 14 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fourteenth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/02-b.yml +# 15 (11th object within 02-b.yml, in order to test `SplitManifests` which assigns `manifest-10` +# to this object which should then come *after* `manifest-9`) +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fifteenth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress +--- +# Source: object-order/templates/01-a.yml +# 4 (Deployment should come after all NetworkPolicy manifests, since 'helm template' outputs in install order) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fourth +spec: + selector: + matchLabels: + pod: fourth + replicas: 1 + template: + metadata: + labels: + pod: fourth + spec: + containers: + - name: hello-world + image: gcr.io/google-samples/node-hello:1.0 +--- +# Source: object-order/templates/02-b.yml +# 6 (implementation detail: currently, 'helm template' outputs hook manifests last; and yes, NetworkPolicy won't make a reasonable hook, this is just a dummy unit test manifest) +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + "helm.sh/hook": pre-install + name: sixth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress diff --git a/cmd/helm/testdata/output/output-comp.txt b/cmd/helm/testdata/output/output-comp.txt new file mode 100644 index 00000000000..6232b2928bd --- /dev/null +++ b/cmd/helm/testdata/output/output-comp.txt @@ -0,0 +1,5 @@ +json Output result in JSON format +table Output result in human-readable format +yaml Output result in YAML format +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/plugin_args_comp.txt b/cmd/helm/testdata/output/plugin_args_comp.txt new file mode 100644 index 00000000000..4070cb1e6f2 --- /dev/null +++ b/cmd/helm/testdata/output/plugin_args_comp.txt @@ -0,0 +1,6 @@ +plugin.complete was called +Namespace: default +Num args received: 1 +Args received: +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/plugin_args_flag_comp.txt b/cmd/helm/testdata/output/plugin_args_flag_comp.txt new file mode 100644 index 00000000000..87300fa9749 --- /dev/null +++ b/cmd/helm/testdata/output/plugin_args_flag_comp.txt @@ -0,0 +1,6 @@ +plugin.complete was called +Namespace: default +Num args received: 2 +Args received: --myflag +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/plugin_args_many_args_comp.txt b/cmd/helm/testdata/output/plugin_args_many_args_comp.txt new file mode 100644 index 00000000000..f3c386b6d3d --- /dev/null +++ b/cmd/helm/testdata/output/plugin_args_many_args_comp.txt @@ -0,0 +1,6 @@ +plugin.complete was called +Namespace: mynamespace +Num args received: 2 +Args received: --myflag start +:2 +Completion ended with directive: ShellCompDirectiveNoSpace diff --git a/cmd/helm/testdata/output/plugin_args_ns_comp.txt b/cmd/helm/testdata/output/plugin_args_ns_comp.txt new file mode 100644 index 00000000000..13bfcd3f4a3 --- /dev/null +++ b/cmd/helm/testdata/output/plugin_args_ns_comp.txt @@ -0,0 +1,6 @@ +plugin.complete was called +Namespace: mynamespace +Num args received: 1 +Args received: +:2 +Completion ended with directive: ShellCompDirectiveNoSpace diff --git a/cmd/helm/testdata/output/plugin_echo_bad_directive.txt b/cmd/helm/testdata/output/plugin_echo_bad_directive.txt new file mode 100644 index 00000000000..9f280258137 --- /dev/null +++ b/cmd/helm/testdata/output/plugin_echo_bad_directive.txt @@ -0,0 +1,6 @@ +echo plugin.complete was called +Namespace: default +Num args received: 1 +Args received: +:0 +Completion ended with directive: ShellCompDirectiveDefault diff --git a/cmd/helm/testdata/output/plugin_echo_no_directive.txt b/cmd/helm/testdata/output/plugin_echo_no_directive.txt new file mode 100644 index 00000000000..99cc47c1369 --- /dev/null +++ b/cmd/helm/testdata/output/plugin_echo_no_directive.txt @@ -0,0 +1,6 @@ +echo plugin.complete was called +Namespace: mynamespace +Num args received: 1 +Args received: +:0 +Completion ended with directive: ShellCompDirectiveDefault diff --git a/cmd/helm/testdata/output/plugin_list_comp.txt b/cmd/helm/testdata/output/plugin_list_comp.txt new file mode 100644 index 00000000000..833efc5e92a --- /dev/null +++ b/cmd/helm/testdata/output/plugin_list_comp.txt @@ -0,0 +1,7 @@ +args echo args +echo echo stuff +env env stuff +exitwith exitwith code +fullenv show env vars +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/plugin_repeat_comp.txt b/cmd/helm/testdata/output/plugin_repeat_comp.txt new file mode 100644 index 00000000000..3fa05f0b3ae --- /dev/null +++ b/cmd/helm/testdata/output/plugin_repeat_comp.txt @@ -0,0 +1,6 @@ +echo echo stuff +env env stuff +exitwith exitwith code +fullenv show env vars +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/release_list_comp.txt b/cmd/helm/testdata/output/release_list_comp.txt new file mode 100644 index 00000000000..226c378a923 --- /dev/null +++ b/cmd/helm/testdata/output/release_list_comp.txt @@ -0,0 +1,5 @@ +aramis foo-0.1.0-beta.1 -> deployed +athos foo-0.1.0-beta.1 -> deployed +porthos foo-0.1.0-beta.1 -> deployed +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/release_list_repeat_comp.txt b/cmd/helm/testdata/output/release_list_repeat_comp.txt new file mode 100644 index 00000000000..aa330f47fc1 --- /dev/null +++ b/cmd/helm/testdata/output/release_list_repeat_comp.txt @@ -0,0 +1,4 @@ +aramis foo-0.1.0-beta.1 -> deployed +athos foo-0.1.0-beta.1 -> deployed +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/repo-add.txt b/cmd/helm/testdata/output/repo-add.txt new file mode 100644 index 00000000000..e8882321e23 --- /dev/null +++ b/cmd/helm/testdata/output/repo-add.txt @@ -0,0 +1 @@ +"test-name" has been added to your repositories diff --git a/cmd/helm/testdata/output/repo-add2.txt b/cmd/helm/testdata/output/repo-add2.txt new file mode 100644 index 00000000000..263ffa9e4bf --- /dev/null +++ b/cmd/helm/testdata/output/repo-add2.txt @@ -0,0 +1 @@ +"test-name" already exists with the same configuration, skipping diff --git a/cmd/helm/testdata/output/revision-comp.txt b/cmd/helm/testdata/output/revision-comp.txt new file mode 100644 index 00000000000..fe9faf1f10c --- /dev/null +++ b/cmd/helm/testdata/output/revision-comp.txt @@ -0,0 +1,6 @@ +8 App: 1.0, Chart: foo-0.1.0-beta.1 +9 App: 1.0, Chart: foo-0.1.0-beta.1 +10 App: 1.0, Chart: foo-0.1.0-beta.1 +11 App: 1.0, Chart: foo-0.1.0-beta.1 +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/revision-wrong-args-comp.txt b/cmd/helm/testdata/output/revision-wrong-args-comp.txt new file mode 100644 index 00000000000..8d9fad576c0 --- /dev/null +++ b/cmd/helm/testdata/output/revision-wrong-args-comp.txt @@ -0,0 +1,2 @@ +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/rollback-comp.txt b/cmd/helm/testdata/output/rollback-comp.txt new file mode 100644 index 00000000000..2cfeed1f965 --- /dev/null +++ b/cmd/helm/testdata/output/rollback-comp.txt @@ -0,0 +1,4 @@ +carabins foo-0.1.0-beta.1 -> superseded +musketeers foo-0.1.0-beta.1 -> deployed +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/rollback-no-args.txt b/cmd/helm/testdata/output/rollback-no-args.txt new file mode 100644 index 00000000000..a1bc30b7ac3 --- /dev/null +++ b/cmd/helm/testdata/output/rollback-no-args.txt @@ -0,0 +1,3 @@ +Error: "helm rollback" requires at least 1 argument + +Usage: helm rollback [REVISION] [flags] diff --git a/cmd/helm/testdata/output/rollback-no-revision.txt b/cmd/helm/testdata/output/rollback-no-revision.txt new file mode 100644 index 00000000000..ae3c6f1c4e0 --- /dev/null +++ b/cmd/helm/testdata/output/rollback-no-revision.txt @@ -0,0 +1 @@ +Rollback was a success! Happy Helming! diff --git a/cmd/helm/testdata/output/rollback-timeout.txt b/cmd/helm/testdata/output/rollback-timeout.txt new file mode 100644 index 00000000000..ae3c6f1c4e0 --- /dev/null +++ b/cmd/helm/testdata/output/rollback-timeout.txt @@ -0,0 +1 @@ +Rollback was a success! Happy Helming! diff --git a/cmd/helm/testdata/output/rollback-wait-for-jobs.txt b/cmd/helm/testdata/output/rollback-wait-for-jobs.txt new file mode 100644 index 00000000000..ae3c6f1c4e0 --- /dev/null +++ b/cmd/helm/testdata/output/rollback-wait-for-jobs.txt @@ -0,0 +1 @@ +Rollback was a success! Happy Helming! diff --git a/cmd/helm/testdata/output/rollback-wait.txt b/cmd/helm/testdata/output/rollback-wait.txt new file mode 100644 index 00000000000..ae3c6f1c4e0 --- /dev/null +++ b/cmd/helm/testdata/output/rollback-wait.txt @@ -0,0 +1 @@ +Rollback was a success! Happy Helming! diff --git a/cmd/helm/testdata/output/rollback-wrong-args-comp.txt b/cmd/helm/testdata/output/rollback-wrong-args-comp.txt new file mode 100644 index 00000000000..8d9fad576c0 --- /dev/null +++ b/cmd/helm/testdata/output/rollback-wrong-args-comp.txt @@ -0,0 +1,2 @@ +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/rollback.txt b/cmd/helm/testdata/output/rollback.txt new file mode 100644 index 00000000000..ae3c6f1c4e0 --- /dev/null +++ b/cmd/helm/testdata/output/rollback.txt @@ -0,0 +1 @@ +Rollback was a success! Happy Helming! diff --git a/cmd/helm/testdata/output/schema-negative-cli.txt b/cmd/helm/testdata/output/schema-negative-cli.txt new file mode 100644 index 00000000000..d6f096e14de --- /dev/null +++ b/cmd/helm/testdata/output/schema-negative-cli.txt @@ -0,0 +1,4 @@ +Error: values don't meet the specifications of the schema(s) in the following chart(s): +empty: +- age: Must be greater than or equal to 0 + diff --git a/cmd/helm/testdata/output/schema-negative.txt b/cmd/helm/testdata/output/schema-negative.txt new file mode 100644 index 00000000000..f7c89dd564b --- /dev/null +++ b/cmd/helm/testdata/output/schema-negative.txt @@ -0,0 +1,5 @@ +Error: values don't meet the specifications of the schema(s) in the following chart(s): +empty: +- (root): employmentInfo is required +- age: Must be greater than or equal to 0 + diff --git a/cmd/helm/testdata/output/schema.txt b/cmd/helm/testdata/output/schema.txt new file mode 100644 index 00000000000..22a94b3f491 --- /dev/null +++ b/cmd/helm/testdata/output/schema.txt @@ -0,0 +1,6 @@ +NAME: schema +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/search-constraint-single.txt b/cmd/helm/testdata/output/search-constraint-single.txt new file mode 100644 index 00000000000..a1f75099f38 --- /dev/null +++ b/cmd/helm/testdata/output/search-constraint-single.txt @@ -0,0 +1,2 @@ +NAME CHART VERSION APP VERSION DESCRIPTION +testing/alpine 0.2.0 2.3.4 Deploy a basic Alpine Linux pod diff --git a/cmd/helm/testdata/output/search-constraint.txt b/cmd/helm/testdata/output/search-constraint.txt new file mode 100644 index 00000000000..9fb22fe76b1 --- /dev/null +++ b/cmd/helm/testdata/output/search-constraint.txt @@ -0,0 +1,2 @@ +NAME CHART VERSION APP VERSION DESCRIPTION +testing/alpine 0.1.0 1.2.3 Deploy a basic Alpine Linux pod diff --git a/cmd/helm/testdata/output/search-multiple-devel-release.txt b/cmd/helm/testdata/output/search-multiple-devel-release.txt new file mode 100644 index 00000000000..7e29a8f7e45 --- /dev/null +++ b/cmd/helm/testdata/output/search-multiple-devel-release.txt @@ -0,0 +1,2 @@ +NAME CHART VERSION APP VERSION DESCRIPTION +testing/alpine 0.3.0-rc.1 3.0.0 Deploy a basic Alpine Linux pod diff --git a/cmd/helm/testdata/output/search-multiple-stable-release.txt b/cmd/helm/testdata/output/search-multiple-stable-release.txt new file mode 100644 index 00000000000..a1f75099f38 --- /dev/null +++ b/cmd/helm/testdata/output/search-multiple-stable-release.txt @@ -0,0 +1,2 @@ +NAME CHART VERSION APP VERSION DESCRIPTION +testing/alpine 0.2.0 2.3.4 Deploy a basic Alpine Linux pod diff --git a/cmd/helm/testdata/output/search-multiple-versions-constraints.txt b/cmd/helm/testdata/output/search-multiple-versions-constraints.txt new file mode 100644 index 00000000000..a6a388858e9 --- /dev/null +++ b/cmd/helm/testdata/output/search-multiple-versions-constraints.txt @@ -0,0 +1,3 @@ +NAME CHART VERSION APP VERSION DESCRIPTION +testing/alpine 0.2.0 2.3.4 Deploy a basic Alpine Linux pod +testing/alpine 0.1.0 1.2.3 Deploy a basic Alpine Linux pod diff --git a/cmd/helm/testdata/output/search-multiple-versions.txt b/cmd/helm/testdata/output/search-multiple-versions.txt new file mode 100644 index 00000000000..a6a388858e9 --- /dev/null +++ b/cmd/helm/testdata/output/search-multiple-versions.txt @@ -0,0 +1,3 @@ +NAME CHART VERSION APP VERSION DESCRIPTION +testing/alpine 0.2.0 2.3.4 Deploy a basic Alpine Linux pod +testing/alpine 0.1.0 1.2.3 Deploy a basic Alpine Linux pod diff --git a/cmd/helm/testdata/output/search-not-found.txt b/cmd/helm/testdata/output/search-not-found.txt new file mode 100644 index 00000000000..4f2a9fd077d --- /dev/null +++ b/cmd/helm/testdata/output/search-not-found.txt @@ -0,0 +1 @@ +No results found diff --git a/cmd/helm/testdata/output/search-output-json.txt b/cmd/helm/testdata/output/search-output-json.txt new file mode 100644 index 00000000000..9b211e1b551 --- /dev/null +++ b/cmd/helm/testdata/output/search-output-json.txt @@ -0,0 +1 @@ +[{"name":"testing/mariadb","version":"0.3.0","app_version":"","description":"Chart for MariaDB"}] diff --git a/cmd/helm/testdata/output/search-output-yaml.txt b/cmd/helm/testdata/output/search-output-yaml.txt new file mode 100644 index 00000000000..122b7f345d2 --- /dev/null +++ b/cmd/helm/testdata/output/search-output-yaml.txt @@ -0,0 +1,4 @@ +- app_version: 2.3.4 + description: Deploy a basic Alpine Linux pod + name: testing/alpine + version: 0.2.0 diff --git a/cmd/helm/testdata/output/search-regex.txt b/cmd/helm/testdata/output/search-regex.txt new file mode 100644 index 00000000000..a1f75099f38 --- /dev/null +++ b/cmd/helm/testdata/output/search-regex.txt @@ -0,0 +1,2 @@ +NAME CHART VERSION APP VERSION DESCRIPTION +testing/alpine 0.2.0 2.3.4 Deploy a basic Alpine Linux pod diff --git a/cmd/helm/testdata/output/search-versions-constraint.txt b/cmd/helm/testdata/output/search-versions-constraint.txt new file mode 100644 index 00000000000..9fb22fe76b1 --- /dev/null +++ b/cmd/helm/testdata/output/search-versions-constraint.txt @@ -0,0 +1,2 @@ +NAME CHART VERSION APP VERSION DESCRIPTION +testing/alpine 0.1.0 1.2.3 Deploy a basic Alpine Linux pod diff --git a/cmd/helm/testdata/output/status-comp.txt b/cmd/helm/testdata/output/status-comp.txt new file mode 100644 index 00000000000..4f56ab30a30 --- /dev/null +++ b/cmd/helm/testdata/output/status-comp.txt @@ -0,0 +1,4 @@ +aramis Aramis-chart-0.0.0 -> uninstalled +athos Athos-chart-1.2.3 -> deployed +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/status-with-desc.txt b/cmd/helm/testdata/output/status-with-desc.txt new file mode 100644 index 00000000000..c681fe3ec46 --- /dev/null +++ b/cmd/helm/testdata/output/status-with-desc.txt @@ -0,0 +1,7 @@ +NAME: flummoxed-chickadee +LAST DEPLOYED: Sat Jan 16 00:00:00 2016 +NAMESPACE: default +STATUS: deployed +REVISION: 0 +DESCRIPTION: Mock description +TEST SUITE: None diff --git a/cmd/helm/testdata/output/status-with-notes.txt b/cmd/helm/testdata/output/status-with-notes.txt new file mode 100644 index 00000000000..e992ce91e2b --- /dev/null +++ b/cmd/helm/testdata/output/status-with-notes.txt @@ -0,0 +1,8 @@ +NAME: flummoxed-chickadee +LAST DEPLOYED: Sat Jan 16 00:00:00 2016 +NAMESPACE: default +STATUS: deployed +REVISION: 0 +TEST SUITE: None +NOTES: +release notes diff --git a/cmd/helm/testdata/output/status-with-test-suite.txt b/cmd/helm/testdata/output/status-with-test-suite.txt new file mode 100644 index 00000000000..58c67e10309 --- /dev/null +++ b/cmd/helm/testdata/output/status-with-test-suite.txt @@ -0,0 +1,13 @@ +NAME: flummoxed-chickadee +LAST DEPLOYED: Sat Jan 16 00:00:00 2016 +NAMESPACE: default +STATUS: deployed +REVISION: 0 +TEST SUITE: passing-test +Last Started: Mon Jan 2 15:04:05 2006 +Last Completed: Mon Jan 2 15:04:07 2006 +Phase: Succeeded +TEST SUITE: failing-test +Last Started: Mon Jan 2 15:10:05 2006 +Last Completed: Mon Jan 2 15:10:07 2006 +Phase: Failed diff --git a/cmd/helm/testdata/output/status-wrong-args-comp.txt b/cmd/helm/testdata/output/status-wrong-args-comp.txt new file mode 100644 index 00000000000..8d9fad576c0 --- /dev/null +++ b/cmd/helm/testdata/output/status-wrong-args-comp.txt @@ -0,0 +1,2 @@ +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/status.json b/cmd/helm/testdata/output/status.json new file mode 100644 index 00000000000..4b499c93521 --- /dev/null +++ b/cmd/helm/testdata/output/status.json @@ -0,0 +1 @@ +{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed","notes":"release notes"},"namespace":"default"} diff --git a/cmd/helm/testdata/output/status.txt b/cmd/helm/testdata/output/status.txt new file mode 100644 index 00000000000..a326c3db036 --- /dev/null +++ b/cmd/helm/testdata/output/status.txt @@ -0,0 +1,6 @@ +NAME: flummoxed-chickadee +LAST DEPLOYED: Sat Jan 16 00:00:00 2016 +NAMESPACE: default +STATUS: deployed +REVISION: 0 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/subchart-schema-cli-negative.txt b/cmd/helm/testdata/output/subchart-schema-cli-negative.txt new file mode 100644 index 00000000000..c0883a8e862 --- /dev/null +++ b/cmd/helm/testdata/output/subchart-schema-cli-negative.txt @@ -0,0 +1,4 @@ +Error: values don't meet the specifications of the schema(s) in the following chart(s): +subchart-with-schema: +- age: Must be greater than or equal to 0 + diff --git a/cmd/helm/testdata/output/subchart-schema-cli.txt b/cmd/helm/testdata/output/subchart-schema-cli.txt new file mode 100644 index 00000000000..22a94b3f491 --- /dev/null +++ b/cmd/helm/testdata/output/subchart-schema-cli.txt @@ -0,0 +1,6 @@ +NAME: schema +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/subchart-schema-negative.txt b/cmd/helm/testdata/output/subchart-schema-negative.txt new file mode 100644 index 00000000000..5a84170fd16 --- /dev/null +++ b/cmd/helm/testdata/output/subchart-schema-negative.txt @@ -0,0 +1,6 @@ +Error: values don't meet the specifications of the schema(s) in the following chart(s): +chart-without-schema: +- (root): lastname is required +subchart-with-schema: +- (root): age is required + diff --git a/cmd/helm/testdata/output/template-chart-with-template-lib-archive-dep.txt b/cmd/helm/testdata/output/template-chart-with-template-lib-archive-dep.txt new file mode 100644 index 00000000000..dc1aa29072d --- /dev/null +++ b/cmd/helm/testdata/output/template-chart-with-template-lib-archive-dep.txt @@ -0,0 +1,61 @@ +--- +# Source: chart-with-template-lib-archive-dep/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + labels: + app: chart-with-template-lib-archive-dep + chart: chart-with-template-lib-archive-dep-0.1.0 + heritage: Helm + release: RELEASE-NAME + name: release-name-chart-with-template-lib-archive-dep +spec: + ports: + - name: http + port: 80 + targetPort: http + selector: + app: chart-with-template-lib-archive-dep + release: RELEASE-NAME + type: ClusterIP +--- +# Source: chart-with-template-lib-archive-dep/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: RELEASE-NAME-chart-with-template-lib-archive-dep + labels: + app: chart-with-template-lib-archive-dep + chart: chart-with-template-lib-archive-dep-0.1.0 + release: RELEASE-NAME + heritage: Helm +spec: + replicas: 1 + selector: + matchLabels: + app: chart-with-template-lib-archive-dep + release: RELEASE-NAME + template: + metadata: + labels: + app: chart-with-template-lib-archive-dep + release: RELEASE-NAME + spec: + containers: + - name: chart-with-template-lib-archive-dep + image: "nginx:stable" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {} diff --git a/cmd/helm/testdata/output/template-chart-with-template-lib-dep.txt b/cmd/helm/testdata/output/template-chart-with-template-lib-dep.txt new file mode 100644 index 00000000000..12adeb28b63 --- /dev/null +++ b/cmd/helm/testdata/output/template-chart-with-template-lib-dep.txt @@ -0,0 +1,61 @@ +--- +# Source: chart-with-template-lib-dep/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + labels: + app: chart-with-template-lib-dep + chart: chart-with-template-lib-dep-0.1.0 + heritage: Helm + release: RELEASE-NAME + name: release-name-chart-with-template-lib-dep +spec: + ports: + - name: http + port: 80 + targetPort: http + selector: + app: chart-with-template-lib-dep + release: RELEASE-NAME + type: ClusterIP +--- +# Source: chart-with-template-lib-dep/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: RELEASE-NAME-chart-with-template-lib-dep + labels: + app: chart-with-template-lib-dep + chart: chart-with-template-lib-dep-0.1.0 + release: RELEASE-NAME + heritage: Helm +spec: + replicas: 1 + selector: + matchLabels: + app: chart-with-template-lib-dep + release: RELEASE-NAME + template: + metadata: + labels: + app: chart-with-template-lib-dep + release: RELEASE-NAME + spec: + containers: + - name: chart-with-template-lib-dep + image: "nginx:stable" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {} diff --git a/cmd/helm/testdata/output/template-lib-chart.txt b/cmd/helm/testdata/output/template-lib-chart.txt new file mode 100644 index 00000000000..d8a3bf275ac --- /dev/null +++ b/cmd/helm/testdata/output/template-lib-chart.txt @@ -0,0 +1 @@ +Error: validation: chart.metadata.type must be application or library diff --git a/cmd/helm/testdata/output/template-name-template.txt b/cmd/helm/testdata/output/template-name-template.txt new file mode 100644 index 00000000000..b9e7cbbe486 --- /dev/null +++ b/cmd/helm/testdata/output/template-name-template.txt @@ -0,0 +1,114 @@ +--- +# Source: subchart/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart-sa +--- +# Source: subchart/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","list","watch"] +--- +# Source: subchart/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart-role +subjects: +- kind: ServiceAccount + name: subchart-sa + namespace: default +--- +# Source: subchart/charts/subcharta/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subcharta + labels: + helm.sh/chart: "subcharta-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subcharta +--- +# Source: subchart/charts/subchartb/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchartb + labels: + helm.sh/chart: "subchartb-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchartb +--- +# Source: subchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchart + labels: + helm.sh/chart: "subchart-0.1.0" + app.kubernetes.io/instance: "foobar-YWJj-baz" + kube-version/major: "1" + kube-version/minor: "20" + kube-version/version: "v1.20.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchart +--- +# Source: subchart/templates/tests/test-config.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: "foobar-YWJj-baz-testconfig" + annotations: + "helm.sh/hook": test +data: + message: Hello World +--- +# Source: subchart/templates/tests/test-nothing.yaml +apiVersion: v1 +kind: Pod +metadata: + name: "foobar-YWJj-baz-test" + annotations: + "helm.sh/hook": test +spec: + containers: + - name: test + image: "alpine:latest" + envFrom: + - configMapRef: + name: "foobar-YWJj-baz-testconfig" + command: + - echo + - "$message" + restartPolicy: Never diff --git a/cmd/helm/testdata/output/template-no-args.txt b/cmd/helm/testdata/output/template-no-args.txt new file mode 100644 index 00000000000..f72f2b8cf62 --- /dev/null +++ b/cmd/helm/testdata/output/template-no-args.txt @@ -0,0 +1,3 @@ +Error: "helm template" requires at least 1 argument + +Usage: helm template [NAME] [CHART] [flags] diff --git a/cmd/helm/testdata/output/template-set.txt b/cmd/helm/testdata/output/template-set.txt new file mode 100644 index 00000000000..177d8e58c24 --- /dev/null +++ b/cmd/helm/testdata/output/template-set.txt @@ -0,0 +1,114 @@ +--- +# Source: subchart/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart-sa +--- +# Source: subchart/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","list","watch"] +--- +# Source: subchart/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart-role +subjects: +- kind: ServiceAccount + name: subchart-sa + namespace: default +--- +# Source: subchart/charts/subcharta/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subcharta + labels: + helm.sh/chart: "subcharta-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subcharta +--- +# Source: subchart/charts/subchartb/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchartb + labels: + helm.sh/chart: "subchartb-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchartb +--- +# Source: subchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchart + labels: + helm.sh/chart: "subchart-0.1.0" + app.kubernetes.io/instance: "RELEASE-NAME" + kube-version/major: "1" + kube-version/minor: "20" + kube-version/version: "v1.20.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subchart +--- +# Source: subchart/templates/tests/test-config.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: "RELEASE-NAME-testconfig" + annotations: + "helm.sh/hook": test +data: + message: Hello World +--- +# Source: subchart/templates/tests/test-nothing.yaml +apiVersion: v1 +kind: Pod +metadata: + name: "RELEASE-NAME-test" + annotations: + "helm.sh/hook": test +spec: + containers: + - name: test + image: "alpine:latest" + envFrom: + - configMapRef: + name: "RELEASE-NAME-testconfig" + command: + - echo + - "$message" + restartPolicy: Never diff --git a/cmd/helm/testdata/output/template-show-only-glob.txt b/cmd/helm/testdata/output/template-show-only-glob.txt new file mode 100644 index 00000000000..b2d2b1c2d1f --- /dev/null +++ b/cmd/helm/testdata/output/template-show-only-glob.txt @@ -0,0 +1,24 @@ +--- +# Source: subchart/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","list","watch"] +--- +# Source: subchart/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart-role +subjects: +- kind: ServiceAccount + name: subchart-sa + namespace: default diff --git a/cmd/helm/testdata/output/template-show-only-multiple.txt b/cmd/helm/testdata/output/template-show-only-multiple.txt new file mode 100644 index 00000000000..20b6bebed07 --- /dev/null +++ b/cmd/helm/testdata/output/template-show-only-multiple.txt @@ -0,0 +1,39 @@ +--- +# Source: subchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchart + labels: + helm.sh/chart: "subchart-0.1.0" + app.kubernetes.io/instance: "RELEASE-NAME" + kube-version/major: "1" + kube-version/minor: "20" + kube-version/version: "v1.20.0" + kube-api-version/test: v1 +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchart +--- +# Source: subchart/charts/subcharta/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subcharta + labels: + helm.sh/chart: "subcharta-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subcharta diff --git a/cmd/helm/testdata/output/template-show-only-one.txt b/cmd/helm/testdata/output/template-show-only-one.txt new file mode 100644 index 00000000000..f3aedb55d81 --- /dev/null +++ b/cmd/helm/testdata/output/template-show-only-one.txt @@ -0,0 +1,22 @@ +--- +# Source: subchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchart + labels: + helm.sh/chart: "subchart-0.1.0" + app.kubernetes.io/instance: "RELEASE-NAME" + kube-version/major: "1" + kube-version/minor: "20" + kube-version/version: "v1.20.0" + kube-api-version/test: v1 +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchart diff --git a/cmd/helm/testdata/output/template-skip-tests.txt b/cmd/helm/testdata/output/template-skip-tests.txt new file mode 100644 index 00000000000..6e657e50b6f --- /dev/null +++ b/cmd/helm/testdata/output/template-skip-tests.txt @@ -0,0 +1,86 @@ +--- +# Source: subchart/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart-sa +--- +# Source: subchart/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","list","watch"] +--- +# Source: subchart/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart-role +subjects: +- kind: ServiceAccount + name: subchart-sa + namespace: default +--- +# Source: subchart/charts/subcharta/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subcharta + labels: + helm.sh/chart: "subcharta-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subcharta +--- +# Source: subchart/charts/subchartb/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchartb + labels: + helm.sh/chart: "subchartb-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchartb +--- +# Source: subchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchart + labels: + helm.sh/chart: "subchart-0.1.0" + app.kubernetes.io/instance: "RELEASE-NAME" + kube-version/major: "1" + kube-version/minor: "20" + kube-version/version: "v1.20.0" + kube-api-version/test: v1 +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchart diff --git a/cmd/helm/testdata/output/template-values-files.txt b/cmd/helm/testdata/output/template-values-files.txt new file mode 100644 index 00000000000..177d8e58c24 --- /dev/null +++ b/cmd/helm/testdata/output/template-values-files.txt @@ -0,0 +1,114 @@ +--- +# Source: subchart/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart-sa +--- +# Source: subchart/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","list","watch"] +--- +# Source: subchart/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart-role +subjects: +- kind: ServiceAccount + name: subchart-sa + namespace: default +--- +# Source: subchart/charts/subcharta/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subcharta + labels: + helm.sh/chart: "subcharta-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subcharta +--- +# Source: subchart/charts/subchartb/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchartb + labels: + helm.sh/chart: "subchartb-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchartb +--- +# Source: subchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchart + labels: + helm.sh/chart: "subchart-0.1.0" + app.kubernetes.io/instance: "RELEASE-NAME" + kube-version/major: "1" + kube-version/minor: "20" + kube-version/version: "v1.20.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subchart +--- +# Source: subchart/templates/tests/test-config.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: "RELEASE-NAME-testconfig" + annotations: + "helm.sh/hook": test +data: + message: Hello World +--- +# Source: subchart/templates/tests/test-nothing.yaml +apiVersion: v1 +kind: Pod +metadata: + name: "RELEASE-NAME-test" + annotations: + "helm.sh/hook": test +spec: + containers: + - name: test + image: "alpine:latest" + envFrom: + - configMapRef: + name: "RELEASE-NAME-testconfig" + command: + - echo + - "$message" + restartPolicy: Never diff --git a/cmd/helm/testdata/output/template-with-api-version.txt b/cmd/helm/testdata/output/template-with-api-version.txt new file mode 100644 index 00000000000..4b2d4ee848e --- /dev/null +++ b/cmd/helm/testdata/output/template-with-api-version.txt @@ -0,0 +1,115 @@ +--- +# Source: subchart/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart-sa +--- +# Source: subchart/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","list","watch"] +--- +# Source: subchart/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart-role +subjects: +- kind: ServiceAccount + name: subchart-sa + namespace: default +--- +# Source: subchart/charts/subcharta/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subcharta + labels: + helm.sh/chart: "subcharta-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subcharta +--- +# Source: subchart/charts/subchartb/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchartb + labels: + helm.sh/chart: "subchartb-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchartb +--- +# Source: subchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchart + labels: + helm.sh/chart: "subchart-0.1.0" + app.kubernetes.io/instance: "RELEASE-NAME" + kube-version/major: "1" + kube-version/minor: "20" + kube-version/version: "v1.20.0" + kube-api-version/test: v1 +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchart +--- +# Source: subchart/templates/tests/test-config.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: "RELEASE-NAME-testconfig" + annotations: + "helm.sh/hook": test +data: + message: Hello World +--- +# Source: subchart/templates/tests/test-nothing.yaml +apiVersion: v1 +kind: Pod +metadata: + name: "RELEASE-NAME-test" + annotations: + "helm.sh/hook": test +spec: + containers: + - name: test + image: "alpine:latest" + envFrom: + - configMapRef: + name: "RELEASE-NAME-testconfig" + command: + - echo + - "$message" + restartPolicy: Never diff --git a/cmd/helm/testdata/output/template-with-crds.txt b/cmd/helm/testdata/output/template-with-crds.txt new file mode 100644 index 00000000000..fe8e24520f3 --- /dev/null +++ b/cmd/helm/testdata/output/template-with-crds.txt @@ -0,0 +1,132 @@ +--- +# Source: crds/crdA.yaml +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: testcrds.testcrdgroups.example.com +spec: + group: testcrdgroups.example.com + version: v1alpha1 + names: + kind: TestCRD + listKind: TestCRDList + plural: testcrds + shortNames: + - tc + singular: authconfig + +--- +# Source: subchart/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart-sa +--- +# Source: subchart/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","list","watch"] +--- +# Source: subchart/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart-role +subjects: +- kind: ServiceAccount + name: subchart-sa + namespace: default +--- +# Source: subchart/charts/subcharta/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subcharta + labels: + helm.sh/chart: "subcharta-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subcharta +--- +# Source: subchart/charts/subchartb/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchartb + labels: + helm.sh/chart: "subchartb-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchartb +--- +# Source: subchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchart + labels: + helm.sh/chart: "subchart-0.1.0" + app.kubernetes.io/instance: "RELEASE-NAME" + kube-version/major: "1" + kube-version/minor: "20" + kube-version/version: "v1.20.0" + kube-api-version/test: v1 +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchart +--- +# Source: subchart/templates/tests/test-config.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: "RELEASE-NAME-testconfig" + annotations: + "helm.sh/hook": test +data: + message: Hello World +--- +# Source: subchart/templates/tests/test-nothing.yaml +apiVersion: v1 +kind: Pod +metadata: + name: "RELEASE-NAME-test" + annotations: + "helm.sh/hook": test +spec: + containers: + - name: test + image: "alpine:latest" + envFrom: + - configMapRef: + name: "RELEASE-NAME-testconfig" + command: + - echo + - "$message" + restartPolicy: Never diff --git a/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt b/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt new file mode 100644 index 00000000000..c1f51185ce4 --- /dev/null +++ b/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt @@ -0,0 +1,13 @@ +--- +# Source: chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml +apiVersion: v1 +kind: Pod +metadata: + name: "RELEASE-NAME-my-alpine" +spec: + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] +invalid +Error: YAML parse error on chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml: error converting YAML to JSON: yaml: line 11: could not find expected ':' diff --git a/cmd/helm/testdata/output/template-with-invalid-yaml.txt b/cmd/helm/testdata/output/template-with-invalid-yaml.txt new file mode 100644 index 00000000000..687227b9085 --- /dev/null +++ b/cmd/helm/testdata/output/template-with-invalid-yaml.txt @@ -0,0 +1,3 @@ +Error: YAML parse error on chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml: error converting YAML to JSON: yaml: line 11: could not find expected ':' + +Use --debug flag to render out invalid YAML diff --git a/cmd/helm/testdata/output/template.txt b/cmd/helm/testdata/output/template.txt new file mode 100644 index 00000000000..4146a0749bd --- /dev/null +++ b/cmd/helm/testdata/output/template.txt @@ -0,0 +1,114 @@ +--- +# Source: subchart/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart-sa +--- +# Source: subchart/templates/subdir/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: subchart-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","list","watch"] +--- +# Source: subchart/templates/subdir/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: subchart-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: subchart-role +subjects: +- kind: ServiceAccount + name: subchart-sa + namespace: default +--- +# Source: subchart/charts/subcharta/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subcharta + labels: + helm.sh/chart: "subcharta-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subcharta +--- +# Source: subchart/charts/subchartb/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchartb + labels: + helm.sh/chart: "subchartb-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchartb +--- +# Source: subchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchart + labels: + helm.sh/chart: "subchart-0.1.0" + app.kubernetes.io/instance: "RELEASE-NAME" + kube-version/major: "1" + kube-version/minor: "20" + kube-version/version: "v1.20.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchart +--- +# Source: subchart/templates/tests/test-config.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: "RELEASE-NAME-testconfig" + annotations: + "helm.sh/hook": test +data: + message: Hello World +--- +# Source: subchart/templates/tests/test-nothing.yaml +apiVersion: v1 +kind: Pod +metadata: + name: "RELEASE-NAME-test" + annotations: + "helm.sh/hook": test +spec: + containers: + - name: test + image: "alpine:latest" + envFrom: + - configMapRef: + name: "RELEASE-NAME-testconfig" + command: + - echo + - "$message" + restartPolicy: Never diff --git a/cmd/helm/testdata/output/uninstall-keep-history.txt b/cmd/helm/testdata/output/uninstall-keep-history.txt new file mode 100644 index 00000000000..f5454b88d7b --- /dev/null +++ b/cmd/helm/testdata/output/uninstall-keep-history.txt @@ -0,0 +1 @@ +release "aeneas" uninstalled diff --git a/cmd/helm/testdata/output/uninstall-multiple.txt b/cmd/helm/testdata/output/uninstall-multiple.txt new file mode 100644 index 00000000000..ee1c67d2f16 --- /dev/null +++ b/cmd/helm/testdata/output/uninstall-multiple.txt @@ -0,0 +1,2 @@ +release "aeneas" uninstalled +release "aeneas2" uninstalled diff --git a/cmd/helm/testdata/output/uninstall-no-args.txt b/cmd/helm/testdata/output/uninstall-no-args.txt new file mode 100644 index 00000000000..fc01a75b954 --- /dev/null +++ b/cmd/helm/testdata/output/uninstall-no-args.txt @@ -0,0 +1,3 @@ +Error: "helm uninstall" requires at least 1 argument + +Usage: helm uninstall RELEASE_NAME [...] [flags] diff --git a/cmd/helm/testdata/output/uninstall-no-hooks.txt b/cmd/helm/testdata/output/uninstall-no-hooks.txt new file mode 100644 index 00000000000..f5454b88d7b --- /dev/null +++ b/cmd/helm/testdata/output/uninstall-no-hooks.txt @@ -0,0 +1 @@ +release "aeneas" uninstalled diff --git a/cmd/helm/testdata/output/uninstall-timeout.txt b/cmd/helm/testdata/output/uninstall-timeout.txt new file mode 100644 index 00000000000..f5454b88d7b --- /dev/null +++ b/cmd/helm/testdata/output/uninstall-timeout.txt @@ -0,0 +1 @@ +release "aeneas" uninstalled diff --git a/cmd/helm/testdata/output/uninstall.txt b/cmd/helm/testdata/output/uninstall.txt new file mode 100644 index 00000000000..f5454b88d7b --- /dev/null +++ b/cmd/helm/testdata/output/uninstall.txt @@ -0,0 +1 @@ +release "aeneas" uninstalled diff --git a/cmd/helm/testdata/output/upgrade-with-bad-dependencies.txt b/cmd/helm/testdata/output/upgrade-with-bad-dependencies.txt new file mode 100644 index 00000000000..6dddc7344d8 --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-bad-dependencies.txt @@ -0,0 +1 @@ +Error: cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator diff --git a/cmd/helm/testdata/output/upgrade-with-bad-or-missing-existing-release.txt b/cmd/helm/testdata/output/upgrade-with-bad-or-missing-existing-release.txt new file mode 100644 index 00000000000..8f24574a68d --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-bad-or-missing-existing-release.txt @@ -0,0 +1 @@ +Error: UPGRADE FAILED: "funny-bunny" has no deployed releases diff --git a/cmd/helm/testdata/output/upgrade-with-install-timeout.txt b/cmd/helm/testdata/output/upgrade-with-install-timeout.txt new file mode 100644 index 00000000000..5d8d3a4ea94 --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-install-timeout.txt @@ -0,0 +1,7 @@ +Release "crazy-bunny" has been upgraded. Happy Helming! +NAME: crazy-bunny +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 2 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/upgrade-with-install.txt b/cmd/helm/testdata/output/upgrade-with-install.txt new file mode 100644 index 00000000000..af61212bdd4 --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-install.txt @@ -0,0 +1,7 @@ +Release "zany-bunny" has been upgraded. Happy Helming! +NAME: zany-bunny +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 2 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/upgrade-with-missing-dependencies.txt b/cmd/helm/testdata/output/upgrade-with-missing-dependencies.txt new file mode 100644 index 00000000000..de62e1d2aef --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-missing-dependencies.txt @@ -0,0 +1 @@ +Error: found in Chart.yaml, but missing in charts/ directory: reqsubchart2 diff --git a/cmd/helm/testdata/output/upgrade-with-pending-install.txt b/cmd/helm/testdata/output/upgrade-with-pending-install.txt new file mode 100644 index 00000000000..57a8e787343 --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-pending-install.txt @@ -0,0 +1 @@ +Error: UPGRADE FAILED: another operation (install/upgrade/rollback) is in progress diff --git a/cmd/helm/testdata/output/upgrade-with-reset-values.txt b/cmd/helm/testdata/output/upgrade-with-reset-values.txt new file mode 100644 index 00000000000..01f1c0ac8b4 --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-reset-values.txt @@ -0,0 +1,7 @@ +Release "funny-bunny" has been upgraded. Happy Helming! +NAME: funny-bunny +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 5 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/upgrade-with-reset-values2.txt b/cmd/helm/testdata/output/upgrade-with-reset-values2.txt new file mode 100644 index 00000000000..fdd1d2db7da --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-reset-values2.txt @@ -0,0 +1,7 @@ +Release "funny-bunny" has been upgraded. Happy Helming! +NAME: funny-bunny +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 6 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/upgrade-with-timeout.txt b/cmd/helm/testdata/output/upgrade-with-timeout.txt new file mode 100644 index 00000000000..be3a4236813 --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-timeout.txt @@ -0,0 +1,7 @@ +Release "funny-bunny" has been upgraded. Happy Helming! +NAME: funny-bunny +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 4 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/upgrade-with-wait-for-jobs.txt b/cmd/helm/testdata/output/upgrade-with-wait-for-jobs.txt new file mode 100644 index 00000000000..500d07a11ab --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-wait-for-jobs.txt @@ -0,0 +1,7 @@ +Release "crazy-bunny" has been upgraded. Happy Helming! +NAME: crazy-bunny +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 3 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/upgrade-with-wait.txt b/cmd/helm/testdata/output/upgrade-with-wait.txt new file mode 100644 index 00000000000..500d07a11ab --- /dev/null +++ b/cmd/helm/testdata/output/upgrade-with-wait.txt @@ -0,0 +1,7 @@ +Release "crazy-bunny" has been upgraded. Happy Helming! +NAME: crazy-bunny +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 3 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/upgrade.txt b/cmd/helm/testdata/output/upgrade.txt new file mode 100644 index 00000000000..bea42db54db --- /dev/null +++ b/cmd/helm/testdata/output/upgrade.txt @@ -0,0 +1,7 @@ +Release "funny-bunny" has been upgraded. Happy Helming! +NAME: funny-bunny +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 3 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/values.json b/cmd/helm/testdata/output/values.json new file mode 100644 index 00000000000..ea830862711 --- /dev/null +++ b/cmd/helm/testdata/output/values.json @@ -0,0 +1 @@ +{"name":"value"} diff --git a/cmd/helm/testdata/output/values.yaml b/cmd/helm/testdata/output/values.yaml new file mode 100644 index 00000000000..54ab03c93bd --- /dev/null +++ b/cmd/helm/testdata/output/values.yaml @@ -0,0 +1 @@ +name: value diff --git a/cmd/helm/testdata/output/version-client-shorthand.txt b/cmd/helm/testdata/output/version-client-shorthand.txt new file mode 100644 index 00000000000..9dc0a8cfa3f --- /dev/null +++ b/cmd/helm/testdata/output/version-client-shorthand.txt @@ -0,0 +1 @@ +version.BuildInfo{Version:"v3.5", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/output/version-client.txt b/cmd/helm/testdata/output/version-client.txt new file mode 100644 index 00000000000..9dc0a8cfa3f --- /dev/null +++ b/cmd/helm/testdata/output/version-client.txt @@ -0,0 +1 @@ +version.BuildInfo{Version:"v3.5", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/output/version-comp.txt b/cmd/helm/testdata/output/version-comp.txt new file mode 100644 index 00000000000..5b0556cf5f6 --- /dev/null +++ b/cmd/helm/testdata/output/version-comp.txt @@ -0,0 +1,5 @@ +0.3.0-rc.1 App: 3.0.0, Created: November 12, 2020 +0.2.0 App: 2.3.4, Created: July 9, 2018 +0.1.0 App: 1.2.3, Created: June 27, 2018 (deprecated) +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/version-invalid-comp.txt b/cmd/helm/testdata/output/version-invalid-comp.txt new file mode 100644 index 00000000000..8d9fad576c0 --- /dev/null +++ b/cmd/helm/testdata/output/version-invalid-comp.txt @@ -0,0 +1,2 @@ +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/version-short.txt b/cmd/helm/testdata/output/version-short.txt new file mode 100644 index 00000000000..3c81e0c56cf --- /dev/null +++ b/cmd/helm/testdata/output/version-short.txt @@ -0,0 +1 @@ +v3.5 diff --git a/cmd/helm/testdata/output/version-template.txt b/cmd/helm/testdata/output/version-template.txt new file mode 100644 index 00000000000..68945e7a48d --- /dev/null +++ b/cmd/helm/testdata/output/version-template.txt @@ -0,0 +1 @@ +Version: v3.5 \ No newline at end of file diff --git a/cmd/helm/testdata/output/version.txt b/cmd/helm/testdata/output/version.txt new file mode 100644 index 00000000000..9dc0a8cfa3f --- /dev/null +++ b/cmd/helm/testdata/output/version.txt @@ -0,0 +1 @@ +version.BuildInfo{Version:"v3.5", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/plugins.yaml b/cmd/helm/testdata/plugins.yaml new file mode 100644 index 00000000000..69086973e5b --- /dev/null +++ b/cmd/helm/testdata/plugins.yaml @@ -0,0 +1,3 @@ +plugins: +- name: testplugin + url: testdata/testplugin diff --git a/cmd/helm/testdata/repositories.yaml b/cmd/helm/testdata/repositories.yaml index 047527ef46e..423b9f1958b 100644 --- a/cmd/helm/testdata/repositories.yaml +++ b/cmd/helm/testdata/repositories.yaml @@ -1,6 +1,4 @@ apiVersion: v1 repositories: - name: charts - url: "https://kubernetes-charts.storage.googleapis.com" - - name: local - url: "http://localhost:8879/charts" + url: "https://charts.helm.sh/stable" diff --git a/cmd/helm/testdata/testcache/foobar-index.yaml b/cmd/helm/testdata/testcache/foobar-index.yaml deleted file mode 100644 index b8083fd06a4..00000000000 --- a/cmd/helm/testdata/testcache/foobar-index.yaml +++ /dev/null @@ -1,24 +0,0 @@ -foobar-0.1.0: - url: https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz - name: foobar - removed: false - chartfile: - name: foobar - description: string - version: 0.1.0 - home: https://github.com/foo - keywords: - - dummy - - hokey -oddness-1.2.3: - url: https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz - name: oddness - removed: false - chartfile: - name: oddness - description: string - version: 1.2.3 - home: https://github.com/something - keywords: - - duck - - sumtin diff --git a/cmd/helm/testdata/testcache/local-index.yaml b/cmd/helm/testdata/testcache/local-index.yaml deleted file mode 100644 index a589e2321f5..00000000000 --- a/cmd/helm/testdata/testcache/local-index.yaml +++ /dev/null @@ -1,27 +0,0 @@ -nginx-0.1.0: - url: https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz - name: nginx - removed: false - chartfile: - name: nginx - description: string - version: 0.1.0 - home: https://github.com/something - keywords: - - popular - - web server - - proxy -alpine-1.0.0: - url: https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz - name: alpine - removed: false - chartfile: - name: alpine - description: string - version: 1.0.0 - home: https://github.com/something - keywords: - - linux - - alpine - - small - - sumtin diff --git a/cmd/helm/testdata/testcharts/alpine/Chart.yaml b/cmd/helm/testdata/testcharts/alpine/Chart.yaml index 6fbb27f1811..1d6bad825b7 100644 --- a/cmd/helm/testdata/testcharts/alpine/Chart.yaml +++ b/cmd/helm/testdata/testcharts/alpine/Chart.yaml @@ -1,6 +1,8 @@ +apiVersion: v1 +appVersion: "3.9" description: Deploy a basic Alpine Linux pod -home: https://k8s.io/helm +home: https://helm.sh/helm name: alpine sources: -- https://github.com/kubernetes/helm +- https://github.com/helm/helm version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/alpine/README.md b/cmd/helm/testdata/testcharts/alpine/README.md index 3c32de5db6a..05d39dbbc44 100644 --- a/cmd/helm/testdata/testcharts/alpine/README.md +++ b/cmd/helm/testdata/testcharts/alpine/README.md @@ -1,4 +1,4 @@ -#Alpine: A simple Helm chart +# Alpine: A simple Helm chart Run a single pod of Alpine Linux. @@ -10,4 +10,4 @@ couple of parameters. The `values.yaml` file contains the default values for the `alpine-pod.yaml` template. -You can install this example using `helm install docs/examples/alpine`. +You can install this example using `helm install ./alpine`. diff --git a/cmd/helm/testdata/testcharts/alpine/templates/alpine-pod.yaml b/cmd/helm/testdata/testcharts/alpine/templates/alpine-pod.yaml index 424920782f5..a1a44e53f28 100644 --- a/cmd/helm/testdata/testcharts/alpine/templates/alpine-pod.yaml +++ b/cmd/helm/testdata/testcharts/alpine/templates/alpine-pod.yaml @@ -3,18 +3,18 @@ kind: Pod metadata: name: "{{.Release.Name}}-{{.Values.Name}}" labels: - # The "heritage" label is used to track which tool deployed a given chart. - # It is useful for admins who want to see what releases a particular tool - # is responsible for. - heritage: {{.Release.Service | quote }} - # The "release" convention makes it easy to tie a release to all of the - # Kubernetes resources that were created as part of that release. - release: {{.Release.Name | quote }} + # The "app.kubernetes.io/managed-by" label is used to track which tool + # deployed a given chart. It is useful for admins who want to see what + # releases a particular tool is responsible for. + app.kubernetes.io/managed-by: {{.Release.Service | quote }} + # The "app.kubernetes.io/instance" convention makes it easy to tie a release + # to all of the Kubernetes resources that were created as part of that + # release. + app.kubernetes.io/instance: {{.Release.Name | quote }} + app.kubernetes.io/version: {{ .Chart.AppVersion }} # This makes it easy to audit chart usage. - chart: "{{.Chart.Name}}-{{.Chart.Version}}" - values: {{.Values.test.Name}} - annotations: - "helm.sh/created": {{.Release.Time.Seconds | quote }} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" + values: {{.Values.Name}} 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. @@ -23,5 +23,5 @@ spec: restartPolicy: {{default "Never" .Values.restartPolicy}} containers: - name: waiter - image: "alpine:3.3" + image: "alpine:{{ .Chart.AppVersion }}" command: ["/bin/sleep","9000"] diff --git a/cmd/helm/testdata/testcharts/alpine/values.yaml b/cmd/helm/testdata/testcharts/alpine/values.yaml index 879d760f924..807e12aea65 100644 --- a/cmd/helm/testdata/testcharts/alpine/values.yaml +++ b/cmd/helm/testdata/testcharts/alpine/values.yaml @@ -1,2 +1 @@ -# The pod name Name: my-alpine diff --git a/cmd/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml b/cmd/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml index 02be4c013be..1f445ee1161 100644 --- a/cmd/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml +++ b/cmd/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml @@ -1,3 +1,8 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: chart-missing-deps version: 0.1.0 +dependencies: + - name: reqsubchart + version: 0.1.0 + repository: "https://example.com/charts" diff --git a/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml b/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml index c3813bc8c2f..356135537aa 100644 --- a/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml +++ b/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: reqsubchart version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-bad-requirements/requirements.yaml b/cmd/helm/testdata/testcharts/chart-bad-requirements/requirements.yaml deleted file mode 100644 index 10c4d6dcbf3..00000000000 --- a/cmd/helm/testdata/testcharts/chart-bad-requirements/requirements.yaml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: - - name: reqsubchart - version: 0.1.0 - repository: "https://example.com/charts" diff --git a/cmd/helm/testdata/testcharts/chart-bad-type/Chart.yaml b/cmd/helm/testdata/testcharts/chart-bad-type/Chart.yaml new file mode 100644 index 00000000000..e77b5afaa7a --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-bad-type/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +description: Deploy a basic Alpine Linux pod +home: https://helm.sh/helm +name: chart-bad-type +sources: + - https://github.com/helm/helm +version: 0.1.0 +type: foobar diff --git a/cmd/helm/testdata/testcharts/chart-bad-type/README.md b/cmd/helm/testdata/testcharts/chart-bad-type/README.md new file mode 100644 index 00000000000..fcf7ee01748 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-bad-type/README.md @@ -0,0 +1,13 @@ +#Alpine: A simple Helm chart + +Run a single pod of Alpine Linux. + +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.yaml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/cmd/helm/testdata/testcharts/chart-bad-type/extra_values.yaml b/cmd/helm/testdata/testcharts/chart-bad-type/extra_values.yaml new file mode 100644 index 00000000000..468bbacbc34 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-bad-type/extra_values.yaml @@ -0,0 +1,2 @@ +test: + Name: extra-values diff --git a/cmd/helm/testdata/testcharts/chart-bad-type/more_values.yaml b/cmd/helm/testdata/testcharts/chart-bad-type/more_values.yaml new file mode 100644 index 00000000000..3d21e1fed41 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-bad-type/more_values.yaml @@ -0,0 +1,2 @@ +test: + Name: more-values diff --git a/cmd/helm/testdata/testcharts/chart-bad-type/templates/alpine-pod.yaml b/cmd/helm/testdata/testcharts/chart-bad-type/templates/alpine-pod.yaml new file mode 100644 index 00000000000..a40ae32d744 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-bad-type/templates/alpine-pod.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{.Release.Name}}-{{.Values.Name}}" + labels: + # The "app.kubernetes.io/managed-by" label is used to track which tool + # deployed a given chart. It is useful for admins who want to see what + # releases a particular tool is responsible for. + app.kubernetes.io/managed-by: {{.Release.Service | quote }} + # The "release" convention makes it easy to tie a release to all of the + # Kubernetes resources that were created as part of that release. + app.kubernetes.io/instance: {{.Release.Name | quote }} + # This makes it easy to audit chart usage. + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" + values: {{.Values.test.Name}} +spec: + # This shows how to use a simple value. This will look for a passed-in value + # called restartPolicy. If it is not found, it will use the default value. + # {{default "Never" .restartPolicy}} is a slightly optimized version of the + # more conventional syntax: {{.restartPolicy | default "Never"}} + restartPolicy: {{default "Never" .Values.restartPolicy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/cmd/helm/testdata/testcharts/chart-bad-type/values.yaml b/cmd/helm/testdata/testcharts/chart-bad-type/values.yaml new file mode 100644 index 00000000000..807e12aea65 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-bad-type/values.yaml @@ -0,0 +1 @@ +Name: my-alpine diff --git a/cmd/helm/testdata/testcharts/chart-missing-deps/Chart.yaml b/cmd/helm/testdata/testcharts/chart-missing-deps/Chart.yaml index 02be4c013be..9605636db04 100644 --- a/cmd/helm/testdata/testcharts/chart-missing-deps/Chart.yaml +++ b/cmd/helm/testdata/testcharts/chart-missing-deps/Chart.yaml @@ -1,3 +1,11 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: chart-missing-deps version: 0.1.0 +dependencies: + - name: reqsubchart + version: 0.1.0 + repository: "https://example.com/charts" + - name: reqsubchart2 + version: 0.2.0 + repository: "https://example.com/charts" diff --git a/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml b/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml index c3813bc8c2f..356135537aa 100644 --- a/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml +++ b/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: reqsubchart version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-missing-deps/requirements.yaml b/cmd/helm/testdata/testcharts/chart-missing-deps/requirements.yaml deleted file mode 100644 index 4b0b8c2db81..00000000000 --- a/cmd/helm/testdata/testcharts/chart-missing-deps/requirements.yaml +++ /dev/null @@ -1,7 +0,0 @@ -dependencies: - - name: reqsubchart - version: 0.1.0 - repository: "https://example.com/charts" - - name: reqsubchart2 - version: 0.2.0 - repository: "https://example.com/charts" diff --git a/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/Chart.yaml new file mode 100644 index 00000000000..a575aa9f838 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: Chart with bad subcharts +name: chart-with-bad-subcharts +version: 0.0.1 diff --git a/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/Chart.yaml new file mode 100644 index 00000000000..a6754b24f04 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/Chart.yaml @@ -0,0 +1 @@ +description: Bad subchart diff --git a/pkg/downloader/testdata/helmhome/repository/local/index.yaml b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/values.yaml similarity index 100% rename from pkg/downloader/testdata/helmhome/repository/local/index.yaml rename to cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/values.yaml diff --git a/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/Chart.yaml new file mode 100644 index 00000000000..895433e3158 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: Good subchart +name: good-subchart +version: 0.0.1 \ No newline at end of file diff --git a/pkg/ignore/testdata/.joonix b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/values.yaml similarity index 100% rename from pkg/ignore/testdata/.joonix rename to cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/values.yaml diff --git a/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/requirements.yaml b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/requirements.yaml new file mode 100644 index 00000000000..de2fbb4dd41 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/requirements.yaml @@ -0,0 +1,5 @@ +dependencies: + - name: good-subchart + version: 0.0.1 + - name: bad-subchart + version: 0.0.1 \ No newline at end of file diff --git a/pkg/ignore/testdata/a.txt b/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/values.yaml similarity index 100% rename from pkg/ignore/testdata/a.txt rename to cmd/helm/testdata/testcharts/chart-with-bad-subcharts/values.yaml diff --git a/cmd/helm/testdata/testcharts/chart-with-lib-dep/.helmignore b/cmd/helm/testdata/testcharts/chart-with-lib-dep/.helmignore new file mode 100644 index 00000000000..f0c13194444 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-lib-dep/.helmignore @@ -0,0 +1,21 @@ +# 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 +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/cmd/helm/testdata/testcharts/chart-with-lib-dep/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-lib-dep/Chart.yaml new file mode 100644 index 00000000000..773cc9f32a3 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-lib-dep/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: chart-with-lib-dep +type: application +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-with-lib-dep/charts/common-0.0.5.tgz b/cmd/helm/testdata/testcharts/chart-with-lib-dep/charts/common-0.0.5.tgz new file mode 100644 index 00000000000..ca0a64ae314 Binary files /dev/null and b/cmd/helm/testdata/testcharts/chart-with-lib-dep/charts/common-0.0.5.tgz differ diff --git a/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/NOTES.txt new file mode 100644 index 00000000000..a758b797189 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "chart-with-lib-dep.fullname" . }}) + export NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "chart-with-lib-dep.fullname" . }}' + export SERVICE_IP=$(kubectl get svc {{ template "chart-with-lib-dep.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods -l "app={{ template "chart-with-lib-dep.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:80 +{{- end }} diff --git a/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/_helpers.tpl b/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/_helpers.tpl new file mode 100644 index 00000000000..b8be8cad694 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "chart-with-lib-dep.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "chart-with-lib-dep.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "chart-with-lib-dep.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/deployment.yaml b/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/deployment.yaml new file mode 100644 index 00000000000..521fa59721b --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/deployment.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "chart-with-lib-dep.fullname" . }} + labels: + app.kubernetes.io/name: {{ template "chart-with-lib-dep.name" . }} + helm.sh/chart: {{ template "chart-with-lib-dep.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ template "chart-with-lib-dep.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ template "chart-with-lib-dep.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/ingress.yaml b/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/ingress.yaml new file mode 100644 index 00000000000..42afd087976 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/ingress.yaml @@ -0,0 +1,38 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "chart-with-lib-dep.fullname" . -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app.kubernetes.io/name: {{ template "chart-with-lib-dep.name" . }} + helm.sh/chart: {{ template "chart-with-lib-dep.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/service.yaml b/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/service.yaml new file mode 100644 index 00000000000..4c2b91a5a71 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/service.yaml @@ -0,0 +1,10 @@ +{{- template "common.service" (list . "mychart.service") -}} +{{- define "mychart.service" -}} +## Define overrides for your Service resource here, e.g. +# metadata: +# labels: +# custom: label +# spec: +# ports: +# - port: 8080 +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-lib-dep/values.yaml b/cmd/helm/testdata/testcharts/chart-with-lib-dep/values.yaml new file mode 100644 index 00000000000..a0cc07e9e1b --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-lib-dep/values.yaml @@ -0,0 +1,48 @@ +# Default values for chart-with-lib-dep. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent + +nameOverride: "" +fullnameOverride: "" + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + path: / + hosts: + - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/cmd/helm/testdata/testcharts/chart-with-only-crds/.helmignore b/cmd/helm/testdata/testcharts/chart-with-only-crds/.helmignore new file mode 100644 index 00000000000..0e8a0eb36f4 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-only-crds/.helmignore @@ -0,0 +1,23 @@ +# 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 +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/cmd/helm/testdata/testcharts/chart-with-only-crds/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-only-crds/Chart.yaml new file mode 100644 index 00000000000..ec3497670ea --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-only-crds/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: crd-test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application and it is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/cmd/helm/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml b/cmd/helm/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml new file mode 100644 index 00000000000..1d7350f1df9 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml @@ -0,0 +1,19 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: tests.test.io +spec: + group: test.io + names: + kind: Test + listKind: TestList + plural: tests + singular: test + scope: Namespaced + versions: + - name : v1alpha2 + served: true + storage: true + - name : v1alpha1 + served: true + storage: false diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/Chart.yaml new file mode 100644 index 00000000000..4e24c2ebb6c --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +name: chart-without-schema +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/Chart.yaml new file mode 100644 index 00000000000..b5a77c5db76 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +name: subchart-with-schema +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/templates/empty.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/templates/empty.yaml new file mode 100644 index 00000000000..c80812f6e0a --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/templates/empty.yaml @@ -0,0 +1 @@ +# This file is intentionally blank diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.schema.json b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.schema.json new file mode 100644 index 00000000000..4ff791844bb --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Values", + "type": "object", + "properties": { + "age": { + "description": "Age", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "age" + ] +} diff --git a/pkg/ignore/testdata/cargo/a.txt b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.yaml similarity index 100% rename from pkg/ignore/testdata/cargo/a.txt rename to cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.yaml diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/templates/empty.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/templates/empty.yaml new file mode 100644 index 00000000000..c80812f6e0a --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/templates/empty.yaml @@ -0,0 +1 @@ +# This file is intentionally blank diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.schema.json b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.schema.json new file mode 100644 index 00000000000..f3094803842 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Values", + "type": "object", + "properties": { + "firstname": { + "description": "First name", + "type": "string" + }, + "lastname": { + "type": "string" + } + }, + "required": [ + "firstname", + "lastname" + ] +} diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.yaml new file mode 100644 index 00000000000..c9deafc006a --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.yaml @@ -0,0 +1 @@ +firstname: "John" diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-negative/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-negative/Chart.yaml new file mode 100644 index 00000000000..395d24f6aa0 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-negative/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +description: Empty testing chart +home: https://k8s.io/helm +name: empty +sources: +- https://github.com/kubernetes/helm +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-negative/templates/empty.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-negative/templates/empty.yaml new file mode 100644 index 00000000000..c80812f6e0a --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-negative/templates/empty.yaml @@ -0,0 +1 @@ +# This file is intentionally blank diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-negative/values.schema.json b/cmd/helm/testdata/testcharts/chart-with-schema-negative/values.schema.json new file mode 100644 index 00000000000..4df89bbe89f --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-negative/values.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "addresses": { + "description": "List of addresses", + "items": { + "properties": { + "city": { + "type": "string" + }, + "number": { + "type": "number" + }, + "street": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "age": { + "description": "Age", + "minimum": 0, + "type": "integer" + }, + "employmentInfo": { + "properties": { + "salary": { + "minimum": 0, + "type": "number" + }, + "title": { + "type": "string" + } + }, + "required": [ + "salary" + ], + "type": "object" + }, + "firstname": { + "description": "First name", + "type": "string" + }, + "lastname": { + "type": "string" + }, + "likesCoffee": { + "type": "boolean" + }, + "phoneNumbers": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "firstname", + "lastname", + "addresses", + "employmentInfo" + ], + "title": "Values", + "type": "object" +} diff --git a/cmd/helm/testdata/testcharts/chart-with-schema-negative/values.yaml b/cmd/helm/testdata/testcharts/chart-with-schema-negative/values.yaml new file mode 100644 index 00000000000..5a1250bff36 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema-negative/values.yaml @@ -0,0 +1,14 @@ +firstname: John +lastname: Doe +age: -5 +likesCoffee: true +addresses: + - city: Springfield + street: Main + number: 12345 + - city: New York + street: Broadway + number: 67890 +phoneNumbers: + - "(888) 888-8888" + - "(555) 555-5555" diff --git a/cmd/helm/testdata/testcharts/chart-with-schema/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-schema/Chart.yaml new file mode 100644 index 00000000000..395d24f6aa0 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +description: Empty testing chart +home: https://k8s.io/helm +name: empty +sources: +- https://github.com/kubernetes/helm +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-with-schema/extra-values.yaml b/cmd/helm/testdata/testcharts/chart-with-schema/extra-values.yaml new file mode 100644 index 00000000000..76c290c4f4c --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema/extra-values.yaml @@ -0,0 +1,2 @@ +age: -5 +employmentInfo: null diff --git a/cmd/helm/testdata/testcharts/chart-with-schema/templates/empty.yaml b/cmd/helm/testdata/testcharts/chart-with-schema/templates/empty.yaml new file mode 100644 index 00000000000..c80812f6e0a --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema/templates/empty.yaml @@ -0,0 +1 @@ +# This file is intentionally blank diff --git a/cmd/helm/testdata/testcharts/chart-with-schema/values.schema.json b/cmd/helm/testdata/testcharts/chart-with-schema/values.schema.json new file mode 100644 index 00000000000..4df89bbe89f --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema/values.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "addresses": { + "description": "List of addresses", + "items": { + "properties": { + "city": { + "type": "string" + }, + "number": { + "type": "number" + }, + "street": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "age": { + "description": "Age", + "minimum": 0, + "type": "integer" + }, + "employmentInfo": { + "properties": { + "salary": { + "minimum": 0, + "type": "number" + }, + "title": { + "type": "string" + } + }, + "required": [ + "salary" + ], + "type": "object" + }, + "firstname": { + "description": "First name", + "type": "string" + }, + "lastname": { + "type": "string" + }, + "likesCoffee": { + "type": "boolean" + }, + "phoneNumbers": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "firstname", + "lastname", + "addresses", + "employmentInfo" + ], + "title": "Values", + "type": "object" +} diff --git a/cmd/helm/testdata/testcharts/chart-with-schema/values.yaml b/cmd/helm/testdata/testcharts/chart-with-schema/values.yaml new file mode 100644 index 00000000000..042dea664b8 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-schema/values.yaml @@ -0,0 +1,17 @@ +firstname: John +lastname: Doe +age: 25 +likesCoffee: true +employmentInfo: + title: Software Developer + salary: 100000 +addresses: + - city: Springfield + street: Main + number: 12345 + - city: New York + street: Broadway + number: 67890 +phoneNumbers: + - "(888) 888-8888" + - "(555) 555-5555" diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-notes/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/Chart.yaml new file mode 100644 index 00000000000..90545a6a353 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +description: Chart with subchart notes +name: chart-with-subchart-notes +version: 0.0.1 +dependencies: + - name: subchart-with-notes + version: 0.0.1 diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/Chart.yaml new file mode 100644 index 00000000000..f0fead9ee33 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: Subchart with notes +name: subchart-with-notes +version: 0.0.1 diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/templates/NOTES.txt new file mode 100644 index 00000000000..1f61a294e2c --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/templates/NOTES.txt @@ -0,0 +1 @@ +SUBCHART NOTES diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-notes/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/templates/NOTES.txt new file mode 100644 index 00000000000..9e166d37024 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-notes/templates/NOTES.txt @@ -0,0 +1 @@ +PARENT NOTES diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.yaml new file mode 100644 index 00000000000..1bc23020036 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v2 +description: Chart with subchart that needs to be fetched +name: chart-with-subchart-update +version: 0.0.1 +dependencies: + - name: subchart-with-notes + version: 0.0.1 + repository: file://../chart-with-subchart-notes/charts diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/Chart.yaml new file mode 100644 index 00000000000..f0fead9ee33 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: Subchart with notes +name: subchart-with-notes +version: 0.0.1 diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/templates/NOTES.txt new file mode 100644 index 00000000000..1f61a294e2c --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/templates/NOTES.txt @@ -0,0 +1 @@ +SUBCHART NOTES diff --git a/cmd/helm/testdata/testcharts/chart-with-subchart-update/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-subchart-update/templates/NOTES.txt new file mode 100644 index 00000000000..9e166d37024 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-subchart-update/templates/NOTES.txt @@ -0,0 +1 @@ +PARENT NOTES diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/.helmignore b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/.helmignore new file mode 100644 index 00000000000..f0c13194444 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/.helmignore @@ -0,0 +1,21 @@ +# 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 +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/Chart.yaml new file mode 100644 index 00000000000..de53ce5e3a1 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: chart-with-template-lib-archive-dep +type: application +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/charts/common-0.0.5.tgz b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/charts/common-0.0.5.tgz new file mode 100644 index 00000000000..465517824e7 Binary files /dev/null and b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/charts/common-0.0.5.tgz differ diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/NOTES.txt new file mode 100644 index 00000000000..5c53ac03d05 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "chart-with-template-lib-archive-dep.fullname" . }}) + export NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "chart-with-template-lib-archive-dep.fullname" . }}' + export SERVICE_IP=$(kubectl get svc {{ template "chart-with-template-lib-archive-dep.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods -l "app={{ template "chart-with-template-lib-archive-dep.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:80 +{{- end }} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/_helpers.tpl b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/_helpers.tpl new file mode 100644 index 00000000000..76ca56b8187 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "chart-with-template-lib-archive-dep.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "chart-with-template-lib-archive-dep.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "chart-with-template-lib-archive-dep.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/deployment.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/deployment.yaml new file mode 100644 index 00000000000..a49572f4a0a --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/deployment.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "chart-with-template-lib-archive-dep.fullname" . }} + labels: + app: {{ template "chart-with-template-lib-archive-dep.name" . }} + chart: {{ template "chart-with-template-lib-archive-dep.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "chart-with-template-lib-archive-dep.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "chart-with-template-lib-archive-dep.name" . }} + release: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/ingress.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/ingress.yaml new file mode 100644 index 00000000000..d3325cf185c --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/ingress.yaml @@ -0,0 +1,38 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "chart-with-template-lib-archive-dep.fullname" . -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "chart-with-template-lib-archive-dep.name" . }} + chart: {{ template "chart-with-template-lib-archive-dep.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/service.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/service.yaml new file mode 100644 index 00000000000..bfcb080b45e --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/service.yaml @@ -0,0 +1,10 @@ +{{- template "common.service" (list . "chart-with-template-lib-archive-dep.service") -}} +{{- define "chart-with-template-lib-archive-dep.service" -}} +## Define overrides for your Service resource here, e.g. +# metadata: +# labels: +# custom: label +# spec: +# ports: +# - port: 8080 +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/values.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/values.yaml new file mode 100644 index 00000000000..b5474cbbdd6 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/values.yaml @@ -0,0 +1,48 @@ +# Default values for chart-with-template-lib-archive-dep. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent + +nameOverride: "" +fullnameOverride: "" + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + path: / + hosts: + - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/.helmignore b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/.helmignore new file mode 100644 index 00000000000..f0c13194444 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/.helmignore @@ -0,0 +1,21 @@ +# 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 +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/Chart.yaml new file mode 100644 index 00000000000..cf6fc390b0e --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: chart-with-template-lib-dep +type: application +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/.helmignore b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/.helmignore new file mode 100755 index 00000000000..f0c13194444 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/.helmignore @@ -0,0 +1,21 @@ +# 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 +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/Chart.yaml new file mode 100755 index 00000000000..ba14ca08905 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +appVersion: 0.0.5 +description: Common chartbuilding components and helpers +home: https://helm.sh +maintainers: +- email: technosophos@gmail.com + name: technosophos +- email: adnan@bitnami.com + name: prydonius +name: common +version: 0.0.5 +type: library diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/README.md b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/README.md new file mode 100755 index 00000000000..ca045947428 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/README.md @@ -0,0 +1,831 @@ +# Common: The Helm Helper Chart + +This chart is designed to make it easier for you to build and maintain Helm +charts. + +It provides utilities that reflect best practices of Kubernetes chart development, +making it faster for you to write charts. + +## Tips + +A few tips for working with Common: + +- Be careful when using functions that generate random data (like `common.fullname.unique`). + They may trigger unwanted upgrades or have other side effects. + +In this document, we use `RELEASE-NAME` as the name of the release. + +## Resource Kinds + +Kubernetes defines a variety of resource kinds, from `Secret` to `StatefulSet`. +We define some of the most common kinds in a way that lets you easily work with +them. + +The resource kind templates are designed to make it much faster for you to +define _basic_ versions of these resources. They allow you to extend and modify +just what you need, without having to copy around lots of boilerplate. + +To make use of these templates you must define a template that will extend the +base template (though it can be empty). The name of this template is then passed +to the base template, for example: + +```yaml +{{- template "common.service" (list . "mychart.service") -}} +{{- define "mychart.service" -}} +## Define overrides for your Service resource here, e.g. +# metadata: +# labels: +# custom: label +# spec: +# ports: +# - port: 8080 +{{- end -}} +``` + +Note that the `common.service` template defines two parameters: + + - The root context (usually `.`) + - A template name containing the service definition overrides + +A limitation of the Go template library is that a template can only take a +single argument. The `list` function is used to workaround this by constructing +a list or array of arguments that is passed to the template. + +The `common.service` template is responsible for rendering the templates with +the root context and merging any overrides. As you can see, this makes it very +easy to create a basic `Service` resource without having to copy around the +standard metadata and labels. + +Each implemented base resource is described in greater detail below. + +### `common.service` + +The `common.service` template creates a basic `Service` resource with the +following defaults: + +- Service type (ClusterIP, NodePort, LoadBalancer) made configurable by `.Values.service.type` +- Named port `http` configured on port 80 +- Selector set to `app: {{ template "common.name" }}, release: {{ .Release.Name | quote }}` to match the default used in the `Deployment` resource + +Example template: + +```yaml +{{- template "common.service" (list . "mychart.mail.service") -}} +{{- define "mychart.mail.service" -}} +metadata: + name: {{ template "common.fullname" . }}-mail # overrides the default name to add a suffix + labels: # appended to the labels section + protocol: mail +spec: + ports: # composes the `ports` section of the service definition. + - name: smtp + port: 25 + targetPort: 25 + - name: imaps + port: 993 + targetPort: 993 + selector: # this is appended to the default selector + protocol: mail +{{- end -}} +--- +{{ template "common.service" (list . "mychart.web.service") -}} +{{- define "mychart.web.service" -}} +metadata: + name: {{ template "common.fullname" . }}-www # overrides the default name to add a suffix + labels: # appended to the labels section + protocol: www +spec: + ports: # composes the `ports` section of the service definition. + - name: www + port: 80 + targetPort: 8080 +{{- end -}} +``` + +The above template defines _two_ services: a web service and a mail service. + +The most important part of a service definition is the `ports` object, which +defines the ports that this service will listen on. Most of the time, +`selector` is computed for you. But you can replace it or add to it. + +The output of the example above is: + +```yaml +apiVersion: v1 +kind: Service +metadata: + labels: + app: service + chart: service-0.1.0 + heritage: Tiller + protocol: mail + release: release-name + name: release-name-service-mail +spec: + ports: + - name: smtp + port: 25 + targetPort: 25 + - name: imaps + port: 993 + targetPort: 993 + selector: + app: service + release: release-name + protocol: mail + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: service + chart: service-0.1.0 + heritage: Tiller + protocol: www + release: release-name + name: release-name-service-www +spec: + ports: + - name: www + port: 80 + targetPort: 8080 + type: ClusterIP +``` + +## `common.deployment` + +The `common.deployment` template defines a basic `Deployment`. Underneath the +hood, it uses `common.container` (see next section). + +By default, the pod template within the deployment defines the labels `app: {{ template "common.name" . }}` +and `release: {{ .Release.Name | quote }` as this is also used as the selector. The +standard set of labels are not used as some of these can change during upgrades, +which causes the replica sets and pods to not correctly match. + +Example use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +## Define overrides for your Deployment resource here, e.g. +spec: + replicas: {{ .Values.replicaCount }} +{{- end -}} +``` + +## `common.container` + +The `common.container` template creates a basic `Container` spec to be used +within a `Deployment` or `ReplicaSet`. It holds the following defaults: + +- The name is set to the chart name +- Uses `.Values.image` to describe the image to run, with the following spec: + ```yaml + image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent + ``` +- Exposes the named port `http` as port 80 +- Lays out the compute resources using `.Values.resources` + +Example use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +## Define overrides for your Deployment resource here, e.g. +spec: + template: + spec: + containers: + - {{ template "common.container" (list . "mychart.deployment.container") }} +{{- end -}} +{{- define "mychart.deployment.container" -}} +## Define overrides for your Container here, e.g. +livenessProbe: + httpGet: + path: / + port: 80 +readinessProbe: + httpGet: + path: / + port: 80 +{{- end -}} +``` + +The above example creates a `Deployment` resource which makes use of the +`common.container` template to populate the PodSpec's container list. The usage +of this template is similar to the other resources, you must define and +reference a template that contains overrides for the container object. + +The most important part of a container definition is the image you want to run. +As mentioned above, this is derived from `.Values.image` by default. It is a +best practice to define the image, tag and pull policy in your charts' values as +this makes it easy for an operator to change the image registry, or use a +specific tag or version. Another example of configuration that should be exposed +to chart operators is the container's required compute resources, as this is +also very specific to an operators environment. An example `values.yaml` for +your chart could look like: + +```yaml +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +``` + +The output of running the above values through the earlier template is: + +```yaml +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: deployment + chart: deployment-0.1.0 + heritage: Tiller + release: release-name + name: release-name-deployment +spec: + template: + metadata: + labels: + app: deployment + spec: + containers: + - image: nginx:stable + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: 80 + name: deployment + ports: + - containerPort: 80 + name: http + readinessProbe: + httpGet: + path: / + port: 80 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +``` + +## `common.configmap` + +The `common.configmap` template creates an empty `ConfigMap` resource that you +can override with your configuration. + +Example use: + +```yaml +{{- template "common.configmap" (list . "mychart.configmap") -}} +{{- define "mychart.configmap" -}} +data: + zeus: cat + athena: cat + julius: cat + one: |- + {{ .Files.Get "file1.txt" }} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +data: + athena: cat + julius: cat + one: This is a file. + zeus: cat +kind: ConfigMap +metadata: + labels: + app: configmap + chart: configmap-0.1.0 + heritage: Tiller + release: release-name + name: release-name-configmap +``` + +## `common.secret` + +The `common.secret` template creates an empty `Secret` resource that you +can override with your secrets. + +Example use: + +```yaml +{{- template "common.secret" (list . "mychart.secret") -}} +{{- define "mychart.secret" -}} +data: + zeus: {{ print "cat" | b64enc }} + athena: {{ print "cat" | b64enc }} + julius: {{ print "cat" | b64enc }} + one: |- + {{ .Files.Get "file1.txt" | b64enc }} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +data: + athena: Y2F0 + julius: Y2F0 + one: VGhpcyBpcyBhIGZpbGUuCg== + zeus: Y2F0 +kind: Secret +metadata: + labels: + app: secret + chart: secret-0.1.0 + heritage: Tiller + release: release-name + name: release-name-secret +type: Opaque +``` + +## `common.ingress` + +The `common.ingress` template is designed to give you a well-defined `Ingress` +resource, that can be configured using `.Values.ingress`. An example values file +that can be used to configure the `Ingress` resource is: + +```yaml +ingress: + hosts: + - chart-example.local + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + tls: + - secretName: chart-example-tls + hosts: + - chart-example.local +``` + +Example use: + +```yaml +{{- template "common.ingress" (list . "mychart.ingress") -}} +{{- define "mychart.ingress" -}} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + labels: + app: ingress + chart: ingress-0.1.0 + heritage: Tiller + release: release-name + name: release-name-ingress +spec: + rules: + - host: chart-example.local + http: + paths: + - backend: + serviceName: release-name-ingress + servicePort: 80 + path: / + tls: + - hosts: + - chart-example.local + secretName: chart-example-tls +``` + +## `common.persistentvolumeclaim` + +`common.persistentvolumeclaim` can be used to easily add a +`PersistentVolumeClaim` resource to your chart that can be configured using +`.Values.persistence`: + +| Value | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------------- | +| persistence.enabled | Whether or not to claim a persistent volume. If false, `common.volume.pvc` will use an emptyDir instead | +| persistence.storageClass | `StorageClass` name | +| persistence.accessMode | Access mode for persistent volume | +| persistence.size | Size of persistent volume | +| persistence.existingClaim | If defined, `PersistentVolumeClaim` is not created and `common.volume.pvc` helper uses this claim | + +An example values file that can be used to configure the +`PersistentVolumeClaim` resource is: + +```yaml +persistence: + enabled: true + storageClass: fast + accessMode: ReadWriteOnce + size: 8Gi +``` + +Example use: + +```yaml +{{- template "common.persistentvolumeclaim" (list . "mychart.persistentvolumeclaim") -}} +{{- define "mychart.persistentvolumeclaim" -}} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: persistentvolumeclaim + chart: persistentvolumeclaim-0.1.0 + heritage: Tiller + release: release-name + name: release-name-persistentvolumeclaim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi + storageClassName: "fast" +``` + +## Partial API Objects + +When writing Kubernetes resources, you may find the following helpers useful to +construct parts of the spec. + +### EnvVar + +Use the EnvVar helpers within a container spec to simplify specifying key-value +environment variables or referencing secrets as values. + +Example Use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +spec: + template: + spec: + containers: + - {{ template "common.container" (list . "mychart.deployment.container") }} +{{- end -}} +{{- define "mychart.deployment.container" -}} +{{- $fullname := include "common.fullname" . -}} +env: +- {{ template "common.envvar.value" (list "ZEUS" "cat") }} +- {{ template "common.envvar.secret" (list "ATHENA" "secret-name" "athena") }} +{{- end -}} +``` + +Output: + +```yaml +... + spec: + containers: + - env: + - name: ZEUS + value: cat + - name: ATHENA + valueFrom: + secretKeyRef: + key: athena + name: secret-name +... +``` + +### Volume + +Use the Volume helpers within a `Deployment` spec to help define ConfigMap and +PersistentVolumeClaim volumes. + +Example Use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +spec: + template: + spec: + volumes: + - {{ template "common.volume.configMap" (list "config" "configmap-name") }} + - {{ template "common.volume.pvc" (list "data" "pvc-name" .Values.persistence) }} +{{- end -}} +``` + +Output: + +```yaml +... + spec: + volumes: + - configMap: + name: configmap-name + name: config + - name: data + persistentVolumeClaim: + claimName: pvc-name +... +``` + +The `common.volume.pvc` helper uses the following configuration from the `.Values.persistence` object: + +| Value | Description | +| ------------------------- | ----------------------------------------------------- | +| persistence.enabled | If false, creates an `emptyDir` instead | +| persistence.existingClaim | If set, uses this instead of the passed in claim name | + +## Utilities + +### `common.fullname` + +The `common.fullname` template generates a name suitable for the `name:` field +in Kubernetes metadata. It is used like this: + +```yaml +name: {{ template "common.fullname" . }} +``` + +The following different values can influence it: + +```yaml +# By default, fullname uses '{{ .Release.Name }}-{{ .Chart.Name }}'. This +# overrides that and uses the given string instead. +fullnameOverride: "some-name" + +# This adds a prefix +fullnamePrefix: "pre-" +# This appends a suffix +fullnameSuffix: "-suf" + +# Global versions of the above +global: + fullnamePrefix: "pp-" + fullnameSuffix: "-ps" +``` + +Example output: + +```yaml +--- +# with the values above +name: pp-pre-some-name-suf-ps + +--- +# the default, for release "happy-panda" and chart "wordpress" +name: happy-panda-wordpress +``` + +Output of this function is truncated at 54 characters, which leaves 9 additional +characters for customized overriding. Thus you can easily extend this name +in your own charts: + +```yaml +{{- define "my.fullname" -}} + {{ template "common.fullname" . }}-my-stuff +{{- end -}} +``` + +### `common.fullname.unique` + +The `common.fullname.unique` variant of fullname appends a unique seven-character +sequence to the end of the common name field. + +This takes all of the same parameters as `common.fullname` + +Example template: + +```yaml +uniqueName: {{ template "common.fullname.unique" . }} +``` + +Example output: + +```yaml +uniqueName: release-name-fullname-jl0dbwx +``` + +It is also impacted by the prefix and suffix definitions, as well as by +`.Values.fullnameOverride` + +Note that the effective maximum length of this function is 63 characters, not 54. + +### `common.name` + +The `common.name` template generates a name suitable for the `app` label. It is used like this: + +```yaml +app: {{ template "common.name" . }} +``` + +The following different values can influence it: + +```yaml +# By default, name uses '{{ .Chart.Name }}'. This +# overrides that and uses the given string instead. +nameOverride: "some-name" + +# This adds a prefix +namePrefix: "pre-" +# This appends a suffix +nameSuffix: "-suf" + +# Global versions of the above +global: + namePrefix: "pp-" + nameSuffix: "-ps" +``` + +Example output: + +```yaml +--- +# with the values above +name: pp-pre-some-name-suf-ps + +--- +# the default, for chart "wordpress" +name: wordpress +``` + +Output of this function is truncated at 54 characters, which leaves 9 additional +characters for customized overriding. Thus you can easily extend this name +in your own charts: + +```yaml +{{- define "my.name" -}} + {{ template "common.name" . }}-my-stuff +{{- end -}} +``` + +### `common.metadata` + +The `common.metadata` helper generates the `metadata:` section of a Kubernetes +resource. + +This takes three objects: + - .top: top context + - .fullnameOverride: override the fullname with this name + - .metadata + - .labels: key/value list of labels + - .annotations: key/value list of annotations + - .hook: name(s) of hook(s) + +It generates standard labels, annotations, hooks, and a name field. + +Example template: + +```yaml +{{ template "common.metadata" (dict "top" . "metadata" .Values.bio) }} +--- +{{ template "common.metadata" (dict "top" . "metadata" .Values.pet "fullnameOverride" .Values.pet.fullnameOverride) }} +``` + +Example values: + +```yaml +bio: + name: example + labels: + first: matt + last: butcher + nick: technosophos + annotations: + format: bio + destination: archive + hook: pre-install + +pet: + fullnameOverride: Zeus + +``` + +Example output: + +```yaml +metadata: + name: release-name-metadata + labels: + app: metadata + heritage: "Tiller" + release: "RELEASE-NAME" + chart: metadata-0.1.0 + first: "matt" + last: "butcher" + nick: "technosophos" + annotations: + "destination": "archive" + "format": "bio" + "helm.sh/hook": "pre-install" +--- +metadata: + name: Zeus + labels: + app: metadata + heritage: "Tiller" + release: "RELEASE-NAME" + chart: metadata-0.1.0 + annotations: +``` + +Most of the common templates that define a resource type (e.g. `common.configmap` +or `common.job`) use this to generate the metadata, which means they inherit +the same `labels`, `annotations`, `nameOverride`, and `hook` fields. + +### `common.labelize` + +`common.labelize` turns a map into a set of labels. + +Example template: + +```yaml +{{- $map := dict "first" "1" "second" "2" "third" "3" -}} +{{- template "common.labelize" $map -}} +``` + +Example output: + +```yaml +first: "1" +second: "2" +third: "3" +``` + +### `common.labels.standard` + +`common.labels.standard` prints the standard set of labels. + +Example usage: + +``` +{{ template "common.labels.standard" . }} +``` + +Example output: + +```yaml +app: labelizer +heritage: "Tiller" +release: "RELEASE-NAME" +chart: labelizer-0.1.0 +``` + +### `common.hook` + +The `common.hook` template is a convenience for defining hooks. + +Example template: + +```yaml +{{ template "common.hook" "pre-install,post-install" }} +``` + +Example output: + +```yaml +"helm.sh/hook": "pre-install,post-install" +``` + +### `common.chartref` + +The `common.chartref` helper prints the chart name and version, escaped to be +legal in a Kubernetes label field. + +Example template: + +```yaml +chartref: {{ template "common.chartref" . }} +``` + +For the chart `foo` with version `1.2.3-beta.55+1234`, this will render: + +```yaml +chartref: foo-1.2.3-beta.55_1234 +``` + +(Note that `+` is an illegal character in label values) diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_chartref.tpl b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_chartref.tpl new file mode 100755 index 00000000000..e6c14866fe1 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_chartref.tpl @@ -0,0 +1,14 @@ +{{- /* +common.chartref prints a chart name and version. + +It does minimal escaping for use in Kubernetes labels. + +Example output: + + zookeeper-1.2.3 + wordpress-3.2.1_20170219 + +*/ -}} +{{- define "common.chartref" -}} + {{- replace "+" "_" .Chart.Version | printf "%s-%s" .Chart.Name -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_configmap.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_configmap.yaml new file mode 100755 index 00000000000..03dbbf858ad --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_configmap.yaml @@ -0,0 +1,9 @@ +{{- define "common.configmap.tpl" -}} +apiVersion: v1 +kind: ConfigMap +{{ template "common.metadata" . }} +data: {} +{{- end -}} +{{- define "common.configmap" -}} +{{- template "common.util.merge" (append . "common.configmap.tpl") -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_container.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_container.yaml new file mode 100755 index 00000000000..540eb0e6ab3 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_container.yaml @@ -0,0 +1,15 @@ +{{- define "common.container.tpl" -}} +name: {{ .Chart.Name }} +image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" +imagePullPolicy: {{ .Values.image.pullPolicy }} +ports: +- name: http + containerPort: 80 +resources: +{{ toYaml .Values.resources | indent 2 }} +{{- end -}} +{{- define "common.container" -}} +{{- /* clear new line so indentation works correctly */ -}} +{{- println "" -}} +{{- include "common.util.merge" (append . "common.container.tpl") | indent 8 -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_deployment.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_deployment.yaml new file mode 100755 index 00000000000..c49dae3eb13 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_deployment.yaml @@ -0,0 +1,18 @@ +{{- define "common.deployment.tpl" -}} +apiVersion: extensions/v1beta1 +kind: Deployment +{{ template "common.metadata" . }} +spec: + template: + metadata: + labels: + app: {{ template "common.name" . }} + release: {{ .Release.Name | quote }} + spec: + containers: + - +{{ include "common.container.tpl" . | indent 8 }} +{{- end -}} +{{- define "common.deployment" -}} +{{- template "common.util.merge" (append . "common.deployment.tpl") -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_envvar.tpl b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_envvar.tpl new file mode 100755 index 00000000000..709251f8f3c --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_envvar.tpl @@ -0,0 +1,31 @@ +{{- define "common.envvar.value" -}} + {{- $name := index . 0 -}} + {{- $value := index . 1 -}} + + name: {{ $name }} + value: {{ default "" $value | quote }} +{{- end -}} + +{{- define "common.envvar.configmap" -}} + {{- $name := index . 0 -}} + {{- $configMapName := index . 1 -}} + {{- $configMapKey := index . 2 -}} + + name: {{ $name }} + valueFrom: + configMapKeyRef: + name: {{ $configMapName }} + key: {{ $configMapKey }} +{{- end -}} + +{{- define "common.envvar.secret" -}} + {{- $name := index . 0 -}} + {{- $secretName := index . 1 -}} + {{- $secretKey := index . 2 -}} + + name: {{ $name }} + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secretKey }} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_fullname.tpl b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_fullname.tpl new file mode 100755 index 00000000000..2da6cdf18cf --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_fullname.tpl @@ -0,0 +1,39 @@ +{{- /* +fullname defines a suitably unique name for a resource by combining +the release name and the chart name. + +The prevailing wisdom is that names should only contain a-z, 0-9 plus dot (.) and dash (-), and should +not exceed 63 characters. + +Parameters: + +- .Values.fullnameOverride: Replaces the computed name with this given name +- .Values.fullnamePrefix: Prefix +- .Values.global.fullnamePrefix: Global prefix +- .Values.fullnameSuffix: Suffix +- .Values.global.fullnameSuffix: Global suffix + +The applied order is: "global prefix + prefix + name + suffix + global suffix" + +Usage: 'name: "{{- template "common.fullname" . -}}"' +*/ -}} +{{- define "common.fullname"}} + {{- $global := default (dict) .Values.global -}} + {{- $base := default (printf "%s-%s" .Release.Name .Chart.Name) .Values.fullnameOverride -}} + {{- $gpre := default "" $global.fullnamePrefix -}} + {{- $pre := default "" .Values.fullnamePrefix -}} + {{- $suf := default "" .Values.fullnameSuffix -}} + {{- $gsuf := default "" $global.fullnameSuffix -}} + {{- $name := print $gpre $pre $base $suf $gsuf -}} + {{- $name | lower | trunc 54 | trimSuffix "-" -}} +{{- end -}} + +{{- /* +common.fullname.unique adds a random suffix to the unique name. + +This takes the same parameters as common.fullname + +*/ -}} +{{- define "common.fullname.unique" -}} + {{ template "common.fullname" . }}-{{ randAlphaNum 7 | lower }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_ingress.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_ingress.yaml new file mode 100755 index 00000000000..78411e15b9f --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_ingress.yaml @@ -0,0 +1,27 @@ +{{- define "common.ingress.tpl" -}} +apiVersion: extensions/v1beta1 +kind: Ingress +{{ template "common.metadata" . }} + {{- if .Values.ingress.annotations }} + annotations: + {{ include "common.annotate" .Values.ingress.annotations | indent 4 }} + {{- end }} +spec: + rules: + {{- range $host := .Values.ingress.hosts }} + - host: {{ $host }} + http: + paths: + - path: / + backend: + serviceName: {{ template "common.fullname" $ }} + servicePort: 80 + {{- end }} + {{- if .Values.ingress.tls }} + tls: +{{ toYaml .Values.ingress.tls | indent 4 }} + {{- end -}} +{{- end -}} +{{- define "common.ingress" -}} +{{- template "common.util.merge" (append . "common.ingress.tpl") -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata.yaml new file mode 100755 index 00000000000..f96ed09fe59 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata.yaml @@ -0,0 +1,10 @@ +{{- /* +common.metadata creates a standard metadata header. +It creates a 'metadata:' section with name and labels. +*/ -}} +{{ define "common.metadata" -}} +metadata: + name: {{ template "common.fullname" . }} + labels: +{{ include "common.labels.standard" . | indent 4 -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_annotations.tpl b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_annotations.tpl new file mode 100755 index 00000000000..dffe1eca90d --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_annotations.tpl @@ -0,0 +1,18 @@ +{{- /* +common.hook defines a hook. + +This is to be used in a 'metadata.annotations' section. + +This should be called as 'template "common.metadata.hook" "post-install"' + +Any valid hook may be passed in. Separate multiple hooks with a ",". +*/ -}} +{{- define "common.hook" -}} +"helm.sh/hook": {{printf "%s" . | quote}} +{{- end -}} + +{{- define "common.annotate" -}} +{{- range $k, $v := . }} +{{ $k | quote }}: {{ $v | quote }} +{{- end -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_labels.tpl b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_labels.tpl new file mode 100755 index 00000000000..15fe00c5f3d --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_labels.tpl @@ -0,0 +1,28 @@ +{{- /* +common.labelize takes a dict or map and generates labels. + +Values will be quoted. Keys will not. + +Example output: + + first: "Matt" + last: "Butcher" + +*/ -}} +{{- define "common.labelize" -}} +{{- range $k, $v := . }} +{{ $k }}: {{ $v | quote }} +{{- end -}} +{{- end -}} + +{{- /* +common.labels.standard prints the standard Helm labels. + +The standard labels are frequently used in metadata. +*/ -}} +{{- define "common.labels.standard" -}} +app: {{ template "common.name" . }} +chart: {{ template "common.chartref" . }} +heritage: {{ .Release.Service | quote }} +release: {{ .Release.Name | quote }} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_name.tpl b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_name.tpl new file mode 100755 index 00000000000..1d42fb06820 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_name.tpl @@ -0,0 +1,29 @@ +{{- /* +name defines a template for the name of the chart. It should be used for the `app` label. +This is common practice in many Kubernetes manifests, and is not Helm-specific. + +The prevailing wisdom is that names should only contain a-z, 0-9 plus dot (.) and dash (-), and should +not exceed 63 characters. + +Parameters: + +- .Values.nameOverride: Replaces the computed name with this given name +- .Values.namePrefix: Prefix +- .Values.global.namePrefix: Global prefix +- .Values.nameSuffix: Suffix +- .Values.global.nameSuffix: Global suffix + +The applied order is: "global prefix + prefix + name + suffix + global suffix" + +Usage: 'name: "{{- template "common.name" . -}}"' +*/ -}} +{{- define "common.name"}} + {{- $global := default (dict) .Values.global -}} + {{- $base := default .Chart.Name .Values.nameOverride -}} + {{- $gpre := default "" $global.namePrefix -}} + {{- $pre := default "" .Values.namePrefix -}} + {{- $suf := default "" .Values.nameSuffix -}} + {{- $gsuf := default "" $global.nameSuffix -}} + {{- $name := print $gpre $pre $base $suf $gsuf -}} + {{- $name | lower | trunc 54 | trimSuffix "-" -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_persistentvolumeclaim.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_persistentvolumeclaim.yaml new file mode 100755 index 00000000000..6c1578c7ed5 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_persistentvolumeclaim.yaml @@ -0,0 +1,24 @@ +{{- define "common.persistentvolumeclaim.tpl" -}} +apiVersion: v1 +kind: PersistentVolumeClaim +{{ template "common.metadata" . }} +spec: + accessModes: + - {{ .Values.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- if .Values.persistence.storageClass }} +{{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" +{{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" +{{- end }} +{{- end }} +{{- end -}} +{{- define "common.persistentvolumeclaim" -}} +{{- $top := first . -}} +{{- if and $top.Values.persistence.enabled (not $top.Values.persistence.existingClaim) -}} +{{- template "common.util.merge" (append . "common.persistentvolumeclaim.tpl") -}} +{{- end -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_secret.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_secret.yaml new file mode 100755 index 00000000000..0615d35cb41 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_secret.yaml @@ -0,0 +1,10 @@ +{{- define "common.secret.tpl" -}} +apiVersion: v1 +kind: Secret +{{ template "common.metadata" . }} +type: Opaque +data: {} +{{- end -}} +{{- define "common.secret" -}} +{{- template "common.util.merge" (append . "common.secret.tpl") -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_service.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_service.yaml new file mode 100755 index 00000000000..67379525fe4 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_service.yaml @@ -0,0 +1,17 @@ +{{- define "common.service.tpl" -}} +apiVersion: v1 +kind: Service +{{ template "common.metadata" . }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: 80 + targetPort: http + selector: + app: {{ template "common.name" . }} + release: {{ .Release.Name | quote }} +{{- end -}} +{{- define "common.service" -}} +{{- template "common.util.merge" (append . "common.service.tpl") -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_util.tpl b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_util.tpl new file mode 100755 index 00000000000..a7d4cc751e3 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_util.tpl @@ -0,0 +1,15 @@ +{{- /* +common.util.merge will merge two YAML templates and output the result. + +This takes an array of three values: +- the top context +- the template name of the overrides (destination) +- the template name of the base (source) + +*/ -}} +{{- define "common.util.merge" -}} +{{- $top := first . -}} +{{- $overrides := fromYaml (include (index . 1) $top) | default (dict ) -}} +{{- $tpl := fromYaml (include (index . 2) $top) | default (dict ) -}} +{{- toYaml (merge $overrides $tpl) -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_volume.tpl b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_volume.tpl new file mode 100755 index 00000000000..521a1f48b00 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_volume.tpl @@ -0,0 +1,22 @@ +{{- define "common.volume.configMap" -}} + {{- $name := index . 0 -}} + {{- $configMapName := index . 1 -}} + + name: {{ $name }} + configMap: + name: {{ $configMapName }} +{{- end -}} + +{{- define "common.volume.pvc" -}} + {{- $name := index . 0 -}} + {{- $claimName := index . 1 -}} + {{- $persistence := index . 2 -}} + + name: {{ $name }} + {{- if $persistence.enabled }} + persistentVolumeClaim: + claimName: {{ $persistence.existingClaim | default $claimName }} + {{- else }} + emptyDir: {} + {{- end -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/configmap.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/configmap.yaml new file mode 100644 index 00000000000..b5bf1dfc3a2 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: common-configmap +data: + myvalue: "Hello World" diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/values.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/values.yaml new file mode 100755 index 00000000000..b7cf514d567 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/values.yaml @@ -0,0 +1,4 @@ +# Default values for commons. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/NOTES.txt b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/NOTES.txt new file mode 100644 index 00000000000..8f6bb9b1d42 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "chart-with-template-lib-dep.fullname" . }}) + export NODE_IP=$(kubectl get nodes -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ template "chart-with-template-lib-dep.fullname" . }}' + export SERVICE_IP=$(kubectl get svc {{ template "chart-with-template-lib-dep.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods -l "app={{ template "chart-with-template-lib-dep.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:80 +{{- end }} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/_helpers.tpl b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/_helpers.tpl new file mode 100644 index 00000000000..0ab79743ded --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "chart-with-template-lib-dep.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "chart-with-template-lib-dep.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "chart-with-template-lib-dep.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/deployment.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/deployment.yaml new file mode 100644 index 00000000000..6b950d13995 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/deployment.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "chart-with-template-lib-dep.fullname" . }} + labels: + app: {{ template "chart-with-template-lib-dep.name" . }} + chart: {{ template "chart-with-template-lib-dep.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "chart-with-template-lib-dep.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "chart-with-template-lib-dep.name" . }} + release: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/ingress.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/ingress.yaml new file mode 100644 index 00000000000..a978df4e7aa --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/ingress.yaml @@ -0,0 +1,38 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "chart-with-template-lib-dep.fullname" . -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ template "chart-with-template-lib-dep.name" . }} + chart: {{ template "chart-with-template-lib-dep.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/service.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/service.yaml new file mode 100644 index 00000000000..d532bb3d89c --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/service.yaml @@ -0,0 +1,10 @@ +{{- template "common.service" (list . "chart-with-template-lib-dep.service") -}} +{{- define "chart-with-template-lib-dep.service" -}} +## Define overrides for your Service resource here, e.g. +# metadata: +# labels: +# custom: label +# spec: +# ports: +# - port: 8080 +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/values.yaml b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/values.yaml new file mode 100644 index 00000000000..d49955c26f7 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/values.yaml @@ -0,0 +1,48 @@ +# Default values for chart-with-template-lib-dep. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent + +nameOverride: "" +fullnameOverride: "" + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + path: / + hosts: + - chart-example.local + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/Chart.yaml new file mode 100644 index 00000000000..29b477b063c --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +description: Deploy a basic Alpine Linux pod +home: https://helm.sh/helm +name: chart-with-template-with-invalid-yaml +sources: + - https://github.com/helm/helm +version: 0.1.0 +type: application diff --git a/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/README.md b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/README.md new file mode 100644 index 00000000000..fcf7ee01748 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/README.md @@ -0,0 +1,13 @@ +#Alpine: A simple Helm chart + +Run a single pod of Alpine Linux. + +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.yaml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml new file mode 100644 index 00000000000..697cb50febb --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{.Release.Name}}-{{.Values.Name}}" +spec: + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] +invalid diff --git a/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/values.yaml b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/values.yaml new file mode 100644 index 00000000000..807e12aea65 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/values.yaml @@ -0,0 +1 @@ +Name: my-alpine diff --git a/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tar.gz b/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tar.gz new file mode 100644 index 00000000000..3c9c24d7606 Binary files /dev/null and b/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tar.gz differ diff --git a/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz b/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz index 575b271286c..3c9c24d7606 100644 Binary files a/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz and b/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/compressedchart-0.2.0.tgz b/cmd/helm/testdata/testcharts/compressedchart-0.2.0.tgz index ba96a80c9c5..16a644a796f 100644 Binary files a/cmd/helm/testdata/testcharts/compressedchart-0.2.0.tgz and b/cmd/helm/testdata/testcharts/compressedchart-0.2.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/compressedchart-0.3.0.tgz b/cmd/helm/testdata/testcharts/compressedchart-0.3.0.tgz index 89776bfa80f..051bd6fd9a0 100644 Binary files a/cmd/helm/testdata/testcharts/compressedchart-0.3.0.tgz and b/cmd/helm/testdata/testcharts/compressedchart-0.3.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/decompressedchart/.helmignore b/cmd/helm/testdata/testcharts/decompressedchart/.helmignore deleted file mode 100644 index 435b756d885..00000000000 --- a/cmd/helm/testdata/testcharts/decompressedchart/.helmignore +++ /dev/null @@ -1,5 +0,0 @@ -# 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 deleted file mode 100644 index 3e65afdfaf2..00000000000 --- a/cmd/helm/testdata/testcharts/decompressedchart/Chart.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: A Helm chart for Kubernetes -name: decompressedchart -version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/deprecated/Chart.yaml b/cmd/helm/testdata/testcharts/deprecated/Chart.yaml new file mode 100644 index 00000000000..10185beeb66 --- /dev/null +++ b/cmd/helm/testdata/testcharts/deprecated/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +description: Deprecated testing chart +home: https://helm.sh/helm +name: deprecated +sources: + - https://github.com/helm/helm +version: 0.1.0 +deprecated: true diff --git a/cmd/helm/testdata/testcharts/deprecated/README.md b/cmd/helm/testdata/testcharts/deprecated/README.md new file mode 100644 index 00000000000..0df9a8bbcbd --- /dev/null +++ b/cmd/helm/testdata/testcharts/deprecated/README.md @@ -0,0 +1,3 @@ +#Deprecated + +This space intentionally left blank. diff --git a/cmd/helm/testdata/testcharts/empty/Chart.yaml b/cmd/helm/testdata/testcharts/empty/Chart.yaml new file mode 100644 index 00000000000..4f1dc00123a --- /dev/null +++ b/cmd/helm/testdata/testcharts/empty/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +description: Empty testing chart +home: https://helm.sh/helm +name: empty +sources: + - https://github.com/helm/helm +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/empty/README.md b/cmd/helm/testdata/testcharts/empty/README.md new file mode 100644 index 00000000000..ed73c1797eb --- /dev/null +++ b/cmd/helm/testdata/testcharts/empty/README.md @@ -0,0 +1,3 @@ +#Empty + +This space intentionally left blank. diff --git a/cmd/helm/testdata/testcharts/empty/templates/empty.yaml b/cmd/helm/testdata/testcharts/empty/templates/empty.yaml new file mode 100644 index 00000000000..c80812f6e0a --- /dev/null +++ b/cmd/helm/testdata/testcharts/empty/templates/empty.yaml @@ -0,0 +1 @@ +# This file is intentionally blank diff --git a/cmd/helm/testdata/testcharts/empty/values.yaml b/cmd/helm/testdata/testcharts/empty/values.yaml new file mode 100644 index 00000000000..1f0ff00e30e --- /dev/null +++ b/cmd/helm/testdata/testcharts/empty/values.yaml @@ -0,0 +1 @@ +Name: my-empty diff --git a/cmd/helm/testdata/testcharts/issue-7233/.helmignore b/cmd/helm/testdata/testcharts/issue-7233/.helmignore new file mode 100644 index 00000000000..50af0317254 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-7233/.helmignore @@ -0,0 +1,22 @@ +# 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 +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/cmd/helm/testdata/testcharts/issue-7233/Chart.yaml b/cmd/helm/testdata/testcharts/issue-7233/Chart.yaml new file mode 100644 index 00000000000..b31997acb72 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-7233/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: issue-7233 +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/issue-7233/requirements.lock b/cmd/helm/testdata/testcharts/issue-7233/requirements.lock new file mode 100644 index 00000000000..62744125b0a --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-7233/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: alpine + repository: file://../alpine + version: 0.1.0 +digest: sha256:7b380b1a826e7be1eecb089f66209d6d3df54be4bf879d4a8e6f8a9e871710e5 +generated: "2020-01-31T11:30:21.911547651Z" diff --git a/cmd/helm/testdata/testcharts/issue-7233/requirements.yaml b/cmd/helm/testdata/testcharts/issue-7233/requirements.yaml new file mode 100644 index 00000000000..f0195cb15b2 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-7233/requirements.yaml @@ -0,0 +1,4 @@ +dependencies: +- name: alpine + version: 0.1.0 + repository: file://../alpine diff --git a/cmd/helm/testdata/testcharts/issue-7233/templates/configmap.yaml b/cmd/helm/testdata/testcharts/issue-7233/templates/configmap.yaml new file mode 100644 index 00000000000..53880b25d39 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-7233/templates/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Release.Name }}-configmap +data: + myvalue: "Hello World" + drink: {{ .Values.favoriteDrink }} diff --git a/cmd/helm/testdata/testcharts/issue-7233/values.yaml b/cmd/helm/testdata/testcharts/issue-7233/values.yaml new file mode 100644 index 00000000000..b1aa168d77b --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-7233/values.yaml @@ -0,0 +1 @@ +favoriteDrink: coffee diff --git a/cmd/helm/testdata/testcharts/issue1979/Chart.yaml b/cmd/helm/testdata/testcharts/issue1979/Chart.yaml new file mode 100644 index 00000000000..5269b5cf6bd --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue1979/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +description: Deploy a basic Alpine Linux pod +home: https://helm.sh/helm +name: alpine +sources: + - https://github.com/helm/helm +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/issue1979/README.md b/cmd/helm/testdata/testcharts/issue1979/README.md new file mode 100644 index 00000000000..fcf7ee01748 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue1979/README.md @@ -0,0 +1,13 @@ +#Alpine: A simple Helm chart + +Run a single pod of Alpine Linux. + +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.yaml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/cmd/helm/testdata/testcharts/issue1979/extra_values.yaml b/cmd/helm/testdata/testcharts/issue1979/extra_values.yaml new file mode 100644 index 00000000000..468bbacbc34 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue1979/extra_values.yaml @@ -0,0 +1,2 @@ +test: + Name: extra-values diff --git a/cmd/helm/testdata/testcharts/issue1979/more_values.yaml b/cmd/helm/testdata/testcharts/issue1979/more_values.yaml new file mode 100644 index 00000000000..3d21e1fed41 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue1979/more_values.yaml @@ -0,0 +1,2 @@ +test: + Name: more-values diff --git a/cmd/helm/testdata/testcharts/issue1979/templates/alpine-pod.yaml b/cmd/helm/testdata/testcharts/issue1979/templates/alpine-pod.yaml new file mode 100644 index 00000000000..6f025fecbb5 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue1979/templates/alpine-pod.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{.Release.Name}}-{{.Values.Name}}" + labels: + # The "app.kubernetes.io/managed-by" label is used to track which tool + # deployed a given chart. It is useful for admins who want to see what + # releases a particular tool is responsible for. + app.kubernetes.io/managed-by: {{.Release.Service | quote }} + # The "app.kubernetes.io/instance" convention makes it easy to tie a release + # to all of the Kubernetes resources that were created as part of that + # release. + app.kubernetes.io/instance: {{.Release.Name | quote }} + # This makes it easy to audit chart usage. + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" + values: {{.Values.test.Name}} +spec: + # This shows how to use a simple value. This will look for a passed-in value + # called restartPolicy. If it is not found, it will use the default value. + # {{default "Never" .restartPolicy}} is a slightly optimized version of the + # more conventional syntax: {{.restartPolicy | default "Never"}} + restartPolicy: {{default "Never" .Values.restartPolicy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/cmd/helm/testdata/testcharts/issue1979/values.yaml b/cmd/helm/testdata/testcharts/issue1979/values.yaml new file mode 100644 index 00000000000..879d760f924 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue1979/values.yaml @@ -0,0 +1,2 @@ +# The pod name +Name: my-alpine diff --git a/cmd/helm/testdata/testcharts/lib-chart/.helmignore b/cmd/helm/testdata/testcharts/lib-chart/.helmignore new file mode 100644 index 00000000000..f0c13194444 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/.helmignore @@ -0,0 +1,21 @@ +# 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 +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/cmd/helm/testdata/testcharts/lib-chart/Chart.yaml b/cmd/helm/testdata/testcharts/lib-chart/Chart.yaml new file mode 100755 index 00000000000..4dcddc85eb9 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +description: Common chartbuilding components and helpers +name: lib-chart +version: 0.0.5 +appVersion: 0.0.5 +home: https://helm.sh +maintainers: +- name: technosophos + email: technosophos@gmail.com +- name: prydonius + email: adnan@bitnami.com +type: Library diff --git a/cmd/helm/testdata/testcharts/lib-chart/README.md b/cmd/helm/testdata/testcharts/lib-chart/README.md new file mode 100644 index 00000000000..aca25792403 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/README.md @@ -0,0 +1,831 @@ +# Common: The Helm Helper Chart + +This chart is designed to make it easier for you to build and maintain Helm +charts. + +It provides utilities that reflect best practices of Kubernetes chart development, +making it faster for you to write charts. + +## Tips + +A few tips for working with Common: + +- Be careful when using functions that generate random data (like `common.fullname.unique`). + They may trigger unwanted upgrades or have other side effects. + +In this document, we use `RELEASE-NAME` as the name of the release. + +## Resource Kinds + +Kubernetes defines a variety of resource kinds, from `Secret` to `StatefulSet`. +We define some of the most common kinds in a way that lets you easily work with +them. + +The resource kind templates are designed to make it much faster for you to +define _basic_ versions of these resources. They allow you to extend and modify +just what you need, without having to copy around lots of boilerplate. + +To make use of these templates you must define a template that will extend the +base template (though it can be empty). The name of this template is then passed +to the base template, for example: + +```yaml +{{- template "common.service" (list . "mychart.service") -}} +{{- define "mychart.service" -}} +## Define overrides for your Service resource here, e.g. +# metadata: +# labels: +# custom: label +# spec: +# ports: +# - port: 8080 +{{- end -}} +``` + +Note that the `common.service` template defines two parameters: + + - The root context (usually `.`) + - A template name containing the service definition overrides + +A limitation of the Go template library is that a template can only take a +single argument. The `list` function is used to workaround this by constructing +a list or array of arguments that is passed to the template. + +The `common.service` template is responsible for rendering the templates with +the root context and merging any overrides. As you can see, this makes it very +easy to create a basic `Service` resource without having to copy around the +standard metadata and labels. + +Each implemented base resource is described in greater detail below. + +### `common.service` + +The `common.service` template creates a basic `Service` resource with the +following defaults: + +- Service type (ClusterIP, NodePort, LoadBalancer) made configurable by `.Values.service.type` +- Named port `http` configured on port 80 +- Selector set to `app.kubernetes.io/name: {{ template "common.name" }}, app.kubernetes.io/instance: {{ .Release.Name | quote }}` to match the default used in the `Deployment` resource + +Example template: + +```yaml +{{- template "common.service" (list . "mychart.mail.service") -}} +{{- define "mychart.mail.service" -}} +metadata: + name: {{ template "common.fullname" . }}-mail # overrides the default name to add a suffix + labels: # appended to the labels section + protocol: mail +spec: + ports: # composes the `ports` section of the service definition. + - name: smtp + port: 25 + targetPort: 25 + - name: imaps + port: 993 + targetPort: 993 + selector: # this is appended to the default selector + protocol: mail +{{- end -}} +--- +{{ template "common.service" (list . "mychart.web.service") -}} +{{- define "mychart.web.service" -}} +metadata: + name: {{ template "common.fullname" . }}-www # overrides the default name to add a suffix + labels: # appended to the labels section + protocol: www +spec: + ports: # composes the `ports` section of the service definition. + - name: www + port: 80 + targetPort: 8080 +{{- end -}} +``` + +The above template defines _two_ services: a web service and a mail service. + +The most important part of a service definition is the `ports` object, which +defines the ports that this service will listen on. Most of the time, +`selector` is computed for you. But you can replace it or add to it. + +The output of the example above is: + +```yaml +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: service + helm.sh/chart: service-0.1.0 + app.kubernetes.io/managed-by: Helm + protocol: mail + app.kubernetes.io/instance: release-name + name: release-name-service-mail +spec: + ports: + - name: smtp + port: 25 + targetPort: 25 + - name: imaps + port: 993 + targetPort: 993 + selector: + app.kubernetes.io/name: service + app.kubernetes.io/instance: release-name + protocol: mail + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: service + helm.sh/chart: service-0.1.0 + app.kubernetes.io/managed-by: Helm + protocol: www + app.kubernetes.io/instance: release-name + name: release-name-service-www +spec: + ports: + - name: www + port: 80 + targetPort: 8080 + type: ClusterIP +``` + +## `common.deployment` + +The `common.deployment` template defines a basic `Deployment`. Underneath the +hood, it uses `common.container` (see next section). + +By default, the pod template within the deployment defines the labels `app: {{ template "common.name" . }}` +and `release: {{ .Release.Name | quote }` as this is also used as the selector. The +standard set of labels are not used as some of these can change during upgrades, +which causes the replica sets and pods to not correctly match. + +Example use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +## Define overrides for your Deployment resource here, e.g. +spec: + replicas: {{ .Values.replicaCount }} +{{- end -}} +``` + +## `common.container` + +The `common.container` template creates a basic `Container` spec to be used +within a `Deployment` or `ReplicaSet`. It holds the following defaults: + +- The name is set to the chart name +- Uses `.Values.image` to describe the image to run, with the following spec: + ```yaml + image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent + ``` +- Exposes the named port `http` as port 80 +- Lays out the compute resources using `.Values.resources` + +Example use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +## Define overrides for your Deployment resource here, e.g. +spec: + template: + spec: + containers: + - {{ template "common.container" (list . "mychart.deployment.container") }} +{{- end -}} +{{- define "mychart.deployment.container" -}} +## Define overrides for your Container here, e.g. +livenessProbe: + httpGet: + path: / + port: 80 +readinessProbe: + httpGet: + path: / + port: 80 +{{- end -}} +``` + +The above example creates a `Deployment` resource which makes use of the +`common.container` template to populate the PodSpec's container list. The usage +of this template is similar to the other resources, you must define and +reference a template that contains overrides for the container object. + +The most important part of a container definition is the image you want to run. +As mentioned above, this is derived from `.Values.image` by default. It is a +best practice to define the image, tag and pull policy in your charts' values as +this makes it easy for an operator to change the image registry, or use a +specific tag or version. Another example of configuration that should be exposed +to chart operators is the container's required compute resources, as this is +also very specific to an operators environment. An example `values.yaml` for +your chart could look like: + +```yaml +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +``` + +The output of running the above values through the earlier template is: + +```yaml +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/name: deployment + helm.sh/chart: deployment-0.1.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + name: release-name-deployment +spec: + template: + metadata: + labels: + app.kubernetes.io/name: deployment + spec: + containers: + - image: nginx:stable + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: 80 + name: deployment + ports: + - containerPort: 80 + name: http + readinessProbe: + httpGet: + path: / + port: 80 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +``` + +## `common.configmap` + +The `common.configmap` template creates an empty `ConfigMap` resource that you +can override with your configuration. + +Example use: + +```yaml +{{- template "common.configmap" (list . "mychart.configmap") -}} +{{- define "mychart.configmap" -}} +data: + zeus: cat + athena: cat + julius: cat + one: |- + {{ .Files.Get "file1.txt" }} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +data: + athena: cat + julius: cat + one: This is a file. + zeus: cat +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/name: configmap + helm.sh/chart: configmap-0.1.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + name: release-name-configmap +``` + +## `common.secret` + +The `common.secret` template creates an empty `Secret` resource that you +can override with your secrets. + +Example use: + +```yaml +{{- template "common.secret" (list . "mychart.secret") -}} +{{- define "mychart.secret" -}} +data: + zeus: {{ print "cat" | b64enc }} + athena: {{ print "cat" | b64enc }} + julius: {{ print "cat" | b64enc }} + one: |- + {{ .Files.Get "file1.txt" | b64enc }} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +data: + athena: Y2F0 + julius: Y2F0 + one: VGhpcyBpcyBhIGZpbGUuCg== + zeus: Y2F0 +kind: Secret +metadata: + labels: + app.kubernetes.io/name: secret + helm.sh/chart: secret-0.1.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + name: release-name-secret +type: Opaque +``` + +## `common.ingress` + +The `common.ingress` template is designed to give you a well-defined `Ingress` +resource, that can be configured using `.Values.ingress`. An example values file +that can be used to configure the `Ingress` resource is: + +```yaml +ingress: + hosts: + - chart-example.local + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + tls: + - secretName: chart-example-tls + hosts: + - chart-example.local +``` + +Example use: + +```yaml +{{- template "common.ingress" (list . "mychart.ingress") -}} +{{- define "mychart.ingress" -}} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + labels: + app.kubernetes.io/name: ingress + helm.sh/chart: ingress-0.1.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + name: release-name-ingress +spec: + rules: + - host: chart-example.local + http: + paths: + - backend: + serviceName: release-name-ingress + servicePort: 80 + path: / + tls: + - hosts: + - chart-example.local + secretName: chart-example-tls +``` + +## `common.persistentvolumeclaim` + +`common.persistentvolumeclaim` can be used to easily add a +`PersistentVolumeClaim` resource to your chart that can be configured using +`.Values.persistence`: + +| Value | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------------- | +| persistence.enabled | Whether or not to claim a persistent volume. If false, `common.volume.pvc` will use an emptyDir instead | +| persistence.storageClass | `StorageClass` name | +| persistence.accessMode | Access mode for persistent volume | +| persistence.size | Size of persistent volume | +| persistence.existingClaim | If defined, `PersistentVolumeClaim` is not created and `common.volume.pvc` helper uses this claim | + +An example values file that can be used to configure the +`PersistentVolumeClaim` resource is: + +```yaml +persistence: + enabled: true + storageClass: fast + accessMode: ReadWriteOnce + size: 8Gi +``` + +Example use: + +```yaml +{{- template "common.persistentvolumeclaim" (list . "mychart.persistentvolumeclaim") -}} +{{- define "mychart.persistentvolumeclaim" -}} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app.kubernetes.io/name: persistentvolumeclaim + helm.sh/chart: persistentvolumeclaim-0.1.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + name: release-name-persistentvolumeclaim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi + storageClassName: "fast" +``` + +## Partial API Objects + +When writing Kubernetes resources, you may find the following helpers useful to +construct parts of the spec. + +### EnvVar + +Use the EnvVar helpers within a container spec to simplify specifying key-value +environment variables or referencing secrets as values. + +Example Use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +spec: + template: + spec: + containers: + - {{ template "common.container" (list . "mychart.deployment.container") }} +{{- end -}} +{{- define "mychart.deployment.container" -}} +{{- $fullname := include "common.fullname" . -}} +env: +- {{ template "common.envvar.value" (list "ZEUS" "cat") }} +- {{ template "common.envvar.secret" (list "ATHENA" "secret-name" "athena") }} +{{- end -}} +``` + +Output: + +```yaml +... + spec: + containers: + - env: + - name: ZEUS + value: cat + - name: ATHENA + valueFrom: + secretKeyRef: + key: athena + name: secret-name +... +``` + +### Volume + +Use the Volume helpers within a `Deployment` spec to help define ConfigMap and +PersistentVolumeClaim volumes. + +Example Use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +spec: + template: + spec: + volumes: + - {{ template "common.volume.configMap" (list "config" "configmap-name") }} + - {{ template "common.volume.pvc" (list "data" "pvc-name" .Values.persistence) }} +{{- end -}} +``` + +Output: + +```yaml +... + spec: + volumes: + - configMap: + name: configmap-name + name: config + - name: data + persistentVolumeClaim: + claimName: pvc-name +... +``` + +The `common.volume.pvc` helper uses the following configuration from the `.Values.persistence` object: + +| Value | Description | +| ------------------------- | ----------------------------------------------------- | +| persistence.enabled | If false, creates an `emptyDir` instead | +| persistence.existingClaim | If set, uses this instead of the passed in claim name | + +## Utilities + +### `common.fullname` + +The `common.fullname` template generates a name suitable for the `name:` field +in Kubernetes metadata. It is used like this: + +```yaml +name: {{ template "common.fullname" . }} +``` + +The following different values can influence it: + +```yaml +# By default, fullname uses '{{ .Release.Name }}-{{ .Chart.Name }}'. This +# overrides that and uses the given string instead. +fullnameOverride: "some-name" + +# This adds a prefix +fullnamePrefix: "pre-" +# This appends a suffix +fullnameSuffix: "-suf" + +# Global versions of the above +global: + fullnamePrefix: "pp-" + fullnameSuffix: "-ps" +``` + +Example output: + +```yaml +--- +# with the values above +name: pp-pre-some-name-suf-ps + +--- +# the default, for release "happy-panda" and chart "wordpress" +name: happy-panda-wordpress +``` + +Output of this function is truncated at 54 characters, which leaves 9 additional +characters for customized overriding. Thus you can easily extend this name +in your own charts: + +```yaml +{{- define "my.fullname" -}} + {{ template "common.fullname" . }}-my-stuff +{{- end -}} +``` + +### `common.fullname.unique` + +The `common.fullname.unique` variant of fullname appends a unique seven-character +sequence to the end of the common name field. + +This takes all of the same parameters as `common.fullname` + +Example template: + +```yaml +uniqueName: {{ template "common.fullname.unique" . }} +``` + +Example output: + +```yaml +uniqueName: release-name-fullname-jl0dbwx +``` + +It is also impacted by the prefix and suffix definitions, as well as by +`.Values.fullnameOverride` + +Note that the effective maximum length of this function is 63 characters, not 54. + +### `common.name` + +The `common.name` template generates a name suitable for the `app` label. It is used like this: + +```yaml +app: {{ template "common.name" . }} +``` + +The following different values can influence it: + +```yaml +# By default, name uses '{{ .Chart.Name }}'. This +# overrides that and uses the given string instead. +nameOverride: "some-name" + +# This adds a prefix +namePrefix: "pre-" +# This appends a suffix +nameSuffix: "-suf" + +# Global versions of the above +global: + namePrefix: "pp-" + nameSuffix: "-ps" +``` + +Example output: + +```yaml +--- +# with the values above +name: pp-pre-some-name-suf-ps + +--- +# the default, for chart "wordpress" +name: wordpress +``` + +Output of this function is truncated at 54 characters, which leaves 9 additional +characters for customized overriding. Thus you can easily extend this name +in your own charts: + +```yaml +{{- define "my.name" -}} + {{ template "common.name" . }}-my-stuff +{{- end -}} +``` + +### `common.metadata` + +The `common.metadata` helper generates the `metadata:` section of a Kubernetes +resource. + +This takes three objects: + - .top: top context + - .fullnameOverride: override the fullname with this name + - .metadata + - .labels: key/value list of labels + - .annotations: key/value list of annotations + - .hook: name(s) of hook(s) + +It generates standard labels, annotations, hooks, and a name field. + +Example template: + +```yaml +{{ template "common.metadata" (dict "top" . "metadata" .Values.bio) }} +--- +{{ template "common.metadata" (dict "top" . "metadata" .Values.pet "fullnameOverride" .Values.pet.fullnameOverride) }} +``` + +Example values: + +```yaml +bio: + name: example + labels: + first: matt + last: butcher + nick: technosophos + annotations: + format: bio + destination: archive + hook: pre-install + +pet: + fullnameOverride: Zeus + +``` + +Example output: + +```yaml +metadata: + name: release-name-metadata + labels: + app.kubernetes.io/name: metadata + app.kubernetes.io/managed-by: "Helm" + app.kubernetes.io/instance: "RELEASE-NAME" + helm.sh/chart: metadata-0.1.0 + first: "matt" + last: "butcher" + nick: "technosophos" + annotations: + "destination": "archive" + "format": "bio" + "helm.sh/hook": "pre-install" +--- +metadata: + name: Zeus + labels: + app.kubernetes.io/name: metadata + app.kubernetes.io/managed-by: "Helm" + app.kubernetes.io/instance: "RELEASE-NAME" + helm.sh/chart: metadata-0.1.0 + annotations: +``` + +Most of the common templates that define a resource type (e.g. `common.configmap` +or `common.job`) use this to generate the metadata, which means they inherit +the same `labels`, `annotations`, `nameOverride`, and `hook` fields. + +### `common.labelize` + +`common.labelize` turns a map into a set of labels. + +Example template: + +```yaml +{{- $map := dict "first" "1" "second" "2" "third" "3" -}} +{{- template "common.labelize" $map -}} +``` + +Example output: + +```yaml +first: "1" +second: "2" +third: "3" +``` + +### `common.labels.standard` + +`common.labels.standard` prints the standard set of labels. + +Example usage: + +``` +{{ template "common.labels.standard" . }} +``` + +Example output: + +```yaml +app.kubernetes.io/name: labelizer +app.kubernetes.io/managed-by: "Tiller" +app.kubernetes.io/instance: "RELEASE-NAME" +helm.sh/chart: labelizer-0.1.0 +``` + +### `common.hook` + +The `common.hook` template is a convenience for defining hooks. + +Example template: + +```yaml +{{ template "common.hook" "pre-install,post-install" }} +``` + +Example output: + +```yaml +"helm.sh/hook": "pre-install,post-install" +``` + +### `common.chartref` + +The `common.chartref` helper prints the chart name and version, escaped to be +legal in a Kubernetes label field. + +Example template: + +```yaml +chartref: {{ template "common.chartref" . }} +``` + +For the chart `foo` with version `1.2.3-beta.55+1234`, this will render: + +```yaml +chartref: foo-1.2.3-beta.55_1234 +``` + +(Note that `+` is an illegal character in label values) diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_chartref.tpl b/cmd/helm/testdata/testcharts/lib-chart/templates/_chartref.tpl new file mode 100644 index 00000000000..e6c14866fe1 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_chartref.tpl @@ -0,0 +1,14 @@ +{{- /* +common.chartref prints a chart name and version. + +It does minimal escaping for use in Kubernetes labels. + +Example output: + + zookeeper-1.2.3 + wordpress-3.2.1_20170219 + +*/ -}} +{{- define "common.chartref" -}} + {{- replace "+" "_" .Chart.Version | printf "%s-%s" .Chart.Name -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_configmap.yaml b/cmd/helm/testdata/testcharts/lib-chart/templates/_configmap.yaml new file mode 100644 index 00000000000..03dbbf858ad --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_configmap.yaml @@ -0,0 +1,9 @@ +{{- define "common.configmap.tpl" -}} +apiVersion: v1 +kind: ConfigMap +{{ template "common.metadata" . }} +data: {} +{{- end -}} +{{- define "common.configmap" -}} +{{- template "common.util.merge" (append . "common.configmap.tpl") -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_container.yaml b/cmd/helm/testdata/testcharts/lib-chart/templates/_container.yaml new file mode 100644 index 00000000000..540eb0e6ab3 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_container.yaml @@ -0,0 +1,15 @@ +{{- define "common.container.tpl" -}} +name: {{ .Chart.Name }} +image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" +imagePullPolicy: {{ .Values.image.pullPolicy }} +ports: +- name: http + containerPort: 80 +resources: +{{ toYaml .Values.resources | indent 2 }} +{{- end -}} +{{- define "common.container" -}} +{{- /* clear new line so indentation works correctly */ -}} +{{- println "" -}} +{{- include "common.util.merge" (append . "common.container.tpl") | indent 8 -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_deployment.yaml b/cmd/helm/testdata/testcharts/lib-chart/templates/_deployment.yaml new file mode 100644 index 00000000000..e99a8cd33c6 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_deployment.yaml @@ -0,0 +1,18 @@ +{{- define "common.deployment.tpl" -}} +apiVersion: extensions/v1beta1 +kind: Deployment +{{ template "common.metadata" . }} +spec: + template: + metadata: + labels: + app.kubernetes.io/name: {{ template "common.name" . }} + app.kubernetes.io/instance: {{ .Release.Name | quote }} + spec: + containers: + - +{{ include "common.container.tpl" . | indent 8 }} +{{- end -}} +{{- define "common.deployment" -}} +{{- template "common.util.merge" (append . "common.deployment.tpl") -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_envvar.tpl b/cmd/helm/testdata/testcharts/lib-chart/templates/_envvar.tpl new file mode 100644 index 00000000000..709251f8f3c --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_envvar.tpl @@ -0,0 +1,31 @@ +{{- define "common.envvar.value" -}} + {{- $name := index . 0 -}} + {{- $value := index . 1 -}} + + name: {{ $name }} + value: {{ default "" $value | quote }} +{{- end -}} + +{{- define "common.envvar.configmap" -}} + {{- $name := index . 0 -}} + {{- $configMapName := index . 1 -}} + {{- $configMapKey := index . 2 -}} + + name: {{ $name }} + valueFrom: + configMapKeyRef: + name: {{ $configMapName }} + key: {{ $configMapKey }} +{{- end -}} + +{{- define "common.envvar.secret" -}} + {{- $name := index . 0 -}} + {{- $secretName := index . 1 -}} + {{- $secretKey := index . 2 -}} + + name: {{ $name }} + valueFrom: + secretKeyRef: + name: {{ $secretName }} + key: {{ $secretKey }} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_fullname.tpl b/cmd/helm/testdata/testcharts/lib-chart/templates/_fullname.tpl new file mode 100644 index 00000000000..2da6cdf18cf --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_fullname.tpl @@ -0,0 +1,39 @@ +{{- /* +fullname defines a suitably unique name for a resource by combining +the release name and the chart name. + +The prevailing wisdom is that names should only contain a-z, 0-9 plus dot (.) and dash (-), and should +not exceed 63 characters. + +Parameters: + +- .Values.fullnameOverride: Replaces the computed name with this given name +- .Values.fullnamePrefix: Prefix +- .Values.global.fullnamePrefix: Global prefix +- .Values.fullnameSuffix: Suffix +- .Values.global.fullnameSuffix: Global suffix + +The applied order is: "global prefix + prefix + name + suffix + global suffix" + +Usage: 'name: "{{- template "common.fullname" . -}}"' +*/ -}} +{{- define "common.fullname"}} + {{- $global := default (dict) .Values.global -}} + {{- $base := default (printf "%s-%s" .Release.Name .Chart.Name) .Values.fullnameOverride -}} + {{- $gpre := default "" $global.fullnamePrefix -}} + {{- $pre := default "" .Values.fullnamePrefix -}} + {{- $suf := default "" .Values.fullnameSuffix -}} + {{- $gsuf := default "" $global.fullnameSuffix -}} + {{- $name := print $gpre $pre $base $suf $gsuf -}} + {{- $name | lower | trunc 54 | trimSuffix "-" -}} +{{- end -}} + +{{- /* +common.fullname.unique adds a random suffix to the unique name. + +This takes the same parameters as common.fullname + +*/ -}} +{{- define "common.fullname.unique" -}} + {{ template "common.fullname" . }}-{{ randAlphaNum 7 | lower }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_ingress.yaml b/cmd/helm/testdata/testcharts/lib-chart/templates/_ingress.yaml new file mode 100644 index 00000000000..78411e15b9f --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_ingress.yaml @@ -0,0 +1,27 @@ +{{- define "common.ingress.tpl" -}} +apiVersion: extensions/v1beta1 +kind: Ingress +{{ template "common.metadata" . }} + {{- if .Values.ingress.annotations }} + annotations: + {{ include "common.annotate" .Values.ingress.annotations | indent 4 }} + {{- end }} +spec: + rules: + {{- range $host := .Values.ingress.hosts }} + - host: {{ $host }} + http: + paths: + - path: / + backend: + serviceName: {{ template "common.fullname" $ }} + servicePort: 80 + {{- end }} + {{- if .Values.ingress.tls }} + tls: +{{ toYaml .Values.ingress.tls | indent 4 }} + {{- end -}} +{{- end -}} +{{- define "common.ingress" -}} +{{- template "common.util.merge" (append . "common.ingress.tpl") -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata.yaml b/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata.yaml new file mode 100644 index 00000000000..f96ed09fe59 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata.yaml @@ -0,0 +1,10 @@ +{{- /* +common.metadata creates a standard metadata header. +It creates a 'metadata:' section with name and labels. +*/ -}} +{{ define "common.metadata" -}} +metadata: + name: {{ template "common.fullname" . }} + labels: +{{ include "common.labels.standard" . | indent 4 -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata_annotations.tpl b/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata_annotations.tpl new file mode 100644 index 00000000000..dffe1eca90d --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata_annotations.tpl @@ -0,0 +1,18 @@ +{{- /* +common.hook defines a hook. + +This is to be used in a 'metadata.annotations' section. + +This should be called as 'template "common.metadata.hook" "post-install"' + +Any valid hook may be passed in. Separate multiple hooks with a ",". +*/ -}} +{{- define "common.hook" -}} +"helm.sh/hook": {{printf "%s" . | quote}} +{{- end -}} + +{{- define "common.annotate" -}} +{{- range $k, $v := . }} +{{ $k | quote }}: {{ $v | quote }} +{{- end -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata_labels.tpl b/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata_labels.tpl new file mode 100644 index 00000000000..bcb8cdaa829 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata_labels.tpl @@ -0,0 +1,28 @@ +{{- /* +common.labelize takes a dict or map and generates labels. + +Values will be quoted. Keys will not. + +Example output: + + first: "Matt" + last: "Butcher" + +*/ -}} +{{- define "common.labelize" -}} +{{- range $k, $v := . }} +{{ $k }}: {{ $v | quote }} +{{- end -}} +{{- end -}} + +{{- /* +common.labels.standard prints the standard Helm labels. + +The standard labels are frequently used in metadata. +*/ -}} +{{- define "common.labels.standard" -}} +app.kubernetes.io/name: {{ template "common.name" . }} +helm.sh/chart: {{ template "common.chartref" . }} +app.kubernetes.io/managed-by: {{ .Release.Service | quote }} +app.kubernetes.io/instance: {{ .Release.Name | quote }} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_name.tpl b/cmd/helm/testdata/testcharts/lib-chart/templates/_name.tpl new file mode 100644 index 00000000000..1d42fb06820 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_name.tpl @@ -0,0 +1,29 @@ +{{- /* +name defines a template for the name of the chart. It should be used for the `app` label. +This is common practice in many Kubernetes manifests, and is not Helm-specific. + +The prevailing wisdom is that names should only contain a-z, 0-9 plus dot (.) and dash (-), and should +not exceed 63 characters. + +Parameters: + +- .Values.nameOverride: Replaces the computed name with this given name +- .Values.namePrefix: Prefix +- .Values.global.namePrefix: Global prefix +- .Values.nameSuffix: Suffix +- .Values.global.nameSuffix: Global suffix + +The applied order is: "global prefix + prefix + name + suffix + global suffix" + +Usage: 'name: "{{- template "common.name" . -}}"' +*/ -}} +{{- define "common.name"}} + {{- $global := default (dict) .Values.global -}} + {{- $base := default .Chart.Name .Values.nameOverride -}} + {{- $gpre := default "" $global.namePrefix -}} + {{- $pre := default "" .Values.namePrefix -}} + {{- $suf := default "" .Values.nameSuffix -}} + {{- $gsuf := default "" $global.nameSuffix -}} + {{- $name := print $gpre $pre $base $suf $gsuf -}} + {{- $name | lower | trunc 54 | trimSuffix "-" -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_persistentvolumeclaim.yaml b/cmd/helm/testdata/testcharts/lib-chart/templates/_persistentvolumeclaim.yaml new file mode 100644 index 00000000000..6c1578c7ed5 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_persistentvolumeclaim.yaml @@ -0,0 +1,24 @@ +{{- define "common.persistentvolumeclaim.tpl" -}} +apiVersion: v1 +kind: PersistentVolumeClaim +{{ template "common.metadata" . }} +spec: + accessModes: + - {{ .Values.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- if .Values.persistence.storageClass }} +{{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" +{{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" +{{- end }} +{{- end }} +{{- end -}} +{{- define "common.persistentvolumeclaim" -}} +{{- $top := first . -}} +{{- if and $top.Values.persistence.enabled (not $top.Values.persistence.existingClaim) -}} +{{- template "common.util.merge" (append . "common.persistentvolumeclaim.tpl") -}} +{{- end -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_secret.yaml b/cmd/helm/testdata/testcharts/lib-chart/templates/_secret.yaml new file mode 100644 index 00000000000..0615d35cb41 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_secret.yaml @@ -0,0 +1,10 @@ +{{- define "common.secret.tpl" -}} +apiVersion: v1 +kind: Secret +{{ template "common.metadata" . }} +type: Opaque +data: {} +{{- end -}} +{{- define "common.secret" -}} +{{- template "common.util.merge" (append . "common.secret.tpl") -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_service.yaml b/cmd/helm/testdata/testcharts/lib-chart/templates/_service.yaml new file mode 100644 index 00000000000..b9dfc378af8 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_service.yaml @@ -0,0 +1,17 @@ +{{- define "common.service.tpl" -}} +apiVersion: v1 +kind: Service +{{ template "common.metadata" . }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: 80 + targetPort: http + selector: + app.kubernetes.io/name: {{ template "common.name" . }} + app.kubernetes.io/instance: {{ .Release.Name | quote }} +{{- end -}} +{{- define "common.service" -}} +{{- template "common.util.merge" (append . "common.service.tpl") -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_util.tpl b/cmd/helm/testdata/testcharts/lib-chart/templates/_util.tpl new file mode 100644 index 00000000000..a7d4cc751e3 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_util.tpl @@ -0,0 +1,15 @@ +{{- /* +common.util.merge will merge two YAML templates and output the result. + +This takes an array of three values: +- the top context +- the template name of the overrides (destination) +- the template name of the base (source) + +*/ -}} +{{- define "common.util.merge" -}} +{{- $top := first . -}} +{{- $overrides := fromYaml (include (index . 1) $top) | default (dict ) -}} +{{- $tpl := fromYaml (include (index . 2) $top) | default (dict ) -}} +{{- toYaml (merge $overrides $tpl) -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/templates/_volume.tpl b/cmd/helm/testdata/testcharts/lib-chart/templates/_volume.tpl new file mode 100644 index 00000000000..521a1f48b00 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/templates/_volume.tpl @@ -0,0 +1,22 @@ +{{- define "common.volume.configMap" -}} + {{- $name := index . 0 -}} + {{- $configMapName := index . 1 -}} + + name: {{ $name }} + configMap: + name: {{ $configMapName }} +{{- end -}} + +{{- define "common.volume.pvc" -}} + {{- $name := index . 0 -}} + {{- $claimName := index . 1 -}} + {{- $persistence := index . 2 -}} + + name: {{ $name }} + {{- if $persistence.enabled }} + persistentVolumeClaim: + claimName: {{ $persistence.existingClaim | default $claimName }} + {{- else }} + emptyDir: {} + {{- end -}} +{{- end -}} diff --git a/cmd/helm/testdata/testcharts/lib-chart/values.yaml b/cmd/helm/testdata/testcharts/lib-chart/values.yaml new file mode 100644 index 00000000000..b7cf514d567 --- /dev/null +++ b/cmd/helm/testdata/testcharts/lib-chart/values.yaml @@ -0,0 +1,4 @@ +# Default values for commons. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name: value diff --git a/cmd/helm/testdata/testcharts/novals/Chart.yaml b/cmd/helm/testdata/testcharts/novals/Chart.yaml deleted file mode 100644 index ce1a81da679..00000000000 --- a/cmd/helm/testdata/testcharts/novals/Chart.yaml +++ /dev/null @@ -1,6 +0,0 @@ -description: Deploy a basic Alpine Linux pod -home: https://k8s.io/helm -name: novals -sources: -- https://github.com/kubernetes/helm -version: 0.2.0 diff --git a/cmd/helm/testdata/testcharts/novals/README.md b/cmd/helm/testdata/testcharts/novals/README.md deleted file mode 100644 index 3c32de5db6a..00000000000 --- a/cmd/helm/testdata/testcharts/novals/README.md +++ /dev/null @@ -1,13 +0,0 @@ -#Alpine: A simple Helm chart - -Run a single pod of Alpine Linux. - -This example was generated using the command `helm create alpine`. - -The `templates/` directory contains a very simple pod resource with a -couple of parameters. - -The `values.yaml` file contains the default values for the -`alpine-pod.yaml` template. - -You can install this example using `helm install docs/examples/alpine`. diff --git a/cmd/helm/testdata/testcharts/novals/templates/alpine-pod.yaml b/cmd/helm/testdata/testcharts/novals/templates/alpine-pod.yaml deleted file mode 100644 index c15ab8efc0f..00000000000 --- a/cmd/helm/testdata/testcharts/novals/templates/alpine-pod.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{.Release.Name}}-{{.Values.Name}}" - labels: - # The "heritage" label is used to track which tool deployed a given chart. - # It is useful for admins who want to see what releases a particular tool - # is responsible for. - heritage: {{.Release.Service | quote }} - # The "release" convention makes it easy to tie a release to all of the - # Kubernetes resources that were created as part of that release. - release: {{.Release.Name | quote }} - # This makes it easy to audit chart usage. - chart: "{{.Chart.Name}}-{{.Chart.Version}}" - annotations: - "helm.sh/created": {{.Release.Time.Seconds | quote }} -spec: - # This shows how to use a simple value. This will look for a passed-in value - # called restartPolicy. If it is not found, it will use the default value. - # {{default "Never" .restartPolicy}} is a slightly optimized version of the - # more conventional syntax: {{.restartPolicy | default "Never"}} - restartPolicy: {{default "Never" .Values.restartPolicy}} - containers: - - name: waiter - image: "alpine:3.3" - command: ["/bin/sleep","9000"] diff --git a/cmd/helm/testdata/testcharts/object-order/Chart.yaml b/cmd/helm/testdata/testcharts/object-order/Chart.yaml new file mode 100644 index 00000000000..d2eb42fd721 --- /dev/null +++ b/cmd/helm/testdata/testcharts/object-order/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: object-order +description: Test ordering of manifests in output +type: application +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/object-order/templates/01-a.yml b/cmd/helm/testdata/testcharts/object-order/templates/01-a.yml new file mode 100644 index 00000000000..32aa4a47525 --- /dev/null +++ b/cmd/helm/testdata/testcharts/object-order/templates/01-a.yml @@ -0,0 +1,57 @@ +# 1 +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: first +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 2 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: second +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 3 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: third +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 4 (Deployment should come after all NetworkPolicy manifests, since 'helm template' outputs in install order) +apiVersion: apps/v1 +kind: Deployment +metadata: + name: fourth +spec: + selector: + matchLabels: + pod: fourth + replicas: 1 + template: + metadata: + labels: + pod: fourth + spec: + containers: + - name: hello-world + image: gcr.io/google-samples/node-hello:1.0 diff --git a/cmd/helm/testdata/testcharts/object-order/templates/02-b.yml b/cmd/helm/testdata/testcharts/object-order/templates/02-b.yml new file mode 100644 index 00000000000..895db8cf762 --- /dev/null +++ b/cmd/helm/testdata/testcharts/object-order/templates/02-b.yml @@ -0,0 +1,143 @@ +# 5 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fifth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 6 (implementation detail: currently, 'helm template' outputs hook manifests last; and yes, NetworkPolicy won't make a reasonable hook, this is just a dummy unit test manifest) +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + annotations: + "helm.sh/hook": pre-install + name: sixth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 7 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: seventh +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 8 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: eighth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 9 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: ninth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 10 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: tenth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 11 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: eleventh +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 12 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: twelfth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 13 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: thirteenth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 14 +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fourteenth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress + +--- + +# 15 (11th object within 02-b.yml, in order to test `SplitManifests` which assigns `manifest-10` +# to this object which should then come *after* `manifest-9`) +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: fifteenth +spec: + podSelector: {} + policyTypes: + - Egress + - Ingress diff --git a/pkg/ignore/testdata/cargo/b.txt b/cmd/helm/testdata/testcharts/object-order/values.yaml similarity index 100% rename from pkg/ignore/testdata/cargo/b.txt rename to cmd/helm/testdata/testcharts/object-order/values.yaml diff --git a/cmd/helm/testdata/testcharts/oci-dependent-chart-0.1.0.tgz b/cmd/helm/testdata/testcharts/oci-dependent-chart-0.1.0.tgz new file mode 100644 index 00000000000..7b4cbeccc1c Binary files /dev/null and b/cmd/helm/testdata/testcharts/oci-dependent-chart-0.1.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/pre-release-chart-0.1.0-alpha.tgz b/cmd/helm/testdata/testcharts/pre-release-chart-0.1.0-alpha.tgz new file mode 100644 index 00000000000..5d5770fed22 Binary files /dev/null and b/cmd/helm/testdata/testcharts/pre-release-chart-0.1.0-alpha.tgz differ diff --git a/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz b/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz index 356bc930303..5d8e46a5080 100644 Binary files a/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz and b/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/reqtest/requirements.lock b/cmd/helm/testdata/testcharts/reqtest/Chart.lock similarity index 100% rename from cmd/helm/testdata/testcharts/reqtest/requirements.lock rename to cmd/helm/testdata/testcharts/reqtest/Chart.lock diff --git a/cmd/helm/testdata/testcharts/reqtest/Chart.yaml b/cmd/helm/testdata/testcharts/reqtest/Chart.yaml index e2fbe4b016f..07b6e2c9770 100644 --- a/cmd/helm/testdata/testcharts/reqtest/Chart.yaml +++ b/cmd/helm/testdata/testcharts/reqtest/Chart.yaml @@ -1,3 +1,14 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: reqtest version: 0.1.0 +dependencies: + - name: reqsubchart + version: 0.1.0 + repository: "https://example.com/charts" + - name: reqsubchart2 + version: 0.2.0 + repository: "https://example.com/charts" + - name: reqsubchart3 + version: ">=0.1.0" + repository: "https://example.com/charts" diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml index c3813bc8c2f..356135537aa 100644 --- a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml +++ b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: reqsubchart version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml index 9f7c22a71bc..5b927737087 100644 --- a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml +++ b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: reqsubchart2 version: 0.2.0 diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz index 84b0fb65e6b..37962b0abcf 100644 Binary files a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz and b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/reqtest/requirements.yaml b/cmd/helm/testdata/testcharts/reqtest/requirements.yaml deleted file mode 100644 index 1ddedc7427c..00000000000 --- a/cmd/helm/testdata/testcharts/reqtest/requirements.yaml +++ /dev/null @@ -1,10 +0,0 @@ -dependencies: - - name: reqsubchart - version: 0.1.0 - repository: "https://example.com/charts" - - name: reqsubchart2 - version: 0.2.0 - repository: "https://example.com/charts" - - name: reqsubchart3 - version: ">=0.1.0" - repository: "https://example.com/charts" diff --git a/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz index 6de9d988d47..c74e5b0ef5c 100644 Binary files a/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz and b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov index 94235399ae0..d325bb266e6 100644 --- a/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov +++ b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov @@ -1,20 +1,21 @@ -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 +apiVersion: v1 description: A Helm chart for Kubernetes name: signtest version: 0.1.0 ... files: - signtest-0.1.0.tgz: sha256:dee72947753628425b82814516bdaa37aef49f25e8820dd2a6e15a33a007823b + signtest-0.1.0.tgz: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55 -----BEGIN PGP SIGNATURE----- -wsBcBAEBCgAQBQJXomNHCRCEO7+YH8GHYgAALywIAG1Me852Fpn1GYu8Q1GCcw4g -l2k7vOFchdDwDhdSVbkh4YyvTaIO3iE2Jtk1rxw+RIJiUr0eLO/rnIJuxZS8WKki -DR1LI9J1VD4dxN3uDETtWDWq7ScoPsRY5mJvYZXC8whrWEt/H2kfqmoA9LloRPWp -flOE0iktA4UciZOblTj6nAk3iDyjh/4HYL4a6tT0LjjKI7OTw4YyHfjHad1ywVCz -9dMUc1rPgTnl+fnRiSPSrlZIWKOt1mcQ4fVrU3nwtRUwTId2k8FtygL0G6M+Y6t0 -S6yaU7qfk9uTxkdkUF7Bf1X3ukxfe+cNBC32vf4m8LY4NkcYfSqK2fGtQsnVr6s= -=NyOM +wsBcBAEBCgAQBQJcoosfCRCEO7+YH8GHYgAA220IALAs8T8NPgkcLvHu+5109cAN +BOCNPSZDNsqLZW/2Dc9cKoBG7Jen4Qad+i5l9351kqn3D9Gm6eRfAWcjfggRobV/ +9daZ19h0nl4O1muQNAkjvdgZt8MOP3+PB3I3/Tu2QCYjI579SLUmuXlcZR5BCFPR +PJy+e3QpV2PcdeU2KZLG4tjtlrq+3QC9ZHHEJLs+BVN9d46Dwo6CxJdHJrrrAkTw +M8MhA92vbiTTPRSCZI9x5qDAwJYhoq0oxLflpuL2tIlo3qVoCsaTSURwMESEHO32 +XwYG7BaVDMELWhAorBAGBGBwWFbJ1677qQ2gd9CN0COiVhekWlFRcnn60800r84= +=k9Y9 -----END PGP SIGNATURE----- \ No newline at end of file diff --git a/cmd/helm/testdata/testcharts/signtest/Chart.yaml b/cmd/helm/testdata/testcharts/signtest/Chart.yaml index 90964b44a60..f1f73723a86 100644 --- a/cmd/helm/testdata/testcharts/signtest/Chart.yaml +++ b/cmd/helm/testdata/testcharts/signtest/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: signtest version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml b/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml index 6fbb27f1811..eec261220ec 100644 --- a/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml +++ b/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml @@ -1,6 +1,7 @@ +apiVersion: v1 description: Deploy a basic Alpine Linux pod -home: https://k8s.io/helm +home: https://helm.sh/helm name: alpine sources: -- https://github.com/kubernetes/helm +- https://github.com/helm/helm version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/signtest/alpine/README.md b/cmd/helm/testdata/testcharts/signtest/alpine/README.md index 5bd595747e4..28bebae070e 100644 --- a/cmd/helm/testdata/testcharts/signtest/alpine/README.md +++ b/cmd/helm/testdata/testcharts/signtest/alpine/README.md @@ -6,4 +6,4 @@ couple of parameters. The `values.yaml` file contains the default values for the `alpine-pod.yaml` template. -You can install this example using `helm install docs/examples/alpine`. +You can install this example using `helm install ./alpine`. diff --git a/cmd/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml b/cmd/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml index 08cf3c2c1f5..5bbae10afb3 100644 --- a/cmd/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml +++ b/cmd/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml @@ -3,11 +3,9 @@ kind: Pod metadata: name: {{.Release.Name}}-{{.Chart.Name}} labels: - heritage: {{.Release.Service}} + app.kubernetes.io/managed-by: {{.Release.Service}} chartName: {{.Chart.Name}} chartVersion: {{.Chart.Version | quote}} - annotations: - "helm.sh/created": "{{.Release.Time.Seconds}}" spec: restartPolicy: {{default "Never" .restart_policy}} containers: diff --git a/cmd/helm/testdata/testcharts/subchart/Chart.yaml b/cmd/helm/testdata/testcharts/subchart/Chart.yaml new file mode 100644 index 00000000000..b03ea3cd32d --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/Chart.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +description: A Helm chart for Kubernetes +name: subchart +version: 0.1.0 +dependencies: + - name: subcharta + repository: http://localhost:10191 + version: 0.1.0 + condition: subcharta.enabled + tags: + - front-end + - subcharta + import-values: + - child: SCAdata + parent: imported-chartA + - child: SCAdata + parent: overridden-chartA + - child: SCAdata + parent: imported-chartA-B + + - name: subchartb + repository: http://localhost:10191 + version: 0.1.0 + condition: subchartb.enabled + import-values: + - child: SCBdata + parent: imported-chartB + - child: SCBdata + parent: imported-chartA-B + - child: exports.SCBexported2 + parent: exports.SCBexported2 + - SCBexported1 + + tags: + - front-end + - subchartb diff --git a/cmd/helm/testdata/testcharts/subchart/charts/subchartA/Chart.yaml b/cmd/helm/testdata/testcharts/subchart/charts/subchartA/Chart.yaml new file mode 100644 index 00000000000..be3edcefbff --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/charts/subchartA/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: A Helm chart for Kubernetes +name: subcharta +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/subchart/charts/subchartA/templates/service.yaml b/cmd/helm/testdata/testcharts/subchart/charts/subchartA/templates/service.yaml new file mode 100644 index 00000000000..27501e1e0b2 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/charts/subchartA/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/cmd/helm/testdata/testcharts/subchart/charts/subchartA/values.yaml b/cmd/helm/testdata/testcharts/subchart/charts/subchartA/values.yaml new file mode 100644 index 00000000000..f0381ae6ab7 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/charts/subchartA/values.yaml @@ -0,0 +1,17 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +# subchartA +service: + name: apache + type: ClusterIP + externalPort: 80 + internalPort: 80 +SCAdata: + SCAbool: false + SCAfloat: 3.1 + SCAint: 55 + SCAstring: "jabba" + SCAnested1: + SCAnested2: true + diff --git a/cmd/helm/testdata/testcharts/subchart/charts/subchartB/Chart.yaml b/cmd/helm/testdata/testcharts/subchart/charts/subchartB/Chart.yaml new file mode 100644 index 00000000000..c3c6bbaf047 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/charts/subchartB/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: A Helm chart for Kubernetes +name: subchartb +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/subchart/charts/subchartB/templates/service.yaml b/cmd/helm/testdata/testcharts/subchart/charts/subchartB/templates/service.yaml new file mode 100644 index 00000000000..27501e1e0b2 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/charts/subchartB/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/cmd/helm/testdata/testcharts/subchart/charts/subchartB/values.yaml b/cmd/helm/testdata/testcharts/subchart/charts/subchartB/values.yaml new file mode 100644 index 00000000000..774fdd75cff --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/charts/subchartB/values.yaml @@ -0,0 +1,35 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 + +SCBdata: + SCBbool: true + SCBfloat: 7.77 + SCBint: 33 + SCBstring: "boba" + +exports: + SCBexported1: + SCBexported1A: + SCBexported1B: 1965 + + SCBexported2: + SCBexported2A: "blaster" + +global: + kolla: + nova: + api: + all: + port: 8774 + metadata: + all: + port: 8775 + + + diff --git a/cmd/helm/testdata/testcharts/subchart/crds/crdA.yaml b/cmd/helm/testdata/testcharts/subchart/crds/crdA.yaml new file mode 100644 index 00000000000..ad770b63291 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/crds/crdA.yaml @@ -0,0 +1,14 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: testcrds.testcrdgroups.example.com +spec: + group: testcrdgroups.example.com + version: v1alpha1 + names: + kind: TestCRD + listKind: TestCRDList + plural: testcrds + shortNames: + - tc + singular: authconfig diff --git a/cmd/helm/testdata/testcharts/subchart/templates/NOTES.txt b/cmd/helm/testdata/testcharts/subchart/templates/NOTES.txt new file mode 100644 index 00000000000..4bdf443f60c --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/templates/NOTES.txt @@ -0,0 +1 @@ +Sample notes for {{ .Chart.Name }} \ No newline at end of file diff --git a/cmd/helm/testdata/testcharts/subchart/templates/service.yaml b/cmd/helm/testdata/testcharts/subchart/templates/service.yaml new file mode 100644 index 00000000000..fee94dced9b --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/templates/service.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + app.kubernetes.io/instance: "{{ .Release.Name }}" + kube-version/major: "{{ .Capabilities.KubeVersion.Major }}" + kube-version/minor: "{{ .Capabilities.KubeVersion.Minor }}" + kube-version/version: "v{{ .Capabilities.KubeVersion.Major }}.{{ .Capabilities.KubeVersion.Minor }}.0" +{{- if .Capabilities.APIVersions.Has "helm.k8s.io/test" }} + kube-api-version/test: v1 +{{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/cmd/helm/testdata/testcharts/subchart/templates/subdir/role.yaml b/cmd/helm/testdata/testcharts/subchart/templates/subdir/role.yaml new file mode 100644 index 00000000000..31cff920029 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/templates/subdir/role.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Chart.Name }}-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","list","watch"] diff --git a/cmd/helm/testdata/testcharts/subchart/templates/subdir/rolebinding.yaml b/cmd/helm/testdata/testcharts/subchart/templates/subdir/rolebinding.yaml new file mode 100644 index 00000000000..5d193f1a670 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/templates/subdir/rolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Chart.Name }}-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Chart.Name }}-role +subjects: +- kind: ServiceAccount + name: {{ .Chart.Name }}-sa + namespace: default diff --git a/cmd/helm/testdata/testcharts/subchart/templates/subdir/serviceaccount.yaml b/cmd/helm/testdata/testcharts/subchart/templates/subdir/serviceaccount.yaml new file mode 100644 index 00000000000..7126c7d89bc --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/templates/subdir/serviceaccount.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Chart.Name }}-sa diff --git a/cmd/helm/testdata/testcharts/subchart/templates/tests/test-config.yaml b/cmd/helm/testdata/testcharts/subchart/templates/tests/test-config.yaml new file mode 100644 index 00000000000..0aa3eea2948 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/templates/tests/test-config.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ .Release.Name }}-testconfig" + annotations: + "helm.sh/hook": test +data: + message: Hello World diff --git a/cmd/helm/testdata/testcharts/subchart/templates/tests/test-nothing.yaml b/cmd/helm/testdata/testcharts/subchart/templates/tests/test-nothing.yaml new file mode 100644 index 00000000000..0fe6dbbf3a6 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/templates/tests/test-nothing.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ .Release.Name }}-test" + annotations: + "helm.sh/hook": test +spec: + containers: + - name: test + image: "alpine:latest" + envFrom: + - configMapRef: + name: "{{ .Release.Name }}-testconfig" + command: + - echo + - "$message" + restartPolicy: Never diff --git a/cmd/helm/testdata/testcharts/subchart/values.yaml b/cmd/helm/testdata/testcharts/subchart/values.yaml new file mode 100644 index 00000000000..8a3ab6c646c --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/values.yaml @@ -0,0 +1,55 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +# subchart +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 + + +SC1data: + SC1bool: true + SC1float: 3.14 + SC1int: 100 + SC1string: "dollywood" + SC1extra1: 11 + +imported-chartA: + SC1extra2: 1.337 + +overridden-chartA: + SCAbool: true + SCAfloat: 3.14 + SCAint: 100 + SCAstring: "jabbathehut" + SC1extra3: true + +imported-chartA-B: + SC1extra5: "tiller" + +overridden-chartA-B: + SCAbool: true + SCAfloat: 3.33 + SCAint: 555 + SCAstring: "wormwood" + SCAextra1: 23 + + SCBbool: true + SCBfloat: 0.25 + SCBint: 98 + SCBstring: "murkwood" + SCBextra1: 13 + + SC1extra6: 77 + +SCBexported1A: + SC1extra7: true + +exports: + SC1exported1: + global: + SC1exported2: + all: + SC1exported3: "SC1expstr" diff --git a/cmd/helm/testdata/testcharts/upgradetest/templates/configmap.yaml b/cmd/helm/testdata/testcharts/upgradetest/templates/configmap.yaml new file mode 100644 index 00000000000..b6b90efbaa9 --- /dev/null +++ b/cmd/helm/testdata/testcharts/upgradetest/templates/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{ .Release.Name }}-configmap" +data: + myvalue: "Hello World" + drink: {{ .Values.favoriteDrink }} \ No newline at end of file diff --git a/cmd/helm/testdata/testcharts/upgradetest/values.yaml b/cmd/helm/testdata/testcharts/upgradetest/values.yaml new file mode 100644 index 00000000000..c429f41f449 --- /dev/null +++ b/cmd/helm/testdata/testcharts/upgradetest/values.yaml @@ -0,0 +1 @@ +favoriteDrink: beer \ No newline at end of file diff --git a/cmd/helm/testdata/testplugin/plugin.yaml b/cmd/helm/testdata/testplugin/plugin.yaml new file mode 100644 index 00000000000..890292cbf51 --- /dev/null +++ b/cmd/helm/testdata/testplugin/plugin.yaml @@ -0,0 +1,4 @@ +name: testplugin +usage: "echo test" +description: "This echos test" +command: "echo test" diff --git a/cmd/helm/uninstall.go b/cmd/helm/uninstall.go new file mode 100644 index 00000000000..f4f5d87e891 --- /dev/null +++ b/cmd/helm/uninstall.go @@ -0,0 +1,78 @@ +/* +Copyright The Helm Authors. + +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" + "time" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" +) + +const uninstallDesc = ` +This command takes a release name and uninstalls the release. + +It removes all of the resources associated with the last release of the chart +as well as the release history, freeing it up for future use. + +Use the '--dry-run' flag to see which releases will be uninstalled without actually +uninstalling them. +` + +func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewUninstall(cfg) + + cmd := &cobra.Command{ + Use: "uninstall RELEASE_NAME [...]", + Aliases: []string{"del", "delete", "un"}, + SuggestFor: []string{"remove", "rm"}, + Short: "uninstall a release", + Long: uninstallDesc, + Args: require.MinimumNArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compListReleases(toComplete, args, cfg) + }, + RunE: func(cmd *cobra.Command, args []string) error { + for i := 0; i < len(args); i++ { + + res, err := client.Run(args[i]) + if err != nil { + return err + } + if res != nil && res.Info != "" { + fmt.Fprintln(out, res.Info) + } + + fmt.Fprintf(out, "release \"%s\" uninstalled\n", args[i]) + } + return nil + }, + } + + f := cmd.Flags() + f.BoolVar(&client.DryRun, "dry-run", false, "simulate a uninstall") + f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation") + f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history") + f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.StringVar(&client.Description, "description", "", "add a custom description") + + return cmd +} diff --git a/cmd/helm/uninstall_test.go b/cmd/helm/uninstall_test.go new file mode 100644 index 00000000000..1a33458c479 --- /dev/null +++ b/cmd/helm/uninstall_test.go @@ -0,0 +1,77 @@ +/* +Copyright The Helm Authors. + +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" + + "helm.sh/helm/v3/pkg/release" +) + +func TestUninstall(t *testing.T) { + tests := []cmdTestCase{ + { + name: "basic uninstall", + cmd: "uninstall aeneas", + golden: "output/uninstall.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, + }, + { + name: "multiple uninstall", + cmd: "uninstall aeneas aeneas2", + golden: "output/uninstall-multiple.txt", + rels: []*release.Release{ + release.Mock(&release.MockReleaseOptions{Name: "aeneas"}), + release.Mock(&release.MockReleaseOptions{Name: "aeneas2"}), + }, + }, + { + name: "uninstall with timeout", + cmd: "uninstall aeneas --timeout 120s", + golden: "output/uninstall-timeout.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, + }, + { + name: "uninstall without hooks", + cmd: "uninstall aeneas --no-hooks", + golden: "output/uninstall-no-hooks.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, + }, + { + name: "keep history", + cmd: "uninstall aeneas --keep-history", + golden: "output/uninstall-keep-history.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, + }, + { + name: "uninstall without release", + cmd: "uninstall", + golden: "output/uninstall-no-args.txt", + wantError: true, + }, + } + runTestCmd(t, tests) +} + +func TestUninstallCompletion(t *testing.T) { + checkReleaseCompletion(t, "uninstall", true) +} + +func TestUninstallFileCompletion(t *testing.T) { + checkFileCompletion(t, "uninstall", false) + checkFileCompletion(t, "uninstall myrelease", false) +} diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 66c4a36577c..1952b842190 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,228 +19,188 @@ package main import ( "fmt" "io" - "strings" + "log" + "time" + "github.com/pkg/errors" "github.com/spf13/cobra" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/storage/driver" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/cli/output" + "helm.sh/helm/v3/pkg/cli/values" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/storage/driver" ) const upgradeDesc = ` This command upgrades a release to a new version of a chart. The upgrade arguments must be a release and chart. The chart -argument can be either: a chart reference('stable/mariadb'), a path to a chart directory, +argument can be either: a chart reference('example/mariadb'), a path to a chart directory, a packaged chart, or a fully qualified URL. For chart references, the latest version will be specified unless the '--version' flag is set. To override values in a chart, use either the '--values' flag and pass in a file or use the '--set' flag and pass configuration from the command line, to force string -values, use '--set-string'. +values, use '--set-string'. In case a value is large and therefore +you want not to use neither '--values' nor '--set', use '--set-file' to read the +single large value from file. You can specify the '--values'/'-f' flag multiple times. The priority will be given to the last (right-most) file specified. For example, if both myvalues.yaml and override.yaml contained a key called 'Test', the value set in override.yaml would take precedence: - $ helm upgrade -f myvalues.yaml -f override.yaml redis ./redis + $ helm upgrade -f myvalues.yaml -f override.yaml redis ./redis You can specify the '--set' flag multiple times. The priority will be given to the last (right-most) set specified. For example, if both 'bar' and 'newbar' values are set for a key called 'foo', the 'newbar' value would take precedence: - $ helm upgrade --set foo=bar --set foo=newbar redis ./redis + $ helm upgrade --set foo=bar --set foo=newbar redis ./redis ` -type upgradeCmd struct { - release string - chart string - out io.Writer - client helm.Interface - dryRun bool - recreate bool - force bool - disableHooks bool - valueFiles valueFiles - values []string - stringValues []string - verify bool - keyring string - install bool - namespace string - version string - timeout int64 - resetValues bool - reuseValues bool - wait bool - repoURL string - username string - password string - devel bool - - certFile string - keyFile string - caFile string -} - -func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { - - upgrade := &upgradeCmd{ - out: out, - client: client, - } +func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewUpgrade(cfg) + valueOpts := &values.Options{} + var outfmt output.Format + var createNamespace bool cmd := &cobra.Command{ - Use: "upgrade [RELEASE] [CHART]", - Short: "upgrade a release", - Long: upgradeDesc, - PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, + Use: "upgrade [RELEASE] [CHART]", + Short: "upgrade a release", + Long: upgradeDesc, + Args: require.ExactArgs(2), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + return compListReleases(toComplete, args, cfg) + } + if len(args) == 1 { + return compListCharts(toComplete, true) + } + return nil, cobra.ShellCompDirectiveNoFileComp + }, RunE: func(cmd *cobra.Command, args []string) error { - if err := checkArgsLength(len(args), "release name", "chart path"); err != nil { - return err + client.Namespace = settings.Namespace() + + // Fixes #7002 - Support reading values from STDIN for `upgrade` command + // Must load values AFTER determining if we have to call install so that values loaded from stdin are are not read twice + if client.Install { + // If a release does not exist, install it. + histClient := action.NewHistory(cfg) + histClient.Max = 1 + if _, err := histClient.Run(args[0]); err == driver.ErrReleaseNotFound { + // Only print this to stdout for table output + if outfmt == output.Table { + fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0]) + } + instClient := action.NewInstall(cfg) + instClient.CreateNamespace = createNamespace + instClient.ChartPathOptions = client.ChartPathOptions + instClient.DryRun = client.DryRun + instClient.DisableHooks = client.DisableHooks + instClient.SkipCRDs = client.SkipCRDs + instClient.Timeout = client.Timeout + instClient.Wait = client.Wait + instClient.WaitForJobs = client.WaitForJobs + instClient.Devel = client.Devel + instClient.Namespace = client.Namespace + instClient.Atomic = client.Atomic + instClient.PostRenderer = client.PostRenderer + instClient.DisableOpenAPIValidation = client.DisableOpenAPIValidation + instClient.SubNotes = client.SubNotes + instClient.Description = client.Description + + rel, err := runInstall(args, instClient, valueOpts, out) + if err != nil { + return err + } + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) + } else if err != nil { + return err + } } - if upgrade.version == "" && upgrade.devel { + if client.Version == "" && client.Devel { debug("setting version to >0.0.0-0") - upgrade.version = ">0.0.0-0" + client.Version = ">0.0.0-0" } - upgrade.release = args[0] - upgrade.chart = args[1] - upgrade.client = ensureHelmClient(upgrade.client) - - return upgrade.run() - }, - } - - f := cmd.Flags() - f.VarP(&upgrade.valueFiles, "values", "f", "specify values in a YAML file or a URL(can specify multiple)") - f.BoolVar(&upgrade.dryRun, "dry-run", false, "simulate an upgrade") - f.BoolVar(&upgrade.recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") - f.BoolVar(&upgrade.force, "force", false, "force resource update through delete/recreate if needed") - f.StringArrayVar(&upgrade.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - f.StringArrayVar(&upgrade.stringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks. DEPRECATED. Use no-hooks") - f.BoolVar(&upgrade.disableHooks, "no-hooks", false, "disable pre/post upgrade hooks") - 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", "", "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") - f.BoolVar(&upgrade.reuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored.") - f.BoolVar(&upgrade.wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") - f.StringVar(&upgrade.repoURL, "repo", "", "chart repository url where to locate the requested chart") - f.StringVar(&upgrade.username, "username", "", "chart repository username where to locate the requested chart") - f.StringVar(&upgrade.password, "password", "", "chart repository password where to locate the requested chart") - f.StringVar(&upgrade.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") - f.StringVar(&upgrade.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") - f.StringVar(&upgrade.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") - f.BoolVar(&upgrade.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") - - f.MarkDeprecated("disable-hooks", "use --no-hooks instead") - - return cmd -} + chartPath, err := client.ChartPathOptions.LocateChart(args[1], settings) + if err != nil { + return err + } -func (u *upgradeCmd) run() error { - chartPath, err := locateChartPath(u.repoURL, u.username, u.password, u.chart, u.version, u.verify, u.keyring, u.certFile, u.keyFile, u.caFile) - if err != nil { - return err - } + vals, err := valueOpts.MergeValues(getter.All(settings)) + if err != nil { + return err + } - if u.install { - // If a release does not exist, install it. If another error occurs during - // the check, ignore the error and continue with the upgrade. - // - // The returned error is a grpc.rpcError that wraps the message from the original error. - // So we're stuck doing string matching against the wrapped error, which is nested somewhere - // inside of the grpc.rpcError message. - releaseHistory, err := u.client.ReleaseHistory(u.release, helm.WithMaxHistory(1)) - - if err == nil { - if u.namespace == "" { - u.namespace = defaultNamespace() + // Check chart dependencies to make sure all are present in /charts + ch, err := loader.Load(chartPath) + if err != nil { + return err } - previousReleaseNamespace := releaseHistory.Releases[0].Namespace - if previousReleaseNamespace != u.namespace { - fmt.Fprintf(u.out, - "WARNING: Namespace %q doesn't match with previous. Release will be deployed to %s\n", - u.namespace, previousReleaseNamespace, - ) + if req := ch.Metadata.Dependencies; req != nil { + if err := action.CheckDependencies(ch, req); err != nil { + return err + } } - } - if err != nil && strings.Contains(err.Error(), driver.ErrReleaseNotFound(u.release).Error()) { - fmt.Fprintf(u.out, "Release %q does not exist. Installing it now.\n", u.release) - ic := &installCmd{ - chartPath: chartPath, - client: u.client, - out: u.out, - name: u.release, - valueFiles: u.valueFiles, - dryRun: u.dryRun, - verify: u.verify, - disableHooks: u.disableHooks, - keyring: u.keyring, - values: u.values, - stringValues: u.stringValues, - namespace: u.namespace, - timeout: u.timeout, - wait: u.wait, + if ch.Metadata.Deprecated { + warning("This chart is deprecated") } - return ic.run() - } - } - rawVals, err := vals(u.valueFiles, u.values, u.stringValues) - if err != nil { - return err - } - - // Check chart requirements to make sure all dependencies are present in /charts - if ch, err := chartutil.Load(chartPath); err == nil { - if req, err := chartutil.LoadRequirements(ch); err == nil { - if err := checkDependencies(ch, req); err != nil { - return err + rel, err := client.Run(args[0], ch, vals) + if err != nil { + return errors.Wrap(err, "UPGRADE FAILED") } - } else if err != chartutil.ErrRequirementsNotFound { - return fmt.Errorf("cannot load requirements: %v", err) - } - } else { - return prettyError(err) - } - resp, err := u.client.UpdateRelease( - u.release, - chartPath, - helm.UpdateValueOverrides(rawVals), - helm.UpgradeDryRun(u.dryRun), - helm.UpgradeRecreate(u.recreate), - helm.UpgradeForce(u.force), - helm.UpgradeDisableHooks(u.disableHooks), - helm.UpgradeTimeout(u.timeout), - helm.ResetValues(u.resetValues), - helm.ReuseValues(u.reuseValues), - helm.UpgradeWait(u.wait)) - if err != nil { - return fmt.Errorf("UPGRADE FAILED: %v", prettyError(err)) - } + if outfmt == output.Table { + fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0]) + } - if settings.Debug { - printRelease(u.out, resp.Release) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) + }, } - fmt.Fprintf(u.out, "Release %q has been upgraded. Happy Helming!\n", u.release) + f := cmd.Flags() + f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present") + f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install") + f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") + f.BoolVar(&client.DryRun, "dry-run", false, "simulate an upgrade") + f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") + f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods") + f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") + f.BoolVar(&client.DisableHooks, "no-hooks", false, "disable pre/post upgrade hooks") + f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the upgrade process will not validate rendered templates against the Kubernetes OpenAPI Schema") + f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed when an upgrade is performed with install flag enabled. By default, CRDs are installed if not already present, when an upgrade is performed with install flag enabled") + f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") + f.BoolVar(&client.ResetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart") + f.BoolVar(&client.ReuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored") + f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout") + f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout") + f.BoolVar(&client.Atomic, "atomic", false, "if set, upgrade process rolls back changes made in case of failed upgrade. The --wait flag will be set automatically if --atomic is used") + f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit") + f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this upgrade when upgrade fails") + f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent") + f.StringVar(&client.Description, "description", "", "add a custom description") + addChartPathOptionsFlags(f, &client.ChartPathOptions) + addValueOptionsFlags(f, valueOpts) + bindOutputFlag(cmd, &outfmt) + bindPostRenderFlag(cmd, &client.PostRenderer) + + err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 2 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compVersionFlag(args[1], toComplete) + }) - // Print the status like status command does - status, err := u.client.ReleaseStatus(u.release) if err != nil { - return prettyError(err) + log.Fatal(err) } - PrintStatus(u.out, status) - return nil + return cmd } diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index 187d3593e1a..e952a5933bb 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,154 +17,405 @@ limitations under the License. package main import ( - "io" + "fmt" "io/ioutil" "os" "path/filepath" + "strings" "testing" - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/proto/hapi/release" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/release" ) func TestUpgradeCmd(t *testing.T) { - tmpChart, _ := ioutil.TempDir("testdata", "tmp") - defer os.RemoveAll(tmpChart) - cfile := &chart.Metadata{ - Name: "testUpgradeChart", - Description: "A Helm chart for Kubernetes", - Version: "0.1.0", + tmpChart := ensure.TempDir(t) + cfile := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "testUpgradeChart", + Description: "A Helm chart for Kubernetes", + Version: "0.1.0", + }, + } + chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) + if err := chartutil.SaveDir(cfile, tmpChart); err != nil { + t.Fatalf("Error creating chart for upgrade: %v", err) } - chartPath, err := chartutil.Create(cfile, tmpChart) + ch, err := loader.Load(chartPath) if err != nil { - t.Errorf("Error creating chart for upgrade: %v", err) + t.Fatalf("Error loading chart: %v", err) } - ch, _ := chartutil.Load(chartPath) - _ = helm.ReleaseMock(&helm.MockReleaseOptions{ + _ = release.Mock(&release.MockReleaseOptions{ Name: "funny-bunny", Chart: ch, }) // update chart version - cfile = &chart.Metadata{ - Name: "testUpgradeChart", - Description: "A Helm chart for Kubernetes", - Version: "0.1.2", - } + cfile.Metadata.Version = "0.1.2" - chartPath, err = chartutil.Create(cfile, tmpChart) - if err != nil { - t.Errorf("Error creating chart: %v", err) + if err := chartutil.SaveDir(cfile, tmpChart); err != nil { + t.Fatalf("Error creating chart: %v", err) } - ch, err = chartutil.Load(chartPath) + ch, err = loader.Load(chartPath) if err != nil { - t.Errorf("Error loading updated chart: %v", err) + t.Fatalf("Error loading updated chart: %v", err) } // update chart version again - cfile = &chart.Metadata{ - Name: "testUpgradeChart", - Description: "A Helm chart for Kubernetes", - Version: "0.1.3", - } + cfile.Metadata.Version = "0.1.3" - chartPath, err = chartutil.Create(cfile, tmpChart) - if err != nil { - t.Errorf("Error creating chart: %v", err) + if err := chartutil.SaveDir(cfile, tmpChart); err != nil { + t.Fatalf("Error creating chart: %v", err) } var ch2 *chart.Chart - ch2, err = chartutil.Load(chartPath) + ch2, err = loader.Load(chartPath) if err != nil { - t.Errorf("Error loading updated chart: %v", err) + t.Fatalf("Error loading updated chart: %v", err) } - originalDepsPath := filepath.Join("testdata/testcharts/reqtest") - missingDepsPath := filepath.Join("testdata/testcharts/chart-missing-deps") - badDepsPath := filepath.Join("testdata/testcharts/chart-bad-requirements") - var ch3 *chart.Chart - ch3, err = chartutil.Load(originalDepsPath) - if err != nil { - t.Errorf("Error loading chart with missing dependencies: %v", err) + missingDepsPath := "testdata/testcharts/chart-missing-deps" + badDepsPath := "testdata/testcharts/chart-bad-requirements" + + relWithStatusMock := func(n string, v int, ch *chart.Chart, status release.Status) *release.Release { + return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch, Status: status}) } - tests := []releaseCase{ + relMock := func(n string, v int, ch *chart.Chart) *release.Release { + return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch}) + } + + tests := []cmdTestCase{ + { + name: "upgrade a release", + cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), + golden: "output/upgrade.txt", + rels: []*release.Release{relMock("funny-bunny", 2, ch)}, + }, + { + name: "upgrade a release with timeout", + cmd: fmt.Sprintf("upgrade funny-bunny --timeout 120s '%s'", chartPath), + golden: "output/upgrade-with-timeout.txt", + rels: []*release.Release{relMock("funny-bunny", 3, ch2)}, + }, + { + name: "upgrade a release with --reset-values", + cmd: fmt.Sprintf("upgrade funny-bunny --reset-values '%s'", chartPath), + golden: "output/upgrade-with-reset-values.txt", + rels: []*release.Release{relMock("funny-bunny", 4, ch2)}, + }, { - name: "upgrade a release", - args: []string{"funny-bunny", chartPath}, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 2, Chart: ch}), - expected: "Release \"funny-bunny\" has been upgraded. Happy Helming!\n", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 2, Chart: ch})}, + name: "upgrade a release with --reuse-values", + cmd: fmt.Sprintf("upgrade funny-bunny --reuse-values '%s'", chartPath), + golden: "output/upgrade-with-reset-values2.txt", + rels: []*release.Release{relMock("funny-bunny", 5, ch2)}, }, { - name: "upgrade a release with timeout", - args: []string{"funny-bunny", chartPath}, - flags: []string{"--timeout", "120"}, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 3, Chart: ch2}), - expected: "Release \"funny-bunny\" has been upgraded. Happy Helming!\n", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 3, Chart: ch2})}, + name: "install a release with 'upgrade --install'", + cmd: fmt.Sprintf("upgrade zany-bunny -i '%s'", chartPath), + golden: "output/upgrade-with-install.txt", + rels: []*release.Release{relMock("zany-bunny", 1, ch)}, }, { - name: "upgrade a release with --reset-values", - args: []string{"funny-bunny", chartPath}, - flags: []string{"--reset-values", "true"}, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 4, Chart: ch2}), - expected: "Release \"funny-bunny\" has been upgraded. Happy Helming!\n", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 4, Chart: ch2})}, + name: "install a release with 'upgrade --install' and timeout", + cmd: fmt.Sprintf("upgrade crazy-bunny -i --timeout 120s '%s'", chartPath), + golden: "output/upgrade-with-install-timeout.txt", + rels: []*release.Release{relMock("crazy-bunny", 1, ch)}, }, { - name: "upgrade a release with --reuse-values", - args: []string{"funny-bunny", chartPath}, - flags: []string{"--reuse-values", "true"}, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 5, Chart: ch2}), - expected: "Release \"funny-bunny\" has been upgraded. Happy Helming!\n", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "funny-bunny", Version: 5, Chart: ch2})}, + name: "upgrade a release with wait", + cmd: fmt.Sprintf("upgrade crazy-bunny --wait '%s'", chartPath), + golden: "output/upgrade-with-wait.txt", + rels: []*release.Release{relMock("crazy-bunny", 2, ch2)}, }, { - name: "install a release with 'upgrade --install'", - args: []string{"zany-bunny", chartPath}, - flags: []string{"-i"}, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "zany-bunny", Version: 1, Chart: ch}), - expected: "Release \"zany-bunny\" has been upgraded. Happy Helming!\n", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "zany-bunny", Version: 1, Chart: ch})}, + name: "upgrade a release with wait-for-jobs", + cmd: fmt.Sprintf("upgrade crazy-bunny --wait --wait-for-jobs '%s'", chartPath), + golden: "output/upgrade-with-wait-for-jobs.txt", + rels: []*release.Release{relMock("crazy-bunny", 2, ch2)}, }, { - name: "install a release with 'upgrade --install' and timeout", - args: []string{"crazy-bunny", chartPath}, - flags: []string{"-i", "--timeout", "120"}, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-bunny", Version: 1, Chart: ch}), - expected: "Release \"crazy-bunny\" has been upgraded. Happy Helming!\n", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-bunny", Version: 1, Chart: ch})}, + name: "upgrade a release with missing dependencies", + cmd: fmt.Sprintf("upgrade bonkers-bunny %s", missingDepsPath), + golden: "output/upgrade-with-missing-dependencies.txt", + wantError: true, }, { - name: "upgrade a release with wait", - args: []string{"crazy-bunny", chartPath}, - flags: []string{"--wait"}, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-bunny", Version: 2, Chart: ch2}), - expected: "Release \"crazy-bunny\" has been upgraded. Happy Helming!\n", - rels: []*release.Release{helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-bunny", Version: 2, Chart: ch2})}, + name: "upgrade a release with bad dependencies", + cmd: fmt.Sprintf("upgrade bonkers-bunny '%s'", badDepsPath), + golden: "output/upgrade-with-bad-dependencies.txt", + wantError: true, }, { - name: "upgrade a release with missing dependencies", - args: []string{"bonkers-bunny", missingDepsPath}, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "bonkers-bunny", Version: 1, Chart: ch3}), - err: true, + name: "upgrade a non-existent release", + cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), + golden: "output/upgrade-with-bad-or-missing-existing-release.txt", + wantError: true, }, { - name: "upgrade a release with bad dependencies", - args: []string{"bonkers-bunny", badDepsPath}, - resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "bonkers-bunny", Version: 1, Chart: ch3}), - err: true, + name: "upgrade a failed release", + cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), + golden: "output/upgrade.txt", + rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, release.StatusFailed)}, }, + { + name: "upgrade a pending install release", + cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), + golden: "output/upgrade-with-pending-install.txt", + wantError: true, + rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, release.StatusPendingInstall)}, + }, + } + runTestCmd(t, tests) +} + +func TestUpgradeWithValue(t *testing.T) { + releaseName := "funny-bunny-v2" + relMock, ch, chartPath := prepareMockRelease(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + store.Create(relMock(releaseName, 3, ch)) + + cmd := fmt.Sprintf("upgrade %s --set favoriteDrink=tea '%s'", releaseName, chartPath) + _, _, err := executeActionCommandC(store, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + updatedRel, err := store.Get(releaseName, 4) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + if !strings.Contains(updatedRel.Manifest, "drink: tea") { + t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) + } + +} + +func TestUpgradeWithStringValue(t *testing.T) { + releaseName := "funny-bunny-v3" + relMock, ch, chartPath := prepareMockRelease(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + store.Create(relMock(releaseName, 3, ch)) + + cmd := fmt.Sprintf("upgrade %s --set-string favoriteDrink=coffee '%s'", releaseName, chartPath) + _, _, err := executeActionCommandC(store, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) } - cmd := func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newUpgradeCmd(c, out) + updatedRel, err := store.Get(releaseName, 4) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) } - runReleaseCases(t, tests, cmd) + if !strings.Contains(updatedRel.Manifest, "drink: coffee") { + t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) + } + +} + +func TestUpgradeInstallWithSubchartNotes(t *testing.T) { + + releaseName := "wacky-bunny-v1" + relMock, ch, _ := prepareMockRelease(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + store.Create(relMock(releaseName, 1, ch)) + + cmd := fmt.Sprintf("upgrade %s -i --render-subchart-notes '%s'", releaseName, "testdata/testcharts/chart-with-subchart-notes") + _, _, err := executeActionCommandC(store, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + upgradedRel, err := store.Get(releaseName, 2) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + if !strings.Contains(upgradedRel.Info.Notes, "PARENT NOTES") { + t.Errorf("The parent notes are not set correctly. NOTES: %s", upgradedRel.Info.Notes) + } + + if !strings.Contains(upgradedRel.Info.Notes, "SUBCHART NOTES") { + t.Errorf("The subchart notes are not set correctly. NOTES: %s", upgradedRel.Info.Notes) + } + +} + +func TestUpgradeWithValuesFile(t *testing.T) { + + releaseName := "funny-bunny-v4" + relMock, ch, chartPath := prepareMockRelease(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + store.Create(relMock(releaseName, 3, ch)) + + cmd := fmt.Sprintf("upgrade %s --values testdata/testcharts/upgradetest/values.yaml '%s'", releaseName, chartPath) + _, _, err := executeActionCommandC(store, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + updatedRel, err := store.Get(releaseName, 4) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + if !strings.Contains(updatedRel.Manifest, "drink: beer") { + t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) + } + +} + +func TestUpgradeWithValuesFromStdin(t *testing.T) { + + releaseName := "funny-bunny-v5" + relMock, ch, chartPath := prepareMockRelease(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + store.Create(relMock(releaseName, 3, ch)) + + in, err := os.Open("testdata/testcharts/upgradetest/values.yaml") + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + cmd := fmt.Sprintf("upgrade %s --values - '%s'", releaseName, chartPath) + _, _, err = executeActionCommandStdinC(store, in, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + updatedRel, err := store.Get(releaseName, 4) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + if !strings.Contains(updatedRel.Manifest, "drink: beer") { + t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) + } +} + +func TestUpgradeInstallWithValuesFromStdin(t *testing.T) { + + releaseName := "funny-bunny-v6" + _, _, chartPath := prepareMockRelease(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + in, err := os.Open("testdata/testcharts/upgradetest/values.yaml") + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + cmd := fmt.Sprintf("upgrade %s -f - --install '%s'", releaseName, chartPath) + _, _, err = executeActionCommandStdinC(store, in, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + updatedRel, err := store.Get(releaseName, 1) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + if !strings.Contains(updatedRel.Manifest, "drink: beer") { + t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) + } + +} + +func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) { + tmpChart := ensure.TempDir(t) + configmapData, err := ioutil.ReadFile("testdata/testcharts/upgradetest/templates/configmap.yaml") + if err != nil { + t.Fatalf("Error loading template yaml %v", err) + } + cfile := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "testUpgradeChart", + Description: "A Helm chart for Kubernetes", + Version: "0.1.0", + }, + Templates: []*chart.File{{Name: "templates/configmap.yaml", Data: configmapData}}, + } + chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) + if err := chartutil.SaveDir(cfile, tmpChart); err != nil { + t.Fatalf("Error creating chart for upgrade: %v", err) + } + ch, err := loader.Load(chartPath) + if err != nil { + t.Fatalf("Error loading chart: %v", err) + } + _ = release.Mock(&release.MockReleaseOptions{ + Name: releaseName, + Chart: ch, + }) + + relMock := func(n string, v int, ch *chart.Chart) *release.Release { + return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch}) + } + + return relMock, ch, chartPath +} + +func TestUpgradeOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "upgrade") +} + +func TestUpgradeVersionCompletion(t *testing.T) { + repoFile := "testdata/helmhome/helm/repositories.yaml" + repoCache := "testdata/helmhome/helm/repository" + + repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache) + + tests := []cmdTestCase{{ + name: "completion for upgrade version flag", + cmd: fmt.Sprintf("%s __complete upgrade releasename testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for upgrade version flag too few args", + cmd: fmt.Sprintf("%s __complete upgrade releasename --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for upgrade version flag too many args", + cmd: fmt.Sprintf("%s __complete upgrade releasename testing/alpine badarg --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for upgrade version flag invalid chart", + cmd: fmt.Sprintf("%s __complete upgrade releasename invalid/invalid --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }} + runTestCmd(t, tests) +} +func TestUpgradeFileCompletion(t *testing.T) { + checkFileCompletion(t, "upgrade", false) + checkFileCompletion(t, "upgrade myrelease", true) + checkFileCompletion(t, "upgrade myrelease repo/chart", false) } diff --git a/cmd/helm/verify.go b/cmd/helm/verify.go index e82eb4e333f..d126c9ef361 100644 --- a/cmd/helm/verify.go +++ b/cmd/helm/verify.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,18 +16,19 @@ limitations under the License. package main import ( - "errors" + "fmt" "io" "github.com/spf13/cobra" - "k8s.io/helm/pkg/downloader" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" ) const verifyDesc = ` Verify that the given chart has a valid provenance file. -Provenance files provide crytographic verification that a chart has not been +Provenance files provide cryptographic verification that a chart has not been tampered with, and was packaged by a trusted provider. This command can be used to verify a local chart. Several other commands provide @@ -35,36 +36,35 @@ This command can be used to verify a local chart. Several other commands provide the 'helm package --sign' command. ` -type verifyCmd struct { - keyring string - chartfile string - - out io.Writer -} - func newVerifyCmd(out io.Writer) *cobra.Command { - vc := &verifyCmd{out: out} + client := action.NewVerify() cmd := &cobra.Command{ - Use: "verify [flags] PATH", + Use: "verify PATH", Short: "verify that a chart at the given path has been signed and is valid", Long: verifyDesc, - RunE: func(cmd *cobra.Command, args []string) error { + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { - return errors.New("a path to a package file is required") + // Allow file completion when completing the argument for the path + return nil, cobra.ShellCompDirectiveDefault } - vc.chartfile = args[0] - return vc.run() + // No more completions, so disable file completion + return nil, cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(cmd *cobra.Command, args []string) error { + err := client.Run(args[0]) + if err != nil { + return err + } + + fmt.Fprint(out, client.Out) + + return nil }, } - f := cmd.Flags() - f.StringVar(&vc.keyring, "keyring", defaultKeyring(), "keyring containing public keys") + cmd.Flags().StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") return cmd } - -func (v *verifyCmd) run() error { - _, err := downloader.VerifyChart(v.chartfile, v.keyring) - return err -} diff --git a/cmd/helm/verify_test.go b/cmd/helm/verify_test.go index 6e8b906fc9e..23b7935577c 100644 --- a/cmd/helm/verify_test.go +++ b/cmd/helm/verify_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,7 +16,6 @@ limitations under the License. package main import ( - "bytes" "fmt" "runtime" "testing" @@ -28,68 +27,71 @@ func TestVerifyCmd(t *testing.T) { statPathMsg := "no such file or directory" statFileMsg := statPathMsg if runtime.GOOS == "windows" { - statExe = "GetFileAttributesEx" + statExe = "FindFirstFile" statPathMsg = "The system cannot find the path specified." statFileMsg = "The system cannot find the file specified." } tests := []struct { - name string - args []string - flags []string - expect string - err bool + name string + cmd string + expect string + wantError bool }{ { - name: "verify requires a chart", - expect: "a path to a package file is required", - err: true, + name: "verify requires a chart", + cmd: "verify", + expect: "\"helm verify\" requires 1 argument\n\nUsage: helm verify PATH [flags]", + wantError: true, }, { - name: "verify requires that chart exists", - args: []string{"no/such/file"}, - expect: fmt.Sprintf("%s no/such/file: %s", statExe, statPathMsg), - err: true, + name: "verify requires that chart exists", + cmd: "verify no/such/file", + expect: fmt.Sprintf("%s no/such/file: %s", statExe, statPathMsg), + wantError: true, }, { - name: "verify requires that chart is not a directory", - args: []string{"testdata/testcharts/signtest"}, - expect: "unpacked charts cannot be verified", - err: true, + name: "verify requires that chart is not a directory", + cmd: "verify testdata/testcharts/signtest", + expect: "unpacked charts cannot be verified", + wantError: true, }, { - name: "verify requires that chart has prov file", - args: []string{"testdata/testcharts/compressedchart-0.1.0.tgz"}, - expect: fmt.Sprintf("could not load provenance file testdata/testcharts/compressedchart-0.1.0.tgz.prov: %s testdata/testcharts/compressedchart-0.1.0.tgz.prov: %s", statExe, statFileMsg), - err: true, + name: "verify requires that chart has prov file", + cmd: "verify testdata/testcharts/compressedchart-0.1.0.tgz", + expect: fmt.Sprintf("could not load provenance file testdata/testcharts/compressedchart-0.1.0.tgz.prov: %s testdata/testcharts/compressedchart-0.1.0.tgz.prov: %s", statExe, statFileMsg), + wantError: true, }, { - name: "verify validates a properly signed chart", - args: []string{"testdata/testcharts/signtest-0.1.0.tgz"}, - flags: []string{"--keyring", "testdata/helm-test-key.pub"}, - expect: "", - err: false, + name: "verify validates a properly signed chart", + cmd: "verify testdata/testcharts/signtest-0.1.0.tgz --keyring testdata/helm-test-key.pub", + expect: "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \nUsing Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\nChart Hash Verified: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\n", + wantError: false, }, } for _, tt := range tests { - b := bytes.NewBuffer(nil) - vc := newVerifyCmd(b) - vc.ParseFlags(tt.flags) - err := vc.RunE(vc, tt.args) - if tt.err { - if err == nil { - t.Errorf("Expected error, but got none: %q", b.String()) + t.Run(tt.name, func(t *testing.T) { + _, out, err := executeActionCommand(tt.cmd) + if tt.wantError { + if err == nil { + t.Errorf("Expected error, but got none: %q", out) + } + if err.Error() != tt.expect { + t.Errorf("Expected error %q, got %q", tt.expect, err) + } + return + } else if err != nil { + t.Errorf("Unexpected error: %s", err) } - if err.Error() != tt.expect { - t.Errorf("Expected error %q, got %q", tt.expect, err) + if out != tt.expect { + t.Errorf("Expected %q, got %q", tt.expect, out) } - continue - } else if err != nil { - t.Errorf("Unexpected error: %s", err) - } - if b.String() != tt.expect { - t.Errorf("Expected %q, got %q", tt.expect, b.String()) - } + }) } } + +func TestVerifyFileCompletion(t *testing.T) { + checkFileCompletion(t, "verify", true) + checkFileCompletion(t, "verify mypath", false) +} diff --git a/cmd/helm/version.go b/cmd/helm/version.go index d541067a010..72f93e5458b 100644 --- a/cmd/helm/version.go +++ b/cmd/helm/version.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,135 +17,85 @@ limitations under the License. package main import ( - "errors" "fmt" "io" + "text/template" "github.com/spf13/cobra" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - apiVersion "k8s.io/apimachinery/pkg/version" - "k8s.io/helm/pkg/helm" - pb "k8s.io/helm/pkg/proto/hapi/version" - "k8s.io/helm/pkg/version" + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/internal/version" ) const versionDesc = ` -Show the client and server versions for Helm and tiller. +Show the version for Helm. -This will print a representation of the client and server versions of Helm and -Tiller. The output will look something like this: +This will print a representation the version of Helm. +The output will look something like this: -Client: &version.Version{SemVer:"v2.0.0", GitCommit:"ff52399e51bb880526e9cd0ed8386f6433b74da1", GitTreeState:"clean"} -Server: &version.Version{SemVer:"v2.0.0", GitCommit:"b0c113dfb9f612a9add796549da66c0d294508a3", GitTreeState:"clean"} +version.BuildInfo{Version:"v3.2.1", GitCommit:"fe51cd1e31e6a202cba7dead9552a6d418ded79a", GitTreeState:"clean", GoVersion:"go1.13.10"} -- SemVer is the semantic version of the release. +- Version is the semantic version of the release. - GitCommit is the SHA for the commit that this version was built from. - GitTreeState is "clean" if there are no local code changes when this binary was built, and "dirty" if the binary was built from locally modified code. +- GoVersion is the version of Go that was used to compile Helm. -To print just the client version, use '--client'. To print just the server version, -use '--server'. +When using the --template flag the following properties are available to use in +the template: + +- .Version contains the semantic version of Helm +- .GitCommit is the git commit +- .GitTreeState is the state of the git tree when Helm was built +- .GoVersion contains the version of Go that Helm was compiled with ` -type versionCmd struct { - out io.Writer - client helm.Interface - showClient bool - showServer bool - short bool - template string +type versionOptions struct { + short bool + template string } -func newVersionCmd(c helm.Interface, out io.Writer) *cobra.Command { - version := &versionCmd{ - client: c, - out: out, - } +func newVersionCmd(out io.Writer) *cobra.Command { + o := &versionOptions{} cmd := &cobra.Command{ - Use: "version", - Short: "print the client/server version information", - Long: versionDesc, + Use: "version", + Short: "print the client version information", + Long: versionDesc, + Args: require.NoArgs, + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { - // If neither is explicitly set, show both. - if !version.showClient && !version.showServer { - version.showClient, version.showServer = true, true - } - if version.showServer { - // We do this manually instead of in PreRun because we only - // need a tunnel if server version is requested. - setupConnection() - } - version.client = ensureHelmClient(version.client) - return version.run() + return o.run(out) }, } f := cmd.Flags() - f.BoolVarP(&version.showClient, "client", "c", false, "client version only") - f.BoolVarP(&version.showServer, "server", "s", false, "server version only") - f.BoolVar(&version.short, "short", false, "print the version number") - f.StringVar(&version.template, "template", "", "template for version string format") + f.BoolVar(&o.short, "short", false, "print the version number") + f.StringVar(&o.template, "template", "", "template for version string format") + f.BoolP("client", "c", true, "display client version information") + f.MarkHidden("client") return cmd } -func (v *versionCmd) run() error { - // Store map data for template rendering - data := map[string]interface{}{} - - if v.showClient { - cv := version.GetVersionProto() - if v.template != "" { - data["Client"] = cv - } else { - fmt.Fprintf(v.out, "Client: %s\n", formatVersion(cv, v.short)) - } - } - - if !v.showServer { - return tpl(v.template, data, v.out) - } - - if settings.Debug { - k8sVersion, err := getK8sVersion() +func (o *versionOptions) run(out io.Writer) error { + if o.template != "" { + tt, err := template.New("_").Parse(o.template) if err != nil { return err } - fmt.Fprintf(v.out, "Kubernetes: %#v\n", k8sVersion) + return tt.Execute(out, version.Get()) } - - resp, err := v.client.GetVersion() - if err != nil { - if grpc.Code(err) == codes.Unimplemented { - return errors.New("server is too old to know its version") - } - debug("%s", err) - return errors.New("cannot connect to Tiller") - } - - if v.template != "" { - data["Server"] = resp.Version - } else { - fmt.Fprintf(v.out, "Server: %s\n", formatVersion(resp.Version, v.short)) - } - return tpl(v.template, data, v.out) + fmt.Fprintln(out, formatVersion(o.short)) + return nil } -func getK8sVersion() (*apiVersion.Info, error) { - var v *apiVersion.Info - _, client, err := getKubeClient(settings.KubeContext) - if err != nil { - return v, err - } - v, err = client.Discovery().ServerVersion() - return v, err -} - -func formatVersion(v *pb.Version, short bool) string { +func formatVersion(short bool) string { + v := version.Get() if short { - return fmt.Sprintf("%s+g%s", v.SemVer, v.GitCommit[:7]) + if len(v.GitCommit) >= 7 { + return fmt.Sprintf("%s+g%s", v.Version, v.GitCommit[:7]) + } + return version.GetVersion() } return fmt.Sprintf("%#v", v) } diff --git a/cmd/helm/version_test.go b/cmd/helm/version_test.go index e25724f4cad..aa3cbfb7dd3 100644 --- a/cmd/helm/version_test.go +++ b/cmd/helm/version_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,50 +16,34 @@ limitations under the License. package main import ( - "fmt" - "io" - "regexp" "testing" - - "github.com/spf13/cobra" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/version" ) func TestVersion(t *testing.T) { - lver := regexp.QuoteMeta(version.GetVersionProto().SemVer) - sver := regexp.QuoteMeta("1.2.3-fakeclient+testonly") - clientVersion := fmt.Sprintf("Client: &version\\.Version{SemVer:\"%s\", GitCommit:\"\", GitTreeState:\"\"}\n", lver) - serverVersion := fmt.Sprintf("Server: &version\\.Version{SemVer:\"%s\", GitCommit:\"\", GitTreeState:\"\"}\n", sver) + tests := []cmdTestCase{{ + name: "default", + cmd: "version", + golden: "output/version.txt", + }, { + name: "short", + cmd: "version --short", + golden: "output/version-short.txt", + }, { + name: "template", + cmd: "version --template='Version: {{.Version}}'", + golden: "output/version-template.txt", + }, { + name: "client", + cmd: "version --client", + golden: "output/version-client.txt", + }, { + name: "client shorthand", + cmd: "version -c", + golden: "output/version-client-shorthand.txt", + }} + runTestCmd(t, tests) +} - tests := []releaseCase{ - { - name: "default", - args: []string{}, - expected: clientVersion + serverVersion, - }, - { - name: "client", - args: []string{}, - flags: []string{"-c"}, - expected: clientVersion, - }, - { - name: "server", - args: []string{}, - flags: []string{"-s"}, - expected: serverVersion, - }, - { - name: "template", - args: []string{}, - flags: []string{"--template", "{{ .Client.SemVer }} {{ .Server.SemVer }}"}, - expected: lver + " " + sver, - }, - } - settings.TillerHost = "fake-localhost" - runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { - return newVersionCmd(c, out) - }) +func TestVersionFileCompletion(t *testing.T) { + checkFileCompletion(t, "version", false) } diff --git a/cmd/rudder/rudder.go b/cmd/rudder/rudder.go deleted file mode 100644 index 30ece399813..00000000000 --- a/cmd/rudder/rudder.go +++ /dev/null @@ -1,158 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "bytes" - "fmt" - "net" - - "golang.org/x/net/context" - "google.golang.org/grpc" - "google.golang.org/grpc/grpclog" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" - - "github.com/spf13/pflag" - "k8s.io/helm/pkg/kube" - rudderAPI "k8s.io/helm/pkg/proto/hapi/rudder" - "k8s.io/helm/pkg/tiller" - "k8s.io/helm/pkg/version" -) - -var kubeClient *kube.Client -var clientset internalclientset.Interface - -type options struct { - listen string -} - -func (opts *options) registerFlags() { - pflag.StringVarP(&opts.listen, "listen", "l", "127.0.0.1:10001", - "Socket for rudder grpc server (default: 127.0.0.1:10001).") -} - -func (opts *options) parseFlags() { - pflag.Parse() -} - -func (opts *options) regAndParseFlags() { - opts.registerFlags() - opts.parseFlags() -} - -func main() { - opts := new(options) - opts.regAndParseFlags() - var err error - kubeClient = kube.New(nil) - clientset, err = kubeClient.ClientSet() - if err != nil { - grpclog.Fatalf("Cannot initialize Kubernetes connection: %s", err) - } - grpclog.Printf("Creating tcp socket on %s\n", opts.listen) - lis, err := net.Listen("tcp", opts.listen) - if err != nil { - grpclog.Fatalf("failed to listen: %v", err) - } - grpcServer := grpc.NewServer() - rudderAPI.RegisterReleaseModuleServiceServer(grpcServer, &ReleaseModuleServiceServer{}) - - grpclog.Printf("Starting server on %s\n", opts.listen) - grpcServer.Serve(lis) -} - -// ReleaseModuleServiceServer provides implementation for rudderAPI.ReleaseModuleServiceServer -type ReleaseModuleServiceServer struct{} - -// Version returns Rudder version based on helm version -func (r *ReleaseModuleServiceServer) Version(ctx context.Context, in *rudderAPI.VersionReleaseRequest) (*rudderAPI.VersionReleaseResponse, error) { - grpclog.Print("version") - return &rudderAPI.VersionReleaseResponse{ - Name: "helm-rudder-native", - Version: version.Version, - }, nil -} - -// InstallRelease creates a release using kubeClient.Create -func (r *ReleaseModuleServiceServer) InstallRelease(ctx context.Context, in *rudderAPI.InstallReleaseRequest) (*rudderAPI.InstallReleaseResponse, error) { - grpclog.Print("install") - b := bytes.NewBufferString(in.Release.Manifest) - err := kubeClient.Create(in.Release.Namespace, b, 500, false) - if err != nil { - grpclog.Printf("error when creating release: %v", err) - } - return &rudderAPI.InstallReleaseResponse{}, err -} - -// DeleteRelease deletes a provided release -func (r *ReleaseModuleServiceServer) DeleteRelease(ctx context.Context, in *rudderAPI.DeleteReleaseRequest) (*rudderAPI.DeleteReleaseResponse, error) { - grpclog.Print("delete") - - resp := &rudderAPI.DeleteReleaseResponse{} - rel := in.Release - vs, err := tiller.GetVersionSet(clientset.Discovery()) - if err != nil { - return resp, fmt.Errorf("Could not get apiVersions from Kubernetes: %v", err) - } - - kept, errs := tiller.DeleteRelease(rel, vs, kubeClient) - rel.Manifest = kept - - allErrors := "" - for _, e := range errs { - allErrors = allErrors + "\n" + e.Error() - } - - if len(allErrors) > 0 { - err = fmt.Errorf(allErrors) - } - - return &rudderAPI.DeleteReleaseResponse{ - Release: rel, - }, err -} - -// RollbackRelease rolls back the release -func (r *ReleaseModuleServiceServer) RollbackRelease(ctx context.Context, in *rudderAPI.RollbackReleaseRequest) (*rudderAPI.RollbackReleaseResponse, error) { - grpclog.Print("rollback") - c := bytes.NewBufferString(in.Current.Manifest) - t := bytes.NewBufferString(in.Target.Manifest) - err := kubeClient.Update(in.Target.Namespace, c, t, in.Force, in.Recreate, in.Timeout, in.Wait) - return &rudderAPI.RollbackReleaseResponse{}, err -} - -// UpgradeRelease upgrades manifests using kubernetes client -func (r *ReleaseModuleServiceServer) UpgradeRelease(ctx context.Context, in *rudderAPI.UpgradeReleaseRequest) (*rudderAPI.UpgradeReleaseResponse, error) { - grpclog.Print("upgrade") - c := bytes.NewBufferString(in.Current.Manifest) - t := bytes.NewBufferString(in.Target.Manifest) - err := kubeClient.Update(in.Target.Namespace, c, t, in.Force, in.Recreate, in.Timeout, in.Wait) - // upgrade response object should be changed to include status - return &rudderAPI.UpgradeReleaseResponse{}, err -} - -// ReleaseStatus retrieves release status -func (r *ReleaseModuleServiceServer) ReleaseStatus(ctx context.Context, in *rudderAPI.ReleaseStatusRequest) (*rudderAPI.ReleaseStatusResponse, error) { - grpclog.Print("status") - - resp, err := kubeClient.Get(in.Release.Namespace, bytes.NewBufferString(in.Release.Manifest)) - in.Release.Info.Status.Resources = resp - return &rudderAPI.ReleaseStatusResponse{ - Release: in.Release, - Info: in.Release.Info, - }, err -} diff --git a/cmd/tiller/probes.go b/cmd/tiller/probes.go deleted file mode 100644 index 144ad8a1b3f..00000000000 --- a/cmd/tiller/probes.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -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 ( - "net/http" - - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -func readinessProbe(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) -} - -func livenessProbe(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) -} - -func newProbesMux() *http.ServeMux { - mux := http.NewServeMux() - mux.HandleFunc("/readiness", readinessProbe) - mux.HandleFunc("/liveness", livenessProbe) - return mux -} - -func addPrometheusHandler(mux *http.ServeMux) { - // Register HTTP handler for the global Prometheus registry. - mux.Handle("/metrics", promhttp.Handler()) -} diff --git a/cmd/tiller/probes_test.go b/cmd/tiller/probes_test.go deleted file mode 100644 index 0b13460e0a6..00000000000 --- a/cmd/tiller/probes_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -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 ( - "net/http" - "net/http/httptest" - "testing" -) - -func TestProbesServer(t *testing.T) { - mux := newProbesMux() - srv := httptest.NewServer(mux) - defer srv.Close() - resp, err := http.Get(srv.URL + "/readiness") - if err != nil { - t.Fatalf("GET /readiness returned an error (%s)", err) - } - if resp.StatusCode != http.StatusOK { - t.Fatalf("GET /readiness returned status code %d, expected %d", resp.StatusCode, http.StatusOK) - } - - resp, err = http.Get(srv.URL + "/liveness") - if err != nil { - t.Fatalf("GET /liveness returned an error (%s)", err) - } - if resp.StatusCode != http.StatusOK { - t.Fatalf("GET /liveness returned status code %d, expected %d", resp.StatusCode, http.StatusOK) - } -} - -func TestPrometheus(t *testing.T) { - mux := http.NewServeMux() - addPrometheusHandler(mux) - srv := httptest.NewServer(mux) - defer srv.Close() - resp, err := http.Get(srv.URL + "/metrics") - if err != nil { - t.Fatalf("GET /metrics returned an error (%s)", err) - } - if resp.StatusCode != http.StatusOK { - t.Fatalf("GET /metrics returned status code %d, expected %d", resp.StatusCode, http.StatusOK) - } -} diff --git a/cmd/tiller/tiller.go b/cmd/tiller/tiller.go deleted file mode 100644 index 5d2db381692..00000000000 --- a/cmd/tiller/tiller.go +++ /dev/null @@ -1,288 +0,0 @@ -/* -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 "k8s.io/helm/cmd/tiller" - -import ( - "crypto/tls" - "flag" - "fmt" - "io/ioutil" - "log" - "net" - "net/http" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - goprom "github.com/grpc-ecosystem/go-grpc-prometheus" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/health" - healthpb "google.golang.org/grpc/health/grpc_health_v1" - "google.golang.org/grpc/keepalive" - - "k8s.io/helm/pkg/kube" - "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/storage" - "k8s.io/helm/pkg/storage/driver" - "k8s.io/helm/pkg/tiller" - "k8s.io/helm/pkg/tiller/environment" - "k8s.io/helm/pkg/tlsutil" - "k8s.io/helm/pkg/version" -) - -const ( - // tlsEnableEnvVar names the environment variable that enables TLS. - tlsEnableEnvVar = "TILLER_TLS_ENABLE" - // tlsVerifyEnvVar names the environment variable that enables - // TLS, as well as certificate verification of the remote. - tlsVerifyEnvVar = "TILLER_TLS_VERIFY" - // tlsCertsEnvVar names the environment variable that points to - // the directory where Tiller's TLS certificates are located. - tlsCertsEnvVar = "TILLER_TLS_CERTS" - // historyMaxEnvVar is the name of the env var for setting max history. - historyMaxEnvVar = "TILLER_HISTORY_MAX" - - storageMemory = "memory" - storageConfigMap = "configmap" - storageSecret = "secret" - - probeAddr = ":44135" - traceAddr = ":44136" - - // defaultMaxHistory sets the maximum number of releases to 0: unlimited - defaultMaxHistory = 0 -) - -var ( - grpcAddr = flag.String("listen", ":44134", "address:port to listen on") - enableTracing = flag.Bool("trace", false, "enable rpc tracing") - store = flag.String("storage", storageConfigMap, "storage driver to use. One of 'configmap', 'memory', or 'secret'") - remoteReleaseModules = flag.Bool("experimental-release", false, "enable experimental release modules") - tlsEnable = flag.Bool("tls", tlsEnableEnvVarDefault(), "enable TLS") - tlsVerify = flag.Bool("tls-verify", tlsVerifyEnvVarDefault(), "enable TLS and verify remote certificate") - keyFile = flag.String("tls-key", tlsDefaultsFromEnv("tls-key"), "path to TLS private key file") - certFile = flag.String("tls-cert", tlsDefaultsFromEnv("tls-cert"), "path to TLS certificate file") - caCertFile = flag.String("tls-ca-cert", tlsDefaultsFromEnv("tls-ca-cert"), "trust certificates signed by this CA") - maxHistory = flag.Int("history-max", historyMaxFromEnv(), "maximum number of releases kept in release history, with 0 meaning no limit") - printVersion = flag.Bool("version", false, "print the version number") - - // rootServer is the root gRPC server. - // - // Each gRPC service registers itself to this server during init(). - rootServer *grpc.Server - - // env is the default environment. - // - // Any changes to env should be done before rootServer.Serve() is called. - env = environment.New() - - logger *log.Logger -) - -func main() { - // TODO: use spf13/cobra for tiller instead of flags - flag.Parse() - - if *printVersion { - fmt.Println(version.GetVersion()) - os.Exit(0) - } - - if *enableTracing { - log.SetFlags(log.Lshortfile) - } - logger = newLogger("main") - - start() -} - -func start() { - - healthSrv := health.NewServer() - healthSrv.SetServingStatus("Tiller", healthpb.HealthCheckResponse_NOT_SERVING) - - clientset, err := kube.New(nil).ClientSet() - if err != nil { - logger.Fatalf("Cannot initialize Kubernetes connection: %s", err) - } - - switch *store { - case storageMemory: - env.Releases = storage.Init(driver.NewMemory()) - case storageConfigMap: - cfgmaps := driver.NewConfigMaps(clientset.Core().ConfigMaps(namespace())) - cfgmaps.Log = newLogger("storage/driver").Printf - - env.Releases = storage.Init(cfgmaps) - env.Releases.Log = newLogger("storage").Printf - case storageSecret: - secrets := driver.NewSecrets(clientset.Core().Secrets(namespace())) - secrets.Log = newLogger("storage/driver").Printf - - env.Releases = storage.Init(secrets) - env.Releases.Log = newLogger("storage").Printf - } - - if *maxHistory > 0 { - env.Releases.MaxHistory = *maxHistory - } - - kubeClient := kube.New(nil) - kubeClient.Log = newLogger("kube").Printf - env.KubeClient = kubeClient - - if *tlsEnable || *tlsVerify { - opts := tlsutil.Options{CertFile: *certFile, KeyFile: *keyFile} - if *tlsVerify { - opts.CaCertFile = *caCertFile - } - } - - var opts []grpc.ServerOption - if *tlsEnable || *tlsVerify { - cfg, err := tlsutil.ServerConfig(tlsOptions()) - if err != nil { - logger.Fatalf("Could not create server TLS configuration: %v", err) - } - opts = append(opts, grpc.Creds(credentials.NewTLS(cfg))) - } - - opts = append(opts, grpc.KeepaliveParams(keepalive.ServerParameters{ - MaxConnectionIdle: 10 * time.Minute, - // If needed, we can configure the max connection age - })) - opts = append(opts, grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ - MinTime: time.Duration(20) * time.Second, // For compatibility with the client keepalive.ClientParameters - })) - - rootServer = tiller.NewServer(opts...) - healthpb.RegisterHealthServer(rootServer, healthSrv) - - lstn, err := net.Listen("tcp", *grpcAddr) - if err != nil { - logger.Fatalf("Server died: %s", err) - } - - logger.Printf("Starting Tiller %s (tls=%t)", version.GetVersion(), *tlsEnable || *tlsVerify) - logger.Printf("GRPC listening on %s", *grpcAddr) - logger.Printf("Probes listening on %s", probeAddr) - logger.Printf("Storage driver is %s", env.Releases.Name()) - logger.Printf("Max history per release is %d", *maxHistory) - - if *enableTracing { - startTracing(traceAddr) - } - - srvErrCh := make(chan error) - probeErrCh := make(chan error) - go func() { - svc := tiller.NewReleaseServer(env, clientset, *remoteReleaseModules) - svc.Log = newLogger("tiller").Printf - services.RegisterReleaseServiceServer(rootServer, svc) - if err := rootServer.Serve(lstn); err != nil { - srvErrCh <- err - } - }() - - go func() { - mux := newProbesMux() - - // Register gRPC server to prometheus to initialized matrix - goprom.Register(rootServer) - addPrometheusHandler(mux) - - if err := http.ListenAndServe(probeAddr, mux); err != nil { - probeErrCh <- err - } - }() - - healthSrv.SetServingStatus("Tiller", healthpb.HealthCheckResponse_SERVING) - - select { - case err := <-srvErrCh: - logger.Fatalf("Server died: %s", err) - case err := <-probeErrCh: - logger.Printf("Probes server died: %s", err) - } -} - -func newLogger(prefix string) *log.Logger { - if len(prefix) > 0 { - prefix = fmt.Sprintf("[%s] ", prefix) - } - return log.New(os.Stderr, prefix, log.Flags()) -} - -// namespace returns the namespace of tiller -func namespace() string { - if ns := os.Getenv("TILLER_NAMESPACE"); ns != "" { - return ns - } - - // Fall back to the namespace associated with the service account token, if available - if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { - if ns := strings.TrimSpace(string(data)); len(ns) > 0 { - return ns - } - } - - return environment.DefaultTillerNamespace -} - -func tlsOptions() tlsutil.Options { - opts := tlsutil.Options{CertFile: *certFile, KeyFile: *keyFile} - if *tlsVerify { - opts.CaCertFile = *caCertFile - - // We want to force the client to not only provide a cert, but to - // provide a cert that we can validate. - // http://www.bite-code.com/2015/06/25/tls-mutual-auth-in-golang/ - opts.ClientAuth = tls.RequireAndVerifyClientCert - } - return opts -} - -func tlsDefaultsFromEnv(name string) (value string) { - switch certsDir := os.Getenv(tlsCertsEnvVar); name { - case "tls-key": - return filepath.Join(certsDir, "tls.key") - case "tls-cert": - return filepath.Join(certsDir, "tls.crt") - case "tls-ca-cert": - return filepath.Join(certsDir, "ca.crt") - } - return "" -} - -func historyMaxFromEnv() int { - val := os.Getenv(historyMaxEnvVar) - if val == "" { - return defaultMaxHistory - } - ret, err := strconv.Atoi(val) - if err != nil { - log.Printf("Invalid max history %q. Defaulting to 0.", val) - return defaultMaxHistory - } - return ret -} - -func tlsEnableEnvVarDefault() bool { return os.Getenv(tlsEnableEnvVar) != "" } -func tlsVerifyEnvVarDefault() bool { return os.Getenv(tlsVerifyEnvVar) != "" } diff --git a/cmd/tiller/tiller_test.go b/cmd/tiller/tiller_test.go deleted file mode 100644 index 0698e9d94c1..00000000000 --- a/cmd/tiller/tiller_test.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -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" - - "k8s.io/helm/pkg/engine" - "k8s.io/helm/pkg/tiller/environment" -) - -// These are canary tests to make sure that the default server actually -// fulfills its requirements. -var _ environment.Engine = &engine.Engine{} - -func TestInit(t *testing.T) { - defer func() { - if recover() != nil { - t.Fatalf("Panic trapped. Check EngineYard.Default()") - } - }() - - // This will panic if it is not correct. - env.EngineYard.Default() - - e, ok := env.EngineYard.Get(environment.GoTplEngine) - if !ok { - t.Fatalf("Could not find GoTplEngine") - } - if e == nil { - t.Fatalf("Template engine GoTplEngine returned nil.") - } -} diff --git a/cmd/tiller/trace.go b/cmd/tiller/trace.go deleted file mode 100644 index 71d7e8f7268..00000000000 --- a/cmd/tiller/trace.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -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 "k8s.io/helm/cmd/tiller" - -import ( - "net/http" - - _ "net/http/pprof" - - "google.golang.org/grpc" -) - -func startTracing(addr string) { - logger.Printf("Tracing server is listening on %s\n", addr) - grpc.EnableTracing = true - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(traceIndexHTML)) - }) - - go func() { - if err := http.ListenAndServe(addr, nil); err != nil { - logger.Printf("tracing error: %s", err) - } - }() -} - -const traceIndexHTML = ` - - - - - -` diff --git a/code-of-conduct.md b/code-of-conduct.md index 0d15c00cf32..91ccaf03533 100644 --- a/code-of-conduct.md +++ b/code-of-conduct.md @@ -1,3 +1,3 @@ -# Kubernetes Community Code of Conduct +# Community Code of Conduct -Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) +Helm follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 752a7e12c55..00000000000 --- a/docs/architecture.md +++ /dev/null @@ -1,64 +0,0 @@ -# The Kubernetes Helm Architecture - -This document describes the Helm architecture at a high level. - -## The Purpose of Helm - -Helm is a tool for managing Kubernetes packages called _charts_. Helm -can do the following: - -- Create new charts from scratch -- Package charts into chart archive (tgz) files -- Interact with chart repositories where charts are stored -- Install and uninstall charts into an existing Kubernetes cluster -- Manage the release cycle of charts that have been installed with Helm - -For Helm, there are three important concepts: - -1. The _chart_ is a bundle of information necessary to create an - instance of a Kubernetes application. -2. The _config_ contains configuration information that can be merged - into a packaged chart to create a releasable object. -3. A _release_ is a running instance of a _chart_, combined with a - specific _config_. - -## Components - -Helm has two major components: - -**The Helm Client** is a command-line client for end users. The client -is responsible for the following domains: - -- Local chart development -- Managing repositories -- Interacting with the Tiller server - - Sending charts to be installed - - Asking for information about releases - - Requesting upgrading or uninstalling of existing releases - -**The Tiller Server** is an in-cluster server that interacts with the -Helm client, and interfaces with the Kubernetes API server. The server -is responsible for the following: - -- Listening for incoming requests from the Helm client -- Combining a chart and configuration to build a release -- Installing charts into Kubernetes, and then tracking the subsequent - release -- Upgrading and uninstalling charts by interacting with Kubernetes - -In a nutshell, the client is responsible for managing charts, and the -server is responsible for managing releases. - -## Implementation - -The Helm client is written in the Go programming language, and uses the -gRPC protocol suite to interact with the Tiller server. - -The Tiller server is also written in Go. It provides a gRPC server to -connect with the client, and it uses the Kubernetes client library to -communicate with Kubernetes. Currently, that library uses REST+JSON. - -The Tiller server stores information in ConfigMaps located inside of -Kubernetes. It does not need its own database. - -Configuration files are, when possible, written in YAML. diff --git a/docs/chart_best_practices/README.md b/docs/chart_best_practices/README.md deleted file mode 100644 index 58cc6540704..00000000000 --- a/docs/chart_best_practices/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# The Chart Best Practices Guide - -This guide covers the Helm Team's considered best practices for creating charts. -It focuses on how charts should be structured. - -We focus primarily on best practices for charts that may be publicly deployed. -We know that many charts are for internal-use only, and authors of such charts -may find that their internal interests override our suggestions here. - -## Table of Contents - -- [General Conventions](conventions.md): Learn about general chart conventions. -- [Values Files](values.md): See the best practices for structuring `values.yaml`. -- [Templates](templates.md): Learn some of the best techniques for writing templates. -- [Requirements](requirements.md): Follow best practices for `requirements.yaml` files. -- [Labels and Annotations](labels.md): Helm has a _heritage_ of labeling and annotating. -- Kubernetes Resources: - - [Pods and Pod Specs](pods.md): See the best practices for working with pod specifications. - - [Role-Based Access Control](rbac.md): Guidance on creating and using service accounts, roles, and role bindings. - - [Third Party Resources](third_party_resources.md): Third Party Resources (TPRs) have their own associated best practices. - diff --git a/docs/chart_best_practices/conventions.md b/docs/chart_best_practices/conventions.md deleted file mode 100644 index 324ef88f9a8..00000000000 --- a/docs/chart_best_practices/conventions.md +++ /dev/null @@ -1,59 +0,0 @@ -# General Conventions - -This part of the Best Practices Guide explains general conventions. - -## Chart Names - -Chart names should be lower case letters and numbers. Words _may_ be separated with dashes (-): - -Examples: - -``` -drupal -nginx-lego -aws-cluster-autoscaler -``` - -Neither uppercase letters nor underscores should be used in chart names. Dots should not be used in chart names. - -The directory that contains a chart MUST have the same name as the chart. Thus, the chart `nginx-lego` MUST be created in a directory called `nginx-lego/`. This is not merely a stylistic detail, but a requirement of the Helm Chart format. - -## Version Numbers - -Wherever possible, Helm uses [SemVer 2](http://semver.org) to represent version numbers. (Note that Docker image tags do not necessarily follow SemVer, and are thus considered an unfortunate exception to the rule.) - -When SemVer versions are stored in Kubernetes labels, we conventionally alter the `+` character to an `_` character, as labels do not allow the `+` sign as a value. - -## Formatting YAML - -YAML files should be indented using _two spaces_ (and never tabs). - -## Usage of the Words Helm, Tiller, and Chart - -There are a few small conventions followed for using the words Helm, helm, Tiller, and tiller. - -- Helm refers to the project, and is often used as an umbrella term -- `helm` refers to the client-side command -- Tiller is the proper name of the backend -- `tiller` is the name of the binary run on the backend -- The term 'chart' does not need to be capitalized, as it is not a proper noun. - -When in doubt, use _Helm_ (with an uppercase 'H'). - -## Restricting Tiller by Version - -A `Chart.yaml` file can specify a `tillerVersion` SemVer constraint: - -```yaml -name: mychart -version: 0.2.0 -tillerVersion: ">=2.4.0" -``` - -This constraint should be set when templates use a new feature that was not -supported in older versions of Helm. While this parameter will accept sophisticated -SemVer rules, the best practice is to default to the form `>=2.4.0`, where `2.4.0` -is the version that introduced the new feature used in the chart. - -This feature was introduced in Helm 2.4.0, so any version of Tiller older than -2.4.0 will simply ignore this field. diff --git a/docs/chart_best_practices/custom_resource_definitions.md b/docs/chart_best_practices/custom_resource_definitions.md deleted file mode 100644 index 96690dc9b79..00000000000 --- a/docs/chart_best_practices/custom_resource_definitions.md +++ /dev/null @@ -1,37 +0,0 @@ -# Custom Resource Definitions - -This section of the Best Practices Guide deals with creating and using Custom Resource Definition -objects. - -When working with Custom Resource Definitions (CRDs), it is important to distinguish -two different pieces: - -- There is a declaration of a CRD. This is the YAML file that has the kind `CustomResourceDefinition` -- Then there are resources that _use_ the CRD. Say a CRD defines `foo.example.com/v1`. Any resource - that has `apiVersion: example.com/v1` and kind `Foo` is a resource that uses the CRD. - -## Install a CRD Declaration Before Using the Resource - -Helm is optimized to load as many resources into Kubernetes as fast as possible. -By design, Kubernetes can take an entire set of manifests and bring them all -online (this is called the reconciliation loop). - -But there's a difference with CRDs. - -For a CRD, the declaration must be registered before any resources of that CRDs -kind(s) can be used. And the registration process sometimes takes a few seconds. - -### Method 1: Separate Charts - -One way to do this is to put the CRD definition in one chart, and then put any -resources that use that CRD in _another_ chart. - -In this method, each chart must be installed separately. - -### Method 2: Pre-install Hooks - -To package the two together, add a `pre-install` hook to the CRD definition so -that it is fully installed before the rest of the chart is executed. - -Note that if you create the CRD with a `pre-install` hook, that CRD definition -will not be deleted when `helm delete` is run. diff --git a/docs/chart_best_practices/labels.md b/docs/chart_best_practices/labels.md deleted file mode 100644 index 7c3ac51db9f..00000000000 --- a/docs/chart_best_practices/labels.md +++ /dev/null @@ -1,32 +0,0 @@ -# Labels and Annotations - -This part of the Best Practices Guide discusses the best practices for using -labels and annotations in your chart. - -## Is it a Label or an Annotation? - -An item of metadata should be a label under the following conditions: - -- It is used by Kubernetes to identify this resource -- It is useful to expose to operators for the purpose of querying the system. - -For example, we suggest using `chart: NAME-VERSION` as a label so that operators -can conveniently find all of the instances of a particular chart to use. - -If an item of metadata is not used for querying, it should be set as an annotation -instead. - -Helm hooks are always annotations. - -## Standard Labels - -The following table defines common labels that Helm charts use. Helm itself never requires that a particular label be present. Labels that are marked REC -are recommended, and _should_ be placed onto a chart for global consistency. Those marked OPT are optional. These are idiomatic or commonly in use, but are not relied upon frequently for operational purposes. - -Name|Status|Description ------|------|---------- -heritage | REC | This should always be set to `{{ .Release.Service }}`. It is for finding all things managed by Tiller. -release | REC | This should be the `{{ .Release.Name }}`. -chart | REC | This should be the chart name and version: `{{ .Chart.Name }}-{{ .Chart.Version \| replace "+" "_" }}`. -app | REC | This should be the app name, reflecting the entire app. Usually `{{ template "name" . }}` is used for this. This is used by many Kubernetes manifests, and is not Helm-specific. -component | OPT | This is a common label for marking the different roles that pieces may play in an application. For example, `component: frontend`. diff --git a/docs/chart_best_practices/pods.md b/docs/chart_best_practices/pods.md deleted file mode 100644 index 3f26b02536d..00000000000 --- a/docs/chart_best_practices/pods.md +++ /dev/null @@ -1,69 +0,0 @@ -# Pods and PodTemplates - -This part of the Best Practices Guide discusses formatting the Pod and PodTemplate -portions in chart manifests. - -The following (non-exhaustive) list of resources use PodTemplates: - -- Deployment -- ReplicationController -- ReplicaSet -- DaemonSet -- StatefulSet - -## Images - -A container image should use a fixed tag or the SHA of the image. It should not use the tags `latest`, `head`, `canary`, or other tags that are designed to be "floating". - - -Images _may_ be defined in the `values.yaml` file to make it easy to swap out images. - -``` -image: {{ .Values.redisImage | quote }} -``` - -An image and a tag _may_ be defined in `values.yaml` as two separate fields: - -``` -image: "{{ .Values.redisImage }}:{{ .Values.redisTag }}" -``` - -## ImagePullPolicy - -`helm create` sets the `imagePullPolicy` to `IfNotPresent` by default by doing the following in your `deployment.yaml`: - -```yaml -imagePullPolicy: {{ .Values.image.pullPolicy }} -``` - -And `values.yaml`: - -```yaml -pullPolicy: IfNotPresent -``` - -Similarly, Kubernetes defaults the `imagePullPolicy` to `IfNotPresent` if it is not defined at all. If you want a value other than `IfNotPresent`, simply update the value in `values.yaml` to your desired value. - - -## PodTemplates Should Declare Selectors - -All PodTemplate sections should specify a selector. For example: - -```yaml -selector: - matchLabels: - app: MyName -template: - metadata: - labels: - app: MyName -``` - -This is a good practice because it makes the relationship between the set and -the pod. - -But this is even more important for sets like Deployment. -Without this, the _entire_ set of labels is used to select matching pods, and -this will break if you use labels that change, like version or release date. - - diff --git a/docs/chart_best_practices/rbac.md b/docs/chart_best_practices/rbac.md deleted file mode 100644 index d699e4fddc0..00000000000 --- a/docs/chart_best_practices/rbac.md +++ /dev/null @@ -1,63 +0,0 @@ -# Role-Based Access Control - -This part of the Best Practices Guide discusses the creation and formatting of RBAC resources in chart manifests. - -RBAC resources are: - -- ServiceAccount (namespaced) -- Role (namespaced) -- ClusterRole -- RoleBinding (namespaced) -- ClusterRoleBinding - -## YAML Configuration - -RBAC and ServiceAccount configuration should happen under separate keys. They are separate things. Splitting these two concepts out in the YAML disambiguates them and make this clearer. - -```yaml -rbac: - # Specifies whether RBAC resources should be created - create: true - -serviceAccount: - # Specifies whether a ServiceAccount should be created - create: true - # The name of the ServiceAccount to use. - # If not set and create is true, a name is generated using the fullname template - name: -``` - -This structure can be extended for more complex charts that require multiple ServiceAccounts. - -```yaml -serviceAccounts: - client: - create: true - name: - server: - create: true - name: -``` - -## RBAC Resources Should be Created by Default - -`rbac.create` should be a boolean value controlling whether RBAC resources are created. The default should be `true`. Users who wish to manage RBAC access controls themselves can set this value to `false` (in which case see below). - -## Using RBAC Resources - -`serviceAccount.name` should set to the name of the ServiceAccount to be used by access-controlled resources created by the chart. If `serviceAccount.create` is true, then a ServiceAccount with this name should be created. If the name is not set, then a name is generated using the `fullname` template, If `serviceAccount.create` is false, then it should not be created, but it should still be associated with the same resources so that manually-created RBAC resources created later that reference it will function correctly. If `serviceAccount.create` is false and the name is not specified, then the default ServiceAccount is used. - -The following helper template should be used for the ServiceAccount. - -```yaml -{{/* -Create the name of the service account to use -*/}} -{{- define "mychart.serviceAccountName" -}} -{{- if .Values.serviceAccount.create -}} - {{ default (include "mychart.fullname" .) .Values.serviceAccount.name }} -{{- else -}} - {{ default "default" .Values.serviceAccount.name }} -{{- end -}} -{{- end -}} -``` diff --git a/docs/chart_best_practices/requirements.md b/docs/chart_best_practices/requirements.md deleted file mode 100644 index ca63f34253d..00000000000 --- a/docs/chart_best_practices/requirements.md +++ /dev/null @@ -1,47 +0,0 @@ -# Requirements Files - -This section of the guide covers best practices for `requirements.yaml` files. - -## Versions - -Where possible, use version ranges instead of pinning to an exact version. The suggested default is to use a patch-level version match: - -```yaml -version: ~1.2.3 -``` - -This will match version `1.2.3` and any patches to that release. In other words, `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0` - -For the complete version matching syntax, please see the [semver documentation](https://github.com/Masterminds/semver#checking-version-constraints) - -### Repository URLs - -Where possible, use `https://` repository URLs, followed by `http://` URLs. - -If the repository has been added to the repository index file, the repository name can be used as an alias of URL. Use `alias:` or `@` followed by repository names. - -File URLs (`file://...`) are considered a "special case" for charts that are assembled by a fixed deployment pipeline. Charts that use `file://` in a `requirements.yaml` file are not allowed in the official Helm repository. - -## Conditions and Tags - -Conditions or tags should be added to any dependencies that _are optional_. - -The preferred form of a condition is: - -```yaml -condition: somechart.enabled -``` - -Where `somechart` is the chart name of the dependency. - -When multiple subcharts (dependencies) together provide an optional or swappable feature, those charts should share the same tags. - -For example, if both `nginx` and `memcached` together provided performance optimizations for the main app in the chart, and were required to both be present when that feature is enabled, then they might both have a -tags section like this: - -``` -tags: - - webaccelerator -``` - -This allows a user to turn that feature on and off with one tag. diff --git a/docs/chart_best_practices/templates.md b/docs/chart_best_practices/templates.md deleted file mode 100644 index 7b21c5feac5..00000000000 --- a/docs/chart_best_practices/templates.md +++ /dev/null @@ -1,198 +0,0 @@ -# Templates - -This part of the Best Practices Guide focuses on templates. - -## Structure of templates/ - -The templates directory should be structured as follows: - -- Template files should have the extension `.yaml` if they produce YAML output. The - extension `.tpl` may be used for template files that produce no formatted content. -- Template file names should use dashed notation (`my-example-configmap.yaml`), not camelcase. -- Each resource definition should be in its own template file. -- Template file names should reflect the resource kind in the name. e.g. `foo-pod.yaml`, - `bar-svc.yaml` - -## Names of Defined Templates - -Defined templates (templates created inside a `{{ define }} ` directive) are -globally accessible. That means that a chart and all of its subcharts will have -access to all of the templates created with `{{ define }}`. - -For that reason, _all defined template names should be namespaced._ - -Correct: - -```yaml -{{- define "nginx.fullname" }} -{{/* ... */}} -{{ end -}} -``` - -Incorrect: - -```yaml -{{- define "fullname" -}} -{{/* ... */}} -{{ end -}} -``` -It is highly recommended that new charts are created via `helm create` command as the template names are automatically defined as per this best practice. - -## Formatting Templates - -Templates should be indented using _two spaces_ (never tabs). - -Template directives should have whitespace after the opening braces and before the -closing braces: - -Correct: -``` -{{ .foo }} -{{ print "foo" }} -{{- print "bar" -}} -``` - -Incorrect: -``` -{{.foo}} -{{print "foo"}} -{{-print "bar"-}} -``` - -Templates should chomp whitespace where possible: - -``` -foo: - {{- range .Values.items }} - {{ . }} - {{ end -}} -``` - -Blocks (such as control structures) may be indented to indicate flow of the template code. - -``` -{{ if $foo -}} - {{- with .Bar }}Hello{{ end -}} -{{- end -}} -``` - -However, since YAML is a whitespace-oriented language, it is often not possible for code indentation to follow that convention. - -## Whitespace in Generated Templates - -It is preferable to keep the amount of whitespace in generated templates to -a minimum. In particular, numerous blank lines should not appear adjacent to each -other. But occasional empty lines (particularly between logical sections) is -fine. - -This is best: - -```yaml -apiVersion: batch/v1 -kind: Job -metadata: - name: example - labels: - first: first - second: second -``` - -This is okay: - -```yaml -apiVersion: batch/v1 -kind: Job - -metadata: - name: example - - labels: - first: first - second: second - -``` - -But this should be avoided: - -```yaml -apiVersion: batch/v1 -kind: Job - -metadata: - name: example - - - - - - labels: - first: first - - second: second - -``` - -## Comments (YAML Comments vs. Template Comments) - -Both YAML and Helm Templates have comment markers. - -YAML comments: -```yaml -# This is a comment -type: sprocket -``` - -Template Comments: -```yaml -{{- /* -This is a comment. -*/ -}} -type: frobnitz -``` - -Template comments should be used when documenting features of a template, such as explaining a defined template: - -```yaml -{{- /* -mychart.shortname provides a 6 char truncated version of the release name. -*/ -}} -{{ define "mychart.shortname" -}} -{{ .Release.Name | trunc 6 }} -{{- end -}} - -``` - -Inside of templates, YAML comments may be used when it is useful for Helm users to (possibly) see the comments during debugging. - -``` -# This may cause problems if the value is more than 100Gi -memory: {{ .Values.maxMem | quote }} -``` - -The comment above is visible when the user runs `helm install --debug`, while -comments specified in `{{- /* */ -}}` sections are not. - -## Use of JSON in Templates and Template Output - -YAML is a superset of JSON. In some cases, using a JSON syntax can be more -readable than other YAML representations. - -For example, this YAML is closer to the normal YAML method of expressing lists: - -```yaml -arguments: - - "--dirname" - - "/foo" -``` - -But it is easier to read when collapsed into a JSON list style: - -```yaml -arguments: ["--dirname", "/foo"] -``` - -Using JSON for increased legibility is good. However, JSON syntax should not -be used for representing more complex constructs. - -When dealing with pure JSON embedded inside of YAML (such as init container -configuration), it is of course appropriate to use the JSON format. diff --git a/docs/chart_best_practices/values.md b/docs/chart_best_practices/values.md deleted file mode 100644 index 2962e7d4514..00000000000 --- a/docs/chart_best_practices/values.md +++ /dev/null @@ -1,155 +0,0 @@ -# Values - -This part of the best practices guide covers using values. In this part of the -guide, we provide recommendations on how you should structure and use your -values, with focus on designing a chart's `values.yaml` file. - -## Naming Conventions - -Variables names should begin with a lowercase letter, and words should be -separated with camelcase: - -Correct: - -```yaml -chicken: true -chickenNoodleSoup: true -``` - -Incorrect: - -```yaml -Chicken: true # initial caps may conflict with built-ins -chicken-noodle-soup: true # do not use hyphens in the name -``` - -Note that all of Helm's built-in variables begin with an uppercase letter to -easily distinguish them from user-defined values: `.Release.Name`, -`.Capabilities.KubeVersion`. - -## Flat or Nested Values - -YAML is a flexible format, and values may be nested deeply or flattened. - -Nested: - -```yaml -server: - name: nginx - port: 80 -``` - -Flat: - -```yaml -serverName: nginx -serverPort: 80 -``` - -In most cases, flat should be favored over nested. The reason for this is that -it is simpler for template developers and users. - - -For optimal safety, a nested value must be checked at every level: - -``` -{{ if .Values.server }} - {{ default "none" .Values.server.name }} -{{ end }} -``` - -For every layer of nesting, an existence check must be done. But for flat -configuration, such checks can be skipped, making the template easier to read -and use. - -``` -{{ default "none" .Values.serverName }} -``` - -When there are a large number of related variables, and at least one of them -is non-optional, nested values may be used to improve readability. - -## Make Types Clear - -YAML's type coercion rules are sometimes counterintuitive. For example, -`foo: false` is not the same as `foo: "false"`. Large integers like `foo: 12345678` -will get converted to scientific notation in some cases. - -The easiest way to avoid type conversion errors is to be explicit about strings, -and implicit about everything else. Or, in short, _quote all strings_. - -Often, to avoid the integer casting issues, it is advantageous to store your -integers as strings as well, and use `{{ int $value }}` in the template to convert -from a string back to an integer. - -In most cases, explicit type tags are respected, so `foo: !!string 1234` should -treat `1234` as a string. _However_, the YAML parser consumes tags, so the type -data is lost after one parse. - -## Consider How Users Will Use Your Values - -There are three potential sources of values: - -- A chart's `values.yaml` file -- A values file supplied by `helm install -f` or `helm upgrade -f` -- The values passed to a `--set` or `--set-string` flag on `helm install` or `helm upgrade` - -When designing the structure of your values, keep in mind that users of your -chart may want to override them via either the `-f` flag or with the `--set` -option. - -Since `--set` is more limited in expressiveness, the first guidelines for writing -your `values.yaml` file is _make it easy to override from `--set`_. - -For this reason, it's often better to structure your values file using maps. - -Difficult to use with `--set`: - -```yaml -servers: - - name: foo - port: 80 - - name: bar - port: 81 -``` - -The above cannot be expressed with `--set` in Helm `<=2.4`. In Helm 2.5, the -accessing the port on foo is `--set servers[0].port=80`. Not only is it harder -for the user to figure out, but it is prone to errors if at some later time the -order of the `servers` is changed. - -Easy to use: - -```yaml -servers: - foo: - port: 80 - bar: - port: 81 -``` - -Accessing foo's port is much more obvious: `--set servers.foo.port=80`. - -## Document 'values.yaml' - -Every defined property in 'values.yaml' should be documented. The documentation string should begin with the name of the property that it describes, and then give at least a one-sentence description. - -Incorrect: - -``` -# the host name for the webserver -serverHost = example -serverPort = 9191 -``` - -Correct: - -``` -# serverHost is the host name for the webserver -serverHost = example -# serverPort is the HTTP listener port for the webserver -serverPort = 9191 - -``` - -Beginning each comment with the name of the parameter it documents makes it easy to grep out documentation, and will enable documentation tools to reliably correlate doc strings with the parameters they describe. diff --git a/docs/chart_repository.md b/docs/chart_repository.md deleted file mode 100644 index 01d457e6324..00000000000 --- a/docs/chart_repository.md +++ /dev/null @@ -1,294 +0,0 @@ -# The Chart Repository Guide - -This section explains how to create and work with Helm chart repositories. At a -high level, a chart repository is a location where packaged charts can be -stored and shared. - -The official chart repository is maintained by the -[Kubernetes Charts](https://github.com/kubernetes/charts), and we welcome -participation. But Helm also makes it easy to create and run your own chart -repository. This guide explains how to do so. - -## Prerequisites - -* Go through the [Quickstart](quickstart.md) Guide -* Read through the [Charts](charts.md) document - -## Create a chart repository - -A _chart repository_ is an HTTP server that houses an `index.yaml` file and -optionally some packaged charts. When you're ready to share your charts, the -preferred way to do so is by uploading them to a chart repository. - -**Note:** For Helm 2.0.0, chart repositories do not have any intrinsic -authentication. There is an [issue tracking progress](https://github.com/kubernetes/helm/issues/1038) -in GitHub. - -Because a chart repository can be any HTTP server that can serve YAML and tar -files and can answer GET requests, you have a plethora of options when it comes -down to hosting your own chart repository. For example, you can use a Google -Cloud Storage (GCS) bucket, Amazon S3 bucket, Github Pages, or even create your -own web server. - -### The chart repository structure - -A chart repository consists of packaged charts and a special file called -`index.yaml` which contains an index of all of the charts in the repository. -Frequently, the charts that `index.yaml` describes are also hosted on the same -server, as are the [provenance files](provenance.md). - -For example, the layout of the repository `https://example.com/charts` might -look like this: - -``` -charts/ - | - |- index.yaml - | - |- alpine-0.1.2.tgz - | - |- alpine-0.1.2.tgz.prov -``` - -In this case, the index file would contain information about one chart, the Alpine -chart, and provide the download URL `https://example.com/charts/alpine-0.1.2.tgz` -for that chart. - -It is not required that a chart package be located on the same server as the -`index.yaml` file. However, doing so is often the easiest. - -### The index file - -The index file is a yaml file called `index.yaml`. It -contains some metadata about the package, including the contents of a -chart's `Chart.yaml` file. A valid chart repository must have an index file. The -index file contains information about each chart in the chart repository. The -`helm repo index` command will generate an index file based on a given local -directory that contains packaged charts. - -This is an example of an index file: - -``` -apiVersion: v1 -entries: - alpine: - - created: 2016-10-06T16:23:20.499814565-06:00 - description: Deploy a basic Alpine Linux pod - digest: 99c76e403d752c84ead610644d4b1c2f2b453a74b921f422b9dcb8a7c8b559cd - home: https://k8s.io/helm - name: alpine - sources: - - https://github.com/kubernetes/helm - urls: - - https://technosophos.github.io/tscharts/alpine-0.2.0.tgz - version: 0.2.0 - - created: 2016-10-06T16:23:20.499543808-06:00 - description: Deploy a basic Alpine Linux pod - digest: 515c58e5f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cd78727 - home: https://k8s.io/helm - name: alpine - sources: - - https://github.com/kubernetes/helm - urls: - - https://technosophos.github.io/tscharts/alpine-0.1.0.tgz - version: 0.1.0 - nginx: - - created: 2016-10-06T16:23:20.499543808-06:00 - description: Create a basic nginx HTTP server - digest: aaff4545f79d8b2913a10cb400ebb6fa9c77fe813287afbacf1a0b897cdffffff - home: https://k8s.io/helm - name: nginx - sources: - - https://github.com/kubernetes/charts - urls: - - https://technosophos.github.io/tscharts/nginx-1.1.0.tgz - version: 1.1.0 -generated: 2016-10-06T16:23:20.499029981-06:00 -``` - -A generated index and packages can be served from a basic webserver. You can test -things out locally with the `helm serve` command, which starts a local server. - -```console -$ helm serve --repo-path ./charts -Regenerating index. This may take a moment. -Now serving you on 127.0.0.1:8879 -``` - -The above starts a local webserver, serving the charts it finds in `./charts`. The -serve command will automatically generate an `index.yaml` file for you during -startup. - -## Hosting Chart Repositories - -This part shows several ways to serve a chart repository. - -### Google Cloud Storage - -The first step is to **create your GCS bucket**. We'll call ours -`fantastic-charts`. - -![Create a GCS Bucket](images/create-a-bucket.png) - -Next, make your bucket public by **editing the bucket permissions**. - -![Edit Permissions](images/edit-permissions.png) - -Insert this line item to **make your bucket public**: - -![Make Bucket Public](images/make-bucket-public.png) - -Congratulations, now you have an empty GCS bucket ready to serve charts! - -You may upload your chart repository using the Google Cloud Storage command line -tool, or using the GCS web UI. This is the technique the official Kubernetes -Charts repository hosts its charts, so you may want to take a -[peek at that project](https://github.com/kubernetes/charts) if you get stuck. - -**Note:** A public GCS bucket can be accessed via simple HTTPS at this address -`https://bucket-name.storage.googleapis.com/`. - -### JFrog Artifactory - -You can also set up chart repositories using JFrog Artifactory. -Read more about chart repositories with JFrog Artifactory [here](https://www.jfrog.com/confluence/display/RTF/Helm+Chart+Repositories) - -### Github Pages example - -In a similar way you can create charts repository using GitHub Pages. - -GitHub allows you to serve static web pages in two different ways: - -- By configuring a project to serve the contents of its `docs/` directory -- By configuring a project to serve a particular branch - -We'll take the second approach, though the first is just as easy. - -The first step will be to **create your gh-pages branch**. You can do that -locally as. - -```console -$ git checkout -b gh-pages -``` - -Or via web browser using **Branch** button on your Github repository: - -![Create Github Pages branch](images/create-a-gh-page-button.png) - -Next, you'll want to make sure your **gh-pages branch** is set as Github Pages, -click on your repo **Settings** and scroll down to **Github pages** section and -set as per below: - -![Create Github Pages branch](images/set-a-gh-page.png) - -By default **Source** usually gets set to **gh-pages branch**. If this is not set by default, then select it. - -You can use a **custom domain** there if you wish so. - -And check that **Enforce HTTPS** is ticked, so the **HTTPS** will be used when -charts are served. - -In such setup you can use **master branch** to store your charts code, and -**gh-pages branch** as charts repository, e.g.: -`https://USERNAME.github.io/REPONAME`. The demonstration [TS Charts](https://github.com/technosophos/tscharts) -repository is accessible at `https://technosophos.github.io/tscharts/`. - -### Ordinary web servers - -To configure an ordinary web server to serve Helm charts, you merely need to do -the following: - -- Put your index and charts in a directory that the server can serve -- Make sure the `index.yaml` file can be accessed with no authentication requirement -- Make sure `yaml` files are served with the correct content type (`text/yaml` or - `text/x-yaml`) - -For example, if you want to serve your charts out of `$WEBROOT/charts`, make sure -there is a `charts/` directory in your web root, and put the index file and -charts inside of that folder. - - -## Managing Chart Repositories - -Now that you have a chart repository, the last part of this guide explains how -to maintain charts in that repository. - - -### Store charts in your chart repository - -Now that you have a chart repository, let's upload a chart and an index file to -the repository. Charts in a chart repository must be packaged -(`helm package chart-name/`) and versioned correctly (following -[SemVer 2](https://semver.org/) guidelines). - -These next steps compose an example workflow, but you are welcome to use -whatever workflow you fancy for storing and updating charts in your chart -repository. - -Once you have a packaged chart ready, create a new directory, and move your -packaged chart to that directory. - -```console -$ helm package docs/examples/alpine/ -$ mkdir fantastic-charts -$ mv alpine-0.1.0.tgz fantastic-charts/ -$ helm repo index fantastic-charts --url https://fantastic-charts.storage.googleapis.com -``` - -The last command takes the path of the local directory that you just created and -the URL of your remote chart repository and composes an `index.yaml` file inside the -given directory path. - -Now you can upload the chart and the index file to your chart repository using -a sync tool or manually. If you're using Google Cloud Storage, check out this -[example workflow](chart_repository_sync_example.md) using the gsutil client. For -GitHub, you can simply put the charts in the appropriate destination branch. - -### Add new charts to an existing repository - -Each time you want to add a new chart to your repository, you must regenerate -the index. The `helm repo index` command will completely rebuild the `index.yaml` -file from scratch, including only the charts that it finds locally. - -However, you can use the `--merge` flag to incrementally add new charts to an -existing `index.yaml` file (a great option when working with a remote repository -like GCS). Run `helm repo index --help` to learn more, - -Make sure that you upload both the revised `index.yaml` file and the chart. And -if you generated a provenance file, upload that too. - -### Share your charts with others - -When you're ready to share your charts, simply let someone know what the URL of -your repository is. - -From there, they will add the repository to their helm client via the `helm -repo add [NAME] [URL]` command with any name they would like to use to -reference the repository. - -```console -$ helm repo add fantastic-charts https://fantastic-charts.storage.googleapis.com -$ helm repo list -fantastic-charts https://fantastic-charts.storage.googleapis.com -``` - -If the charts are backed by HTTP basic authentication, you can also supply the -username and password here: - -```console -$ helm repo add fantastic-charts https://fantastic-charts.storage.googleapis.com --username my-username --password my-password -$ helm repo list -fantastic-charts https://fantastic-charts.storage.googleapis.com -``` - -**Note:** A repository will not be added if it does not contain a valid -`index.yaml`. - -After that, your users will be able to search through your charts. After you've updated -the repository, they can use the `helm repo update` command to get the latest -chart information. - -*Under the hood, the `helm repo add` and `helm repo update` commands are -fetching the index.yaml file and storing them in the -`$HELM_HOME/repository/cache/` directory. This is where the `helm search` -function finds information about charts.* diff --git a/docs/chart_repository_faq.md b/docs/chart_repository_faq.md deleted file mode 100644 index a3e6392bac6..00000000000 --- a/docs/chart_repository_faq.md +++ /dev/null @@ -1,17 +0,0 @@ -# Chart Repositories: Frequently Asked Questions - -This section tracks some of the more frequently encountered issues with using chart repositories. - -**We'd love your help** making this document better. To add, correct, or remove -information, [file an issue](https://github.com/kubernetes/helm/issues) or -send us a pull request. - -## Fetching - -**Q: Why do I get a `unsupported protocol scheme ""` error when trying to fetch a chart from my custom repo?** - -A: (Helm < 2.5.0) This is likely caused by you creating your chart repo index without specifying the `--url` flag. -Try recreating your `index.yaml` file with a command like `helm repo index --url http://my-repo/charts .`, -and then re-uploading it to your custom charts repo. - -This behavior was changed in Helm 2.5.0. diff --git a/docs/chart_repository_sync_example.md b/docs/chart_repository_sync_example.md deleted file mode 100644 index de140c6360f..00000000000 --- a/docs/chart_repository_sync_example.md +++ /dev/null @@ -1,74 +0,0 @@ -# Syncing Your Chart Repository -*Note: This example is specifically for a Google Cloud Storage (GCS) bucket which serves a chart repository.* - -## Prerequisites -* Install the [gsutil](https://cloud.google.com/storage/docs/gsutil) tool. *We rely heavily on the gsutil rsync functionality* -* Be sure to have access to the Helm binary -* _Optional: We recommend you set [object versioning](https://cloud.google.com/storage/docs/gsutil/addlhelp/ObjectVersioningandConcurrencyControl#top_of_page) on your GCS bucket in case you accidentally delete something._ - -## Set up a local chart repository directory -Create a local directory like we did in [the chart repository guide](chart_repository.md), and place your packaged charts in that directory. - -For example: -```console -$ mkdir fantastic-charts -$ mv alpine-0.1.0.tgz fantastic-charts/ -``` - -## Generate an updated index.yaml -Use Helm to generate an updated index.yaml file by passing in the directory path and the url of the remote repository to the `helm repo index` command like this: - -```console -$ helm repo index fantastic-charts/ --url https://fantastic-charts.storage.googleapis.com -``` -This will generate an updated index.yaml file and place in the `fantastic-charts/` directory. - -## Sync your local and remote chart repositories -Upload the contents of the directory to your GCS bucket by running `scripts/sync-repo.sh` and pass in the local directory name and the GCS bucket name. - -For example: -```console -$ pwd -/Users/funuser/go/src/github.com/kubernetes/helm -$ scripts/sync-repo.sh fantastic-charts/ fantastic-charts -Getting ready to sync your local directory (fantastic-charts/) to a remote repository at gs://fantastic-charts -Verifying Prerequisites.... -Thumbs up! Looks like you have gsutil. Let's continue. -Building synchronization state... -Starting synchronization -Would copy file://fantastic-charts/alpine-0.1.0.tgz to gs://fantastic-charts/alpine-0.1.0.tgz -Would copy file://fantastic-charts/index.yaml to gs://fantastic-charts/index.yaml -Are you sure you would like to continue with these changes?? [y/N]} y -Building synchronization state... -Starting synchronization -Copying file://fantastic-charts/alpine-0.1.0.tgz [Content-Type=application/x-tar]... -Uploading gs://fantastic-charts/alpine-0.1.0.tgz: 740 B/740 B -Copying file://fantastic-charts/index.yaml [Content-Type=application/octet-stream]... -Uploading gs://fantastic-charts/index.yaml: 347 B/347 B -Congratulations your remote chart repository now matches the contents of fantastic-charts/ -``` -## Updating your chart repository -You'll want to keep a local copy of the contents of your chart repository or use `gsutil rsync` to copy the contents of your remote chart repository to a local directory. - -For example: -```console -$ gsutil rsync -d -n gs://bucket-name local-dir/ # the -n flag does a dry run -Building synchronization state... -Starting synchronization -Would copy gs://bucket-name/alpine-0.1.0.tgz to file://local-dir/alpine-0.1.0.tgz -Would copy gs://bucket-name/index.yaml to file://local-dir/index.yaml - -$ gsutil rsync -d gs://bucket-name local-dir/ # performs the copy actions -Building synchronization state... -Starting synchronization -Copying gs://bucket-name/alpine-0.1.0.tgz... -Downloading file://local-dir/alpine-0.1.0.tgz: 740 B/740 B -Copying gs://bucket-name/index.yaml... -Downloading file://local-dir/index.yaml: 346 B/346 B -``` - - -Helpful Links: -* Documentation on [gsutil rsync](https://cloud.google.com/storage/docs/gsutil/commands/rsync#description) -* [The Chart Repository Guide](chart_repository.md) -* Documentation on [object versioning and concurrency control](https://cloud.google.com/storage/docs/gsutil/addlhelp/ObjectVersioningandConcurrencyControl#overview) in Google Cloud Storage diff --git a/docs/chart_template_guide/accessing_files.md b/docs/chart_template_guide/accessing_files.md deleted file mode 100644 index 250fd9520f6..00000000000 --- a/docs/chart_template_guide/accessing_files.md +++ /dev/null @@ -1,209 +0,0 @@ -# Accessing Files Inside Templates - -In the previous section we looked at several ways to create and access named templates. This makes it easy to import one template from within another template. But sometimes it is desirable to import a _file that is not a template_ and inject its contents without sending the contents through the template renderer. - -Helm provides access to files through the `.Files` object. Before we get going with the template examples, though, there are a few things to note about how this works: - -- It is okay to add extra files to your Helm chart. These files will be bundled and sent to Tiller. Be careful, though. Charts must be smaller than 1M because of the storage limitations of Kubernetes objects. -- Some files cannot be accessed through the `.Files` object, usually for security reasons. - - Files in `templates/` cannot be accessed. - - Files excluded using `.helmignore` cannot be accessed. -- Charts do not preserve UNIX mode information, so file-level permissions will have no impact on the availability of a file when it comes to the `.Files` object. - - - - - -- [Basic example](#basic-example) -- [Path helpers](#path-helpers) -- [Glob patterns](#glob-patterns) -- [ConfigMap and Secrets utility functions](#configmap-and-secrets-utility-functions) -- [Encoding](#encoding) -- [Lines](#lines) - - - -## Basic example - -With those caveats behind, let's write a template that reads three files into our ConfigMap. To get started, we will add three files to the chart, putting all three directly inside of the `mychart/` directory. - -`config1.toml`: - -```toml -message = Hello from config 1 -``` - -`config2.toml`: - -```toml -message = This is config 2 -``` - -`config3.toml`: - -```toml -message = Goodbye from config 3 -``` - -Each of these is a simple TOML file (think old-school Windows INI files). We know the names of these files, so we can use a `range` function to loop through them and inject their contents into our ConfigMap. - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - {{- $files := .Files }} - {{- range tuple "config1.toml" "config2.toml" "config3.toml" }} - {{ . }}: |- - {{ $files.Get . }} - {{- end }} -``` - -This config map uses several of the techniques discussed in previous sections. For example, we create a `$files` variable to hold a reference to the `.Files` object. We also use the `tuple` function to create a list of files that we loop through. Then we print each file name (`{{.}}: |-`) followed by the contents of the file `{{ $files.Get . }}`. - -Running this template will produce a single ConfigMap with the contents of all three files: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: quieting-giraf-configmap -data: - config1.toml: |- - message = Hello from config 1 - - config2.toml: |- - message = This is config 2 - - config3.toml: |- - message = Goodbye from config 3 -``` - -## Path helpers - -When working with files, it can be very useful to perform some standard -operations on the file paths themselves. To help with this, Helm imports many of -the functions from Go's [path](https://golang.org/pkg/path/) package for your -use. They are all accessible with the same names as in the Go package, but -with a lowercase first letter. For example, `Base` becomes `base`, etc. - -The imported functions are: -- Base -- Dir -- Ext -- IsAbs -- Clean - -## Glob patterns - -As your chart grows, you may find you have a greater need to organize your -files more, and so we provide a `Files.Glob(pattern string)` method to assist -in extracting certain files with all the flexibility of [glob patterns](https://godoc.org/github.com/gobwas/glob). - -`.Glob` returns a `Files` type, so you may call any of the `Files` methods on -the returned object. - -For example, imagine the directory structure: - -``` -foo/: - foo.txt foo.yaml - -bar/: - bar.go bar.conf baz.yaml -``` - -You have multiple options with Globs: - - -```yaml -{{ range $path := .Files.Glob "**.yaml" }} -{{ $path }}: | -{{ .Files.Get $path }} -{{ end }} -``` - -Or - -```yaml -{{ range $path, $bytes := .Files.Glob "foo/*" }} -{{ $path }}: '{{ b64enc $bytes }}' -{{ end }} -``` - -## ConfigMap and Secrets utility functions - -(Not present in version 2.0.2 or prior) - -It is very common to want to place file content into both configmaps and -secrets, for mounting into your pods at run time. To help with this, we provide a -couple utility methods on the `Files` type. - -For further organization, it is especially useful to use these methods in -conjunction with the `Glob` method. - -Given the directory structure from the [Glob](#glob-patterns) example above: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: conf -data: -{{ (.Files.Glob "foo/*").AsConfig | indent 2 }} ---- -apiVersion: v1 -kind: Secret -metadata: - name: very-secret -type: Opaque -data: -{{ (.Files.Glob "bar/*").AsSecrets | indent 2 }} -``` - -## Encoding - -You can import a file and have the template base-64 encode it to ensure successful transmission: - -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: {{ .Release.Name }}-secret -type: Opaque -data: - token: |- - {{ .Files.Get "config1.toml" | b64enc }} -``` - -The above will take the same `config1.toml` file we used before and encode it: - -```yaml -# Source: mychart/templates/secret.yaml -apiVersion: v1 -kind: Secret -metadata: - name: lucky-turkey-secret -type: Opaque -data: - token: |- - bWVzc2FnZSA9IEhlbGxvIGZyb20gY29uZmlnIDEK -``` - -## Lines - -Sometimes it is desirable to access each line of a file in your template. We -provide a convenient `Lines` method for this. - -```yaml -data: - some-file.txt: {{ range .Files.Lines "foo/bar.txt" }} - {{ . }}{{ end }} -``` - -Currently, there is no way to pass files external to the chart during `helm install`. So if you are asking users to supply data, it must be loaded using `helm install -f` or `helm install --set`. - -This discussion wraps up our dive into the tools and techniques for writing Helm templates. In the next section we will see how you can use one special file, `templates/NOTES.txt`, to send post-installation instructions to the users of your chart. - diff --git a/docs/chart_template_guide/builtin_objects.md b/docs/chart_template_guide/builtin_objects.md deleted file mode 100644 index 11982229bcf..00000000000 --- a/docs/chart_template_guide/builtin_objects.md +++ /dev/null @@ -1,35 +0,0 @@ -# Built-in Objects - -Objects are passed into a template from the template engine. And your code can pass objects around (we'll see examples when we look at the `with` and `range` statements). There are even a few ways to create new objects within your templates, like with the `tuple` function we'll see later. - -Objects can be simple, and have just one value. Or they can contain other objects or functions. For example. the `Release` object contains several objects (like `Release.Name`) and the `Files` object has a few functions. - -In the previous section, we use `{{.Release.Name}}` to insert the name of a release into a template. `Release` is one of the top-level objects that you can access in your templates. - -- `Release`: This object describes the release itself. It has several objects inside of it: - - `Release.Name`: The release name - - `Release.Time`: The time of the release - - `Release.Namespace`: The namespace to be released into (if the manifest doesn't override) - - `Release.Service`: The name of the releasing service (always `Tiller`). - - `Release.Revision`: The revision number of this release. It begins at 1 and is incremented for each `helm upgrade`. - - `Release.IsUpgrade`: This is set to `true` if the current operation is an upgrade or rollback. - - `Release.IsInstall`: This is set to `true` if the current operation is an install. -- `Values`: Values passed into the template from the `values.yaml` file and from user-supplied files. By default, `Values` is empty. -- `Chart`: The contents of the `Chart.yaml` file. Any data in `Chart.yaml` will be accessible here. For example `{{.Chart.Name}}-{{.Chart.Version}}` will print out the `mychart-0.1.0`. - - The available fields are listed in the [Charts Guide](https://github.com/kubernetes/helm/blob/master/docs/charts.md#the-chartyaml-file) -- `Files`: This provides access to all non-special files in a chart. While you cannot use it to access templates, you can use it to access other files in the chart. See the section _Accessing Files_ for more. - - `Files.Get` is a function for getting a file by name (`.Files.Get config.ini`) - - `Files.GetBytes` is a function for getting the contents of a file as an array of bytes instead of as a string. This is useful for things like images. -- `Capabilities`: This provides information about what capabilities the Kubernetes cluster supports. - - `Capabilities.APIVersions` is a set of versions. - - `Capabilities.APIVersions.Has $version` indicates whether a version (`batch/v1`) is enabled on the cluster. - - `Capabilities.KubeVersion` provides a way to look up the Kubernetes version. It has the following values: `Major`, `Minor`, `GitVersion`, `GitCommit`, `GitTreeState`, `BuildDate`, `GoVersion`, `Compiler`, and `Platform`. - - `Capabilities.TillerVersion` provides a way to look up the Tiller version. It has the following values: `SemVer`, `GitCommit`, and `GitTreeState`. -- `Template`: Contains information about the current template that is being executed - - `Name`: A namespaced filepath to the current template (e.g. `mychart/templates/mytemplate.yaml`) - - `BasePath`: The namespaced path to the templates directory of the current chart (e.g. `mychart/templates`). - -The values are available to any top-level template. As we will see later, this does not necessarily mean that they will be available _everywhere_. - -The built-in values always begin with a capital letter. This is in keeping with Go's naming convention. When you create your own names, you are free to use a convention that suits your team. Some teams, like the [Kubernetes Charts](https://github.com/kubernetes/charts) team, choose to use only initial lower case letters in order to distinguish local names from those built-in. In this guide, we follow that convention. - diff --git a/docs/chart_template_guide/control_structures.md b/docs/chart_template_guide/control_structures.md deleted file mode 100644 index 7575ebc356a..00000000000 --- a/docs/chart_template_guide/control_structures.md +++ /dev/null @@ -1,354 +0,0 @@ -# Flow Control - -Control structures (called "actions" in template parlance) provide you, the template author, with the ability to control the flow of a template's generation. Helm's template language provides the following control structures: - -- `if`/`else` for creating conditional blocks -- `with` to specify a scope -- `range`, which provides a "for each"-style loop - -In addition to these, it provides a few actions for declaring and using named template segments: - -- `define` declares a new named template inside of your template -- `template` imports a named template -- `block` declares a special kind of fillable template area - -In this section, we'll talk about `if`, `with`, and `range`. The others are covered in the "Named Templates" section later in this guide. - -## If/Else - -The first control structure we'll look at is for conditionally including blocks of text in a template. This is the `if`/`else` block. - -The basic structure for a conditional looks like this: - -``` -{{ if PIPELINE }} - # Do something -{{ else if OTHER PIPELINE }} - # Do something else -{{ else }} - # Default case -{{ end }} -``` - -Notice that we're now talking about _pipelines_ instead of values. The reason for this is to make it clear that control structures can execute an entire pipeline, not just evaluate a value. - -A pipeline is evaluated as _false_ if the value is: - -- a boolean false -- a numeric zero -- an empty string -- a `nil` (empty or null) -- an empty collection (`map`, `slice`, `tuple`, `dict`, `array`) - -Under all other conditions, the condition is true. - -Let's add a simple conditional to our ConfigMap. We'll add another setting if the drink is set to coffee: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ .Values.favorite.drink | default "tea" | quote }} - food: {{ .Values.favorite.food | upper | quote }} - {{ if eq .Values.favorite.drink "coffee" }}mug: true{{ end }} -``` - -Since we commented out `drink: coffee` in our last example, the output should not include a `mug: true` flag. But if we add that line back into our `values.yaml` file, the output should look like this: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: eyewitness-elk-configmap -data: - myvalue: "Hello World" - drink: "coffee" - food: "PIZZA" - mug: true -``` - -## Controlling Whitespace - -While we're looking at conditionals, we should take a quick look at the way whitespace is controlled in templates. Let's take the previous example and format it to be a little easier to read: - -``` -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ .Values.favorite.drink | default "tea" | quote }} - food: {{ .Values.favorite.food | upper | quote }} - {{if eq .Values.favorite.drink "coffee"}} - mug: true - {{end}} -``` - -Initially, this looks good. But if we run it through the template engine, we'll get an unfortunate result: - -```console -$ helm install --dry-run --debug ./mychart -SERVER: "localhost:44134" -CHART PATH: /Users/mattbutcher/Code/Go/src/k8s.io/helm/_scratch/mychart -Error: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 9: did not find expected key -``` - -What happened? We generated incorrect YAML because of the whitespacing above. - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: eyewitness-elk-configmap -data: - myvalue: "Hello World" - drink: "coffee" - food: "PIZZA" - mug: true -``` - -`mug` is incorrectly indented. Let's simply out-dent that one line, and re-run: - -``` -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ .Values.favorite.drink | default "tea" | quote }} - food: {{ .Values.favorite.food | upper | quote }} - {{if eq .Values.favorite.drink "coffee"}} - mug: true - {{end}} -``` - -When we sent that, we'll get YAML that is valid, but still looks a little funny: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: telling-chimp-configmap -data: - myvalue: "Hello World" - drink: "coffee" - food: "PIZZA" - - mug: true - -``` - -Notice that we received a few empty lines in our YAML. Why? When the template engine runs, it _removes_ the contents inside of `{{` and `}}`, but it leaves the remaining whitespace exactly as is. - -YAML ascribes meaning to whitespace, so managing the whitespace becomes pretty important. Fortunately, Helm templates have a few tools to help. - -First, the curly brace syntax of template declarations can be modified with special characters to tell the template engine to chomp whitespace. `{{- ` (with the dash and space added) indicates that whitespace should be chomped left, while ` -}}` means whitespace to the right should be consumed. _Be careful! Newlines are whitespace!_ - -> Make sure there is a space between the `-` and the rest of your directive. `{{- 3 }}` means "trim left whitespace and print 3" while `{{-3}}` means "print -3". - -Using this syntax, we can modify our template to get rid of those new lines: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ .Values.favorite.drink | default "tea" | quote }} - food: {{ .Values.favorite.food | upper | quote }} - {{- if eq .Values.favorite.drink "coffee"}} - mug: true - {{- end}} -``` - -Just for the sake of making this point clear, let's adjust the above, and substitute an `*` for each whitespace that will be deleted following this rule. an `*` at the end of the line indicates a newline character that would be removed - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ .Values.favorite.drink | default "tea" | quote }} - food: {{ .Values.favorite.food | upper | quote }}* -**{{- if eq .Values.favorite.drink "coffee"}} - mug: true* -**{{- end}} - -``` - -Keeping that in mind, we can run our template through Helm and see the result: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: clunky-cat-configmap -data: - myvalue: "Hello World" - drink: "coffee" - food: "PIZZA" - mug: true -``` - -Be careful with the chomping modifiers. It is easy to accidentally do things like this: - -```yaml - food: {{ .Values.favorite.food | upper | quote }} - {{- if eq .Values.favorite.drink "coffee" -}} - mug: true - {{- end -}} - -``` - -That will produce `food: "PIZZA"mug:true` because it consumed newlines on both sides. - -> For the details on whitespace control in templates, see the [Official Go template documentation](https://godoc.org/text/template) - -Finally, sometimes it's easier to tell the template system how to indent for you instead of trying to master the spacing of template directives. For that reason, you may sometimes find it useful to use the `indent` function (`{{indent 2 "mug:true"}}`). - -## Modifying scope using `with` - -The next control structure to look at is the `with` action. This controls variable scoping. Recall that `.` is a reference to _the current scope_. So `.Values` tells the template to find the `Values` object in the current scope. - -The syntax for `with` is similar to a simple `if` statement: - -``` -{{ with PIPELINE }} - # restricted scope -{{ end }} -``` - -Scopes can be changed. `with` can allow you to set the current scope (`.`) to a particular object. For example, we've been working with `.Values.favorites`. Let's rewrite our ConfigMap to alter the `.` scope to point to `.Values.favorites`: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - {{- with .Values.favorite }} - drink: {{ .drink | default "tea" | quote }} - food: {{ .food | upper | quote }} - {{- end }} -``` - -(Note that we removed the `if` conditional from the previous exercise) - -Notice that now we can reference `.drink` and `.food` without qualifying them. That is because the `with` statement sets `.` to point to `.Values.favorite`. The `.` is reset to its previous scope after `{{ end }}`. - -But here's a note of caution! Inside of the restricted scope, you will not be able to access the other objects from the parent scope. This, for example, will fail: - -```yaml - {{- with .Values.favorite }} - drink: {{ .drink | default "tea" | quote }} - food: {{ .food | upper | quote }} - release: {{ .Release.Name }} - {{- end }} -``` - -It will produce an error because `Release.Name` is not inside of the restricted scope for `.`. However, if we swap the last two lines, all will work as expected because the scope is reset after `{{end}}`. - -```yaml - {{- with .Values.favorite }} - drink: {{ .drink | default "tea" | quote }} - food: {{ .food | upper | quote }} - {{- end }} - release: {{ .Release.Name }} -``` - -After looking a `range`, we will take a look at template variables, which offer one solution to the scoping issue above. - -## Looping with the `range` action - -Many programming languages have support for looping using `for` loops, `foreach` loops, or similar functional mechanisms. In Helm's template language, the way to iterate through a collection is to use the `range` operator. - -To start, let's add a list of pizza toppings to our `values.yaml` file: - -```yaml -favorite: - drink: coffee - food: pizza -pizzaToppings: - - mushrooms - - cheese - - peppers - - onions -``` - -Now we have a list (called a `slice` in templates) of `pizzaToppings`. We can modify our template to print this list into our ConfigMap: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - {{- with .Values.favorite }} - drink: {{ .drink | default "tea" | quote }} - food: {{ .food | upper | quote }} - {{- end }} - toppings: |- - {{- range .Values.pizzaToppings }} - - {{ . | title | quote }} - {{- end }} - -``` - -Let's take a closer look at the `toppings:` list. The `range` function will "range over" (iterate through) the `pizzaToppings` list. But now something interesting happens. Just like `with` sets the scope of `.`, so does a `range` operator. Each time through the loop, `.` is set to the current pizza topping. That is, the first time, `.` is set to `mushrooms`. The second iteration it is set to `cheese`, and so on. - -We can send the value of `.` directly down a pipeline, so when we do `{{ . | title | quote }}`, it sends `.` to `title` (title case function) and then to `quote`. If we run this template, the output will be: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: edgy-dragonfly-configmap -data: - myvalue: "Hello World" - drink: "coffee" - food: "PIZZA" - toppings: |- - - "Mushrooms" - - "Cheese" - - "Peppers" - - "Onions" -``` - -Now, in this example we've done something tricky. The `toppings: |-` line is declaring a multi-line string. So our list of toppings is actually not a YAML list. It's a big string. Why would we do this? Because the data in ConfigMaps `data` is composed of key/value pairs, where both the key and the value are simple strings. To understand why this is the case, take a look at the [Kubernetes ConfigMap docs](http://kubernetes.io/docs/user-guide/configmap/). For us, though, this detail doesn't matter much. - -> The `|-` marker in YAML takes a multi-line string. This can be a useful technique for embedding big blocks of data inside of your manifests, as exemplified here. - -Sometimes it's useful to be able to quickly make a list inside of your template, and then iterate over that list. Helm templates have a function to make this easy: `tuple`. In computer science, a tuple is a list-like collection of fixed size, but with arbitrary data types. This roughly conveys the way a `tuple` is used. - -```yaml - sizes: |- - {{- range tuple "small" "medium" "large" }} - - {{ . }} - {{- end }} -``` - -The above will produce this: - -```yaml - sizes: |- - - small - - medium - - large -``` - -In addition to lists and tuples, `range` can be used to iterate over collections that have a key and a value (like a `map` or `dict`). We'll see how to do that in the next section when we introduce template variables. diff --git a/docs/chart_template_guide/data_types.md b/docs/chart_template_guide/data_types.md deleted file mode 100644 index 2e6a9f15bec..00000000000 --- a/docs/chart_template_guide/data_types.md +++ /dev/null @@ -1,14 +0,0 @@ -# Appendix: Go Data Types and Templates - -The Helm template language is implemented in the strongly typed Go programming language. For that reason, variables in templates are _typed_. For the most part, variables will be exposed as one of the following types: - -- string: A string of text -- bool: a `true` or `false` -- int: An integer value (there are also 8, 16, 32, and 64 bit signed and unsigned variants of this) -- float64: a 64-bit floating point value (there are also 8, 16, and 32 bit varieties of this) -- a byte slice (`[]byte`), often used to hold (potentially) binary data -- struct: an object with properties and methods -- a slice (indexed list) of one of the previous types -- a string-keyed map (`map[string]interface{}`) where the value is one of the previous types - -There are many other types in Go, and sometimes you will have to convert between them in your templates. The easiest way to debug an object's type is to pass it through `printf "%t"` in a template, which will print the type. Also see the `typeOf` and `kindOf` functions. \ No newline at end of file diff --git a/docs/chart_template_guide/debugging.md b/docs/chart_template_guide/debugging.md deleted file mode 100644 index fac788cc48c..00000000000 --- a/docs/chart_template_guide/debugging.md +++ /dev/null @@ -1,30 +0,0 @@ -# Debugging Templates - -Debugging templates can be tricky simply because the templates are rendered on the Tiller server, not the Helm client. And then the rendered templates are sent to the Kubernetes API server, which may reject the YAML files for reasons other than formatting. - -There are a few commands that can help you debug. - -- `helm lint` is your go-to tool for verifying that your chart follows best practices -- `helm install --dry-run --debug`: We've seen this trick already. It's a great way to have the server render your templates, then return the resulting manifest file. -- `helm get manifest`: This is a good way to see what templates are installed on the server. - -When your YAML is failing to parse, but you want to see what is generated, one -easy way to retrieve the YAML is to comment out the problem section in the template, -and then re-run `helm install --dry-run --debug`: - -```YAML -apiVersion: v1 -# some: problem section -# {{ .Values.foo | quote }} -``` - -The above will be rendered and returned with the comments intact: - -```YAML -apiVersion: v1 -# some: problem section -# "bar" -``` - -This provides a quick way of viewing the generated content without YAML parse -errors blocking. diff --git a/docs/chart_template_guide/functions_and_pipelines.md b/docs/chart_template_guide/functions_and_pipelines.md deleted file mode 100644 index 54eb8e24f53..00000000000 --- a/docs/chart_template_guide/functions_and_pipelines.md +++ /dev/null @@ -1,155 +0,0 @@ -# Template Functions and Pipelines - -So far, we've seen how to place information into a template. But that information is placed into the template unmodified. Sometimes we want to transform the supplied data in a way that makes it more useable to us. - -Let's start with a best practice: When injecting strings from the `.Values` object into the template, we ought to quote these strings. We can do that by calling the `quote` function in the template directive: - -``` -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ quote .Values.favorite.drink }} - food: {{ quote .Values.favorite.food }} -``` - -Template functions follow the syntax `functionName arg1 arg2...`. In the snippet above, `quote .Values.favorite.drink` calls the `quote` function and passes it a single argument. - -Helm has over 60 available functions. Some of them are defined by the [Go template language](https://godoc.org/text/template) itself. Most of the others are part of the [Sprig template library](https://godoc.org/github.com/Masterminds/sprig). We'll see many of them as we progress through the examples. - -> While we talk about the "Helm template language" as if it is Helm-specific, it is actually a combination of the Go template language, some extra functions, and a variety of wrappers to expose certain objects to the templates. Many resources on Go templates may be helpful as you learn about templating. - -## Pipelines - -One of the powerful features of the template language is its concept of _pipelines_. Drawing on a concept from UNIX, pipelines are a tool for chaining together a series of template commands to compactly express a series of transformations. In other words, pipelines are an efficient way of getting several things done in sequence. Let's rewrite the above example using a pipeline. - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ .Values.favorite.drink | quote }} - food: {{ .Values.favorite.food | quote }} -``` - -In this example, instead of calling `quote ARGUMENT`, we inverted the order. We "sent" the argument to the function using a pipeline (`|`): `.Values.favorite.drink | quote`. Using pipelines, we can chain several functions together: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ .Values.favorite.drink | quote }} - food: {{ .Values.favorite.food | upper | quote }} -``` - -> Inverting the order is a common practice in templates. You will see `.val | quote` more often than `quote .val`. Either practice is fine. - -When evaluated, that template will produce this: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: trendsetting-p-configmap -data: - myvalue: "Hello World" - drink: "coffee" - food: "PIZZA" -``` - -Note that our original `pizza` has now been transformed to `"PIZZA"`. - -When pipelining arguments like this, the result of the first evaluation (`.Values.favorite.drink`) is sent as the _last argument to the function_. We can modify the drink example above to illustrate with a function that takes two arguments: `repeat COUNT STRING`: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ .Values.favorite.drink | repeat 5 | quote }} - food: {{ .Values.favorite.food | upper | quote }} -``` - -The `repeat` function will echo the given string the given number of times, so we will get this for output: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: melting-porcup-configmap -data: - myvalue: "Hello World" - drink: "coffeecoffeecoffeecoffeecoffee" - food: "PIZZA" -``` - -## Using the `default` function - -One function frequently used in templates is the `default` function: `default DEFAULT_VALUE GIVEN_VALUE`. This function allows you to specify a default value inside of the template, in case the value is omitted. Let's use it to modify the drink example above: - -```yaml -drink: {{ .Values.favorite.drink | default "tea" | quote }} -``` - -If we run this as normal, we'll get our `coffee`: - -``` -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: virtuous-mink-configmap -data: - myvalue: "Hello World" - drink: "coffee" - food: "PIZZA" -``` - -Now, we will remove the favorite drink setting from `values.yaml`: - -```yaml -favorite: - #drink: coffee - food: pizza -``` - -Now re-running `helm install --dry-run --debug ./mychart` will produce this YAML: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: fair-worm-configmap -data: - myvalue: "Hello World" - drink: "tea" - food: "PIZZA" -``` - -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. - -## Operators are functions - -For templates, the operators (`eq`, `ne`, `lt`, `gt`, `and`, `or` and so on) are all implemented as functions. In pipelines, operations can be grouped with parentheses (`(`, and `)`). - -Now we can turn from functions and pipelines to flow control with conditions, loops, and scope modifiers. diff --git a/docs/chart_template_guide/getting_started.md b/docs/chart_template_guide/getting_started.md deleted file mode 100644 index 87ae5fa3cde..00000000000 --- a/docs/chart_template_guide/getting_started.md +++ /dev/null @@ -1,213 +0,0 @@ -# Getting Started with a Chart Template - -In this section of the guide, we'll create a chart and then add a first template. The chart we created here will be used throughout the rest of the guide. - -To get going, let's take a brief look at a Helm chart. - -## Charts - -As described in the [Charts Guide](../charts.md), Helm charts are structured like -this: - -``` -mychart/ - Chart.yaml - values.yaml - charts/ - templates/ - ... -``` - -The `templates/` directory is for template files. When Tiller evaluates a chart, -it will send all of the files in the `templates/` directory through the -template rendering engine. Tiller then collects the results of those templates -and sends them on to Kubernetes. - -The `values.yaml` file is also important to templates. This file contains the -_default values_ for a chart. These values may be overridden by users during -`helm install` or `helm upgrade`. - -The `Chart.yaml` file contains a description of the chart. You can access it -from within a template. The `charts/` directory _may_ contain other charts (which -we call _subcharts_). Later in this guide we will see how those work when it -comes to template rendering. - -## A Starter Chart - -For this guide, we'll create a simple chart called `mychart`, and then we'll -create some templates inside of the chart. - -```console -$ helm create mychart -Creating mychart -``` - -From here on, we'll be working in the `mychart` directory. - -### A Quick Glimpse of `mychart/templates/` - -If you take a look at the `mychart/templates/` directory, you'll notice a few files -already there. - -- `NOTES.txt`: The "help text" for your chart. This will be displayed to your users - when they run `helm install`. -- `deployment.yaml`: A basic manifest for creating a Kubernetes [deployment](http://kubernetes.io/docs/user-guide/deployments/) -- `service.yaml`: A basic manifest for creating a [service endpoint](http://kubernetes.io/docs/user-guide/services/) for your deployment -- `_helpers.tpl`: A place to put template helpers that you can re-use throughout the chart - -And what we're going to do is... _remove them all!_ That way we can work through our tutorial from scratch. We'll actually create our own `NOTES.txt` and `_helpers.tpl` as we go. - -```console -$ rm -rf mychart/templates/*.* -``` - -When you're writing production grade charts, having basic versions of these charts can be really useful. So in your day-to-day chart authoring, you probably won't want to remove them. - -## A First Template - -The first template we are going to create will be a `ConfigMap`. In Kubernetes, -a ConfigMap is simply a container for storing configuration data. Other things, -like pods, can access the data in a ConfigMap. - -Because ConfigMaps are basic resources, they make a great starting point for us. - -Let's begin by creating a file called `mychart/templates/configmap.yaml`: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: mychart-configmap -data: - myvalue: "Hello World" -``` - -**TIP:** Template names do not follow a rigid naming pattern. However, we recommend -using the suffix `.yaml` for YAML files and `.tpl` for helpers. - -The YAML file above is a bare-bones ConfigMap, having the minimal necessary fields. -In virtue of the fact that this file is in the `templates/` directory, it will -be sent through the template engine. - -It is just fine to put a plain YAML file like this in the `templates/` directory. -When Tiller reads this template, it will simply send it to Kubernetes as-is. - -With this simple template, we now have an installable chart. And we can install -it like this: - -```console -$ helm install ./mychart -NAME: full-coral -LAST DEPLOYED: Tue Nov 1 17:36:01 2016 -NAMESPACE: default -STATUS: DEPLOYED - -RESOURCES: -==> v1/ConfigMap -NAME DATA AGE -mychart-configmap 1 1m -``` - -In the output above, we can see that our ConfigMap was created. Using Helm, we -can retrieve the release and see the actual template that was loaded. - -```console -$ helm get manifest full-coral - ---- -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: mychart-configmap -data: - myvalue: "Hello World" -``` - -The `helm get manifest` command takes a release name (`full-coral`) and prints -out all of the Kubernetes resources that were uploaded to the server. Each file -begins with `---` to indicate the start of a YAML document, and then is followed -by an automatically generated comment line that tells us what template file -generated this YAML document. - -From there on, we can see that the YAML data is exactly what we put in our -`configmap.yaml` file. - -Now we can delete our release: `helm delete full-coral`. - -### Adding a Simple Template Call - -Hard-coding the `name:` into a resource is usually considered to be bad practice. -Names should be unique to a release. So we might want to generate a name field -by inserting the release name. - -**TIP:** The `name:` field is limited to 63 characters because of limitations to -the DNS system. For that reason, release names are limited to 53 characters. -Kubernetes 1.3 and earlier limited to only 24 characters (thus 14 character names). - -Let's alter `configmap.yaml` accordingly. - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" -``` - -The big change comes in the value of the `name:` field, which is now -`{{ .Release.Name }}-configmap`. - -> A template directive is enclosed in `{{` and `}}` blocks. - -The template directive `{{ .Release.Name }}` injects the release name into the template. The values that are passed into a template can be thought of as _namespaced objects_, where a dot (`.`) separates each namespaced element. - -The leading dot before `Release` indicates that we start with the top-most namespace for this scope (we'll talk about scope in a bit). So we could read `.Release.Name` as "start at the top namespace, find the `Release` object, then look inside of it for an object called `Name`". - -The `Release` object is one of the built-in objects for Helm, and we'll cover it in more depth later. But for now, it is sufficient to say that this will display the release name that Tiller assigns to our release. - -Now when we install our resource, we'll immediately see the result of using this template directive: - -```console -$ helm install ./mychart -NAME: clunky-serval -LAST DEPLOYED: Tue Nov 1 17:45:37 2016 -NAMESPACE: default -STATUS: DEPLOYED - -RESOURCES: -==> v1/ConfigMap -NAME DATA AGE -clunky-serval-configmap 1 1m -``` - -Note that in the `RESOURCES` section, the name we see there is `clunky-serval-configmap` -instead of `mychart-configmap`. - -You can run `helm get manifest clunky-serval` to see the entire generated YAML. - -At this point, we've seen templates at their most basic: YAML files that have template directives embedded in `{{` and `}}`. In the next part, we'll take a deeper look into templates. But before moving on, there's one quick trick that can make building templates faster: When you want to test the template rendering, but not actually install anything, you can use `helm install --debug --dry-run ./mychart`. This will send the chart to the Tiller server, which will render the templates. But instead of installing the chart, it will return the rendered template to you so you can see the output: - -```console -$ helm install --debug --dry-run ./mychart -SERVER: "localhost:44134" -CHART PATH: /Users/mattbutcher/Code/Go/src/k8s.io/helm/_scratch/mychart -NAME: goodly-guppy -TARGET NAMESPACE: default -CHART: mychart 0.1.0 -MANIFEST: ---- -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: goodly-guppy-configmap -data: - myvalue: "Hello World" - -``` - -Using `--dry-run` will make it easier to test your code, but it won't ensure that Kubernetes itself will accept the templates you generate. It's best not to assume that your chart will install just because `--dry-run` works. - -In the next few sections, we'll take the basic chart we defined here and explore the Helm template language in detail. And we'll get started with built-in objects. diff --git a/docs/chart_template_guide/index.md b/docs/chart_template_guide/index.md deleted file mode 100644 index c2bcc8f4291..00000000000 --- a/docs/chart_template_guide/index.md +++ /dev/null @@ -1,16 +0,0 @@ -# The Chart Template Developer's Guide - -This guide provides an introduction to Helm's chart templates, with emphasis on -the template language. - -Templates generate manifest files, which are YAML-formatted resource descriptions -that Kubernetes can understand. We'll look at how templates are structured, -how they can be used, how to write Go templates, and how to debug your work. - -This guide focuses on the following concepts: - -- The Helm template language -- Using values -- Techniques for working with templates - -This guide is oriented toward learning the ins and outs of the Helm template language. Other guides provide introductory material, examples, and best practices. \ No newline at end of file diff --git a/docs/chart_template_guide/named_templates.md b/docs/chart_template_guide/named_templates.md deleted file mode 100644 index 33425d102bc..00000000000 --- a/docs/chart_template_guide/named_templates.md +++ /dev/null @@ -1,264 +0,0 @@ -# Named Templates - -It is time to move beyond one template, and begin to create others. In this section, we will see how to define _named templates_ in one file, and then use them elsewhere. A _named template_ (sometimes called a _partial_ or a _subtemplate_) is simply a template defined inside of a file, and given a name. We'll see two ways to create them, and a few different ways to use them. - -In the "Flow Control" section we introduced three actions for declaring and managing templates: `define`, `template`, and `block`. In this section, we'll cover those three actions, and also introduce a special-purpose `include` function that works similarly to the `template` action. - -An important detail to keep in mind when naming templates: **template names are global**. If you declare two templates with the same name, whichever one is loaded last will be the one used. Because templates in subcharts are compiled together with top-level templates, you should be careful to name your templates with _chart-specific names_. - -One popular naming convention is to prefix each defined template with the name of the chart: `{{ define "mychart.labels" }}`. By using the specific chart name as a prefix we can avoid any conflicts that may arise due to two different charts that implement templates of the same name. - -## Partials and `_` files - -So far, we've used one file, and that one file has contained a single template. But Helm's template language allows you to create named embedded templates, that can be accessed by name elsewhere. - -Before we get to the nuts-and-bolts of writing those templates, there is file naming convention that deserves mention: - -* Most files in `templates/` are treated as if they contain Kubernetes manifests -* The `NOTES.txt` is one exception -* But files whose name begins with an underscore (`_`) are assumed to _not_ have a manifest inside. These files are not rendered to Kubernetes object definitions, but are available everywhere within other chart templates for use. - -These files are used to store partials and helpers. In fact, when we first created `mychart`, we saw a file called `_helpers.tpl`. That file is the default location for template partials. - -## Declaring and using templates with `define` and `template` - -The `define` action allows us to create a named template inside of a template file. Its syntax goes like this: - -```yaml -{{ define "MY.NAME" }} - # body of template here -{{ end }} -``` - -For example, we can define a template to encapsulate a Kubernetes block of labels: - -```yaml -{{- define "mychart.labels" }} - labels: - generator: helm - date: {{ now | htmlDate }} -{{- end }} -``` - -Now we can embed this template inside of our existing ConfigMap, and then include it with the `template` action: - -```yaml -{{- define "mychart.labels" }} - labels: - generator: helm - date: {{ now | htmlDate }} -{{- end }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap - {{- template "mychart.labels" }} -data: - myvalue: "Hello World" - {{- range $key, $val := .Values.favorite }} - {{ $key }}: {{ $val | quote }} - {{- end }} -``` - -When the template engine reads this file, it will store away the reference to `mychart.labels` until `template "mychart.labels"` is called. Then it will render that template inline. So the result will look like this: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: running-panda-configmap - labels: - generator: helm - date: 2016-11-02 -data: - myvalue: "Hello World" - drink: "coffee" - food: "pizza" -``` - -Conventionally, Helm charts put these templates inside of a partials file, usually `_helpers.tpl`. Let's move this function there: - -```yaml -{{/* Generate basic labels */}} -{{- define "mychart.labels" }} - labels: - generator: helm - date: {{ now | htmlDate }} -{{- end }} -``` - -By convention, `define` functions should have a simple documentation block (`{{/* ... */}}`) describing what they do. - -Even though this definition is in `_helpers.tpl`, it can still be accessed in `configmap.yaml`: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap - {{- template "mychart.labels" }} -data: - myvalue: "Hello World" - {{- range $key, $val := .Values.favorite }} - {{ $key }}: {{ $val | quote }} - {{- end }} -``` - -As mentioned above, **template names are global**. As a result of this, if two templates are declared with the same name the last occurrence will be the one that is used. Since templates in subcharts are compiled together with top-level templates, it is best to name your templates with _chart specific names_. A popular naming convention is to prefix each defined template with the name of the chart: `{{ define "mychart.labels" }}`. - -## Setting the scope of a template - -In the template we defined above, we did not use any objects. We just used functions. Let's modify our defined template to include the chart name and chart version: - -```yaml -{{/* Generate basic labels */}} -{{- define "mychart.labels" }} - labels: - generator: helm - date: {{ now | htmlDate }} - chart: {{ .Chart.Name }} - version: {{ .Chart.Version }} -{{- end }} -``` - -If we render this, the result will not be what we expect: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: moldy-jaguar-configmap - labels: - generator: helm - date: 2016-11-02 - chart: - version: -``` - -What happened to the name and version? They weren't in the scope for our defined template. When a named template (created with `define`) is rendered, it will receive the scope passed in by the `template` call. In our example, we included the template like this: - -```yaml -{{- template "mychart.labels" }} -``` - -No scope was passed in, so within the template we cannot access anything in `.`. This is easy enough to fix, though. We simply pass a scope to the template: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap - {{- template "mychart.labels" . }} -``` - -Note that we pass `.` at the end of the `template` call. We could just as easily pass `.Values` or `.Values.favorite` or whatever scope we want. But what we want is the top-level scope. - -Now when we execute this template with `helm install --dry-run --debug ./mychart`, we get this: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: plinking-anaco-configmap - labels: - generator: helm - date: 2016-11-02 - chart: mychart - version: 0.1.0 -``` - -Now `{{ .Chart.Name }}` resolves to `mychart`, and `{{ .Chart.Version }}` resolves to `0.1.0`. - -## The `include` function - -Say we've defined a simple template that looks like this: - -```yaml -{{- define "mychart.app" -}} -app_name: {{ .Chart.Name }} -app_version: "{{ .Chart.Version }}+{{ .Release.Time.Seconds }}" -{{- end -}} -``` - -Now say I want to insert this both into the `labels:` section of my template, and also the `data:` section: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap - labels: - {{ template "mychart.app" .}} -data: - myvalue: "Hello World" - {{- range $key, $val := .Values.favorite }} - {{ $key }}: {{ $val | quote }} - {{- end }} -{{ template "mychart.app" . }} -``` - -The output will not be what we expect: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: measly-whippet-configmap - labels: - app_name: mychart -app_version: "0.1.0+1478129847" -data: - myvalue: "Hello World" - drink: "coffee" - food: "pizza" - app_name: mychart -app_version: "0.1.0+1478129847" -``` - -Note that the indentation on `app_version` is wrong in both places. Why? Because the template that is substituted in has the text aligned to the right. Because `template` is an action, and not a function, there is no way to pass the output of a `template` call to other functions; the data is simply inserted inline. - -To work around this case, Helm provides an alternative to `template` that will import the contents of a template into the present pipeline where it can be passed along to other functions in the pipeline. - -Here's the example above, corrected to use `indent` to indent the `mychart_app` template correctly: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap - labels: -{{ include "mychart.app" . | indent 4 }} -data: - myvalue: "Hello World" - {{- range $key, $val := .Values.favorite }} - {{ $key }}: {{ $val | quote }} - {{- end }} -{{ include "mychart.app" . | indent 2 }} -``` - -Now the produced YAML is correctly indented for each section: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: edgy-mole-configmap - labels: - app_name: mychart - app_version: "0.1.0+1478129987" -data: - myvalue: "Hello World" - drink: "coffee" - food: "pizza" - app_name: mychart - app_version: "0.1.0+1478129987" -``` - -> It is considered preferable to use `include` over `template` in Helm templates simply so that the output formatting can be handled better for YAML documents. - -Sometimes we want to import content, but not as templates. That is, we want to import files verbatim. We can achieve this by accessing files through the `.Files` object described in the next section. diff --git a/docs/chart_template_guide/notes_files.md b/docs/chart_template_guide/notes_files.md deleted file mode 100644 index 5a8b78ca436..00000000000 --- a/docs/chart_template_guide/notes_files.md +++ /dev/null @@ -1,45 +0,0 @@ -# Creating a NOTES.txt File - -In this section we are going to look at Helm's tool for providing instructions to your chart users. At the end of a `chart install` or `chart upgrade`, Helm can print out a block of helpful information for users. This information is highly customizable using templates. - -To add installation notes to your chart, simply create a `templates/NOTES.txt` file. This file is plain text, but it is processed like as a template, and has all the normal template functions and objects available. - -Let's create a simple `NOTES.txt` file: - -``` -Thank you for installing {{ .Chart.Name }}. - -Your release is named {{ .Release.Name }}. - -To learn more about the release, try: - - $ helm status {{ .Release.Name }} - $ helm get {{ .Release.Name }} - -``` - -Now if we run `helm install ./mychart` we will see this message at the bottom: - -``` -RESOURCES: -==> v1/Secret -NAME TYPE DATA AGE -rude-cardinal-secret Opaque 1 0s - -==> v1/ConfigMap -NAME DATA AGE -rude-cardinal-configmap 3 0s - - -NOTES: -Thank you for installing mychart. - -Your release is named rude-cardinal. - -To learn more about the release, try: - - $ helm status rude-cardinal - $ helm get rude-cardinal -``` - -Using `NOTES.txt` this way is a great way to give your users detailed information about how to use their newly installed chart. Creating a `NOTES.txt` file is strongly recommended, though it is not required. diff --git a/docs/chart_template_guide/subcharts_and_globals.md b/docs/chart_template_guide/subcharts_and_globals.md deleted file mode 100644 index 33274effe0f..00000000000 --- a/docs/chart_template_guide/subcharts_and_globals.md +++ /dev/null @@ -1,207 +0,0 @@ -# Subcharts and Global Values - -To this point we have been working only with one chart. But charts can have dependencies, called _subcharts_, that also have their own values and templates. In this section we will create a subchart and see the different ways we can access values from within templates. - -Before we dive into the code, there are a few important details to learn about subcharts. - -1. A subchart is considered "stand-alone", which means a subchart can never explicitly depend on its parent chart. -2. For that reason, a subchart cannot access the values of its parent. -3. A parent chart can override values for subcharts. -4. Helm has a concept of _global values_ that can be accessed by all charts. - -As we walk through the examples in this section, many of these concepts will become clearer. - -## Creating a Subchart - -For these exercises, we'll start with the `mychart/` chart we created at the beginning of this guide, and we'll add a new chart inside of it. - -```console -$ cd mychart/charts -$ helm create mysubchart -Creating mysubchart -$ rm -rf mysubchart/templates/*.* -``` - -Notice that just as before, we deleted all of the base templates so that we can start from scratch. In this guide, we are focused on how templates work, not on managing dependencies. But the [Charts Guide](../charts.md) has more information on how subcharts work. - -## Adding Values and a Template to the Subchart - -Next, let's create a simple template and values file for our `mysubchart` chart. There should already be a `values.yaml` in `mychart/charts/mysubchart`. We'll set it up like this: - -```yaml -dessert: cake -``` - -Next, we'll create a new ConfigMap template in `mychart/charts/mysubchart/templates/configmap.yaml`: - -``` -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-cfgmap2 -data: - dessert: {{ .Values.dessert }} -``` - -Because every subchart is a _stand-alone chart_, we can test `mysubchart` on its own: - -```console -$ helm install --dry-run --debug mychart/charts/mysubchart -SERVER: "localhost:44134" -CHART PATH: /Users/mattbutcher/Code/Go/src/k8s.io/helm/_scratch/mychart/charts/mysubchart -NAME: newbie-elk -TARGET NAMESPACE: default -CHART: mysubchart 0.1.0 -MANIFEST: ---- -# Source: mysubchart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: newbie-elk-cfgmap2 -data: - dessert: cake -``` - -## Overriding Values from a Parent Chart - -Our original chart, `mychart` is now the _parent_ chart of `mysubchart`. This relationship is based entirely on the fact that `mysubchart` is within `mychart/charts`. - -Because `mychart` is a parent, we can specify configuration in `mychart` and have that configuration pushed into `mysubchart`. For example, we can modify `mychart/values.yaml` like this: - -```yaml -favorite: - drink: coffee - food: pizza -pizzaToppings: - - mushrooms - - cheese - - peppers - - onions - -mysubchart: - dessert: ice cream -``` - -Note the last two lines. Any directives inside of the `mysubchart` section will be sent to the `mysubchart` chart. So if we run `helm install --dry-run --debug mychart`, one of the things we will see is the `mysubchart` ConfigMap: - -```yaml -# Source: mychart/charts/mysubchart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: unhinged-bee-cfgmap2 -data: - dessert: ice cream -``` - -The value at the top level has now overridden the value of the subchart. - -There's an important detail to notice here. We didn't change the template of `mychart/charts/mysubchart/templates/configmap.yaml` to point to `.Values.mysubchart.dessert`. From that template's perspective, the value is still located at `.Values.dessert`. As the template engine passes values along, it sets the scope. So for the `mysubchart` templates, only values specifically for `mysubchart` will be available in `.Values`. - -Sometimes, though, you do want certain values to be available to all of the templates. This is accomplished using global chart values. - -## Global Chart Values - -Global values are values that can be accessed from any chart or subchart by exactly the same name. Globals require explicit declaration. You can't use an existing non-global as if it were a global. - -The Values data type has a reserved section called `Values.global` where global values can be set. Let's set one in our `mychart/values.yaml` file. - -```yaml -favorite: - drink: coffee - food: pizza -pizzaToppings: - - mushrooms - - cheese - - peppers - - onions - -mysubchart: - dessert: ice cream - -global: - salad: caesar -``` - -Because of the way globals work, both `mychart/templates/configmap.yaml` and `mysubchart/templates/configmap.yaml` should be able to access that value as `{{ .Values.global.salad}}`. - -`mychart/templates/configmap.yaml`: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - salad: {{ .Values.global.salad }} -``` - -`mysubchart/templates/configmap.yaml`: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-cfgmap2 -data: - dessert: {{ .Values.dessert }} - salad: {{ .Values.global.salad }} -``` - -Now if we run a dry run install, we'll see the same value in both outputs: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: silly-snake-configmap -data: - salad: caesar - ---- -# Source: mychart/charts/mysubchart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: silly-snake-cfgmap2 -data: - dessert: ice cream - salad: caesar -``` - -Globals are useful for passing information like this, though it does take some planning to make sure the right templates are configured to use globals. - -## Sharing Templates with Subcharts - -Parent charts and subcharts can share templates. Any defined block in any chart is -available to other charts. - -For example, we can define a simple template like this: - -```yaml -{{- define "labels" }}from: mychart{{ end }} -``` - -Recall how the labels on templates are _globally shared_. Thus, the `labels` chart -can be included from any other chart. - -While chart developers have a choice between `include` and `template`, one advantage -of using `include` is that `include` can dynamically reference templates: - -```yaml -{{ include $mytemplate }} -``` - -The above will dereference `$mytemplate`. The `template` function, in contrast, -will only accept a string literal. - -## Avoid Using Blocks - -The Go template language provides a `block` keyword that allows developers to provide -a default implementation which is overridden later. In Helm charts, blocks are not -the best tool for overriding because it if multiple implementations of the same block -are provided, the one selected is unpredictable. - -The suggestion is to instead use `include`. diff --git a/docs/chart_template_guide/values_files.md b/docs/chart_template_guide/values_files.md deleted file mode 100644 index 32a178735ee..00000000000 --- a/docs/chart_template_guide/values_files.md +++ /dev/null @@ -1,132 +0,0 @@ -# Values Files - -In the previous section we looked at the built-in objects that Helm templates offer. One of the four built-in objects is `Values`. This object provides access to values passed into the chart. Its contents come from four sources: - -- The `values.yaml` file in the chart -- If this is a subchart, the `values.yaml` file of a parent chart -- A values file if passed into `helm install` or `helm upgrade` with the `-f` flag (`helm install -f myvals.yaml ./mychart`) -- Individual parameters passed with `--set` (such as `helm install --set foo=bar ./mychart`) - -The list above is in order of specificity: `values.yaml` is the default, which can be overridden by a parent chart's `values.yaml`, which can in turn be overridden by a user-supplied values file, which can in turn be overridden by `--set` parameters. - -Values files are plain YAML files. Let's edit `mychart/values.yaml` and then edit our ConfigMap template. - -Removing the defaults in `values.yaml`, we'll set just one parameter: - -```yaml -favoriteDrink: coffee -``` - -Now we can use this inside of a template: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ .Values.favoriteDrink }} -``` - -Notice on the last line we access `favoriteDrink` as an attribute of `Values`: `{{ .Values.favoriteDrink}}`. - -Let's see how this renders. - -```console -$ helm install --dry-run --debug ./mychart -SERVER: "localhost:44134" -CHART PATH: /Users/mattbutcher/Code/Go/src/k8s.io/helm/_scratch/mychart -NAME: geared-marsupi -TARGET NAMESPACE: default -CHART: mychart 0.1.0 -MANIFEST: ---- -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: geared-marsupi-configmap -data: - myvalue: "Hello World" - drink: coffee -``` - -Because `favoriteDrink` is set in the default `values.yaml` file to `coffee`, that's the value displayed in the template. We can easily override that by adding a `--set` flag in our call to `helm install`: - -``` -helm install --dry-run --debug --set favoriteDrink=slurm ./mychart -SERVER: "localhost:44134" -CHART PATH: /Users/mattbutcher/Code/Go/src/k8s.io/helm/_scratch/mychart -NAME: solid-vulture -TARGET NAMESPACE: default -CHART: mychart 0.1.0 -MANIFEST: ---- -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: solid-vulture-configmap -data: - myvalue: "Hello World" - drink: slurm -``` - -Since `--set` has a higher precedence than the default `values.yaml` file, our template generates `drink: slurm`. - -Values files can contain more structured content, too. For example, we could create a `favorite` section in our `values.yaml` file, and then add several keys there: - -```yaml -favorite: - drink: coffee - food: pizza -``` - -Now we would have to modify the template slightly: - -``` -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - drink: {{ .Values.favorite.drink }} - food: {{ .Values.favorite.food }} -``` - -While structuring data this way is possible, the recommendation is that you keep your values trees shallow, favoring flatness. When we look at assigning values to subcharts, we'll see how values are named using a tree structure. - -## Deleting a default key - -If you need to delete a key from the default values, you may override the value of the key to be `null`, in which case Helm will remove the key from the overridden values merge. - -For example, the stable Drupal chart allows configuring the liveness probe, in case you configure a custom image. Here are the default values: -```yaml -livenessProbe: - httpGet: - path: /user/login - port: http - initialDelaySeconds: 120 -``` - -If you try to override the livenessProbe handler to `exec` instead of `httpGet` using `--set livenessProbe.exec.command=[cat,docroot/CHANGELOG.txt]`, Helm will coalesce the default and overridden keys together, resulting in the following YAML: -```yaml -livenessProbe: - httpGet: - path: /user/login - port: http - exec: - command: - - cat - - docroot/CHANGELOG.txt - initialDelaySeconds: 120 -``` - -However, Kubernetes would then fail because you can not declare more than one livenessProbe handler. To overcome this, you may instruct Helm to delete the `livenessProbe.httpGet` by setting it to null: -```sh -helm install stable/drupal --set image=my-registry/drupal:0.1.0 --set livenessProbe.exec.command=[cat,docroot/CHANGELOG.txt] --set livenessProbe.httpGet=null -``` - -At this point, we've seen several built-in objects, and used them to inject information into a template. Now we will take a look at another aspect of the template engine: functions and pipelines. diff --git a/docs/chart_template_guide/variables.md b/docs/chart_template_guide/variables.md deleted file mode 100644 index b55e6e42295..00000000000 --- a/docs/chart_template_guide/variables.md +++ /dev/null @@ -1,129 +0,0 @@ -# Variables - -With functions, pipelines, objects, and control structures under our belts, we can turn to one of the more basic ideas in many programming languages: variables. In templates, they are less frequently used. But we will see how to use them to simplify code, and to make better use of `with` and `range`. - -In an earlier example, we saw that this code will fail: - -```yaml - {{- with .Values.favorite }} - drink: {{ .drink | default "tea" | quote }} - food: {{ .food | upper | quote }} - release: {{ .Release.Name }} - {{- end }} -``` - -`Release.Name` is not inside of the scope that's restricted in the `with` block. One way to work around scoping issues is to assign objects to variables that can be accessed without respect to the present scope. - -In Helm templates, a variable is a named reference to another object. It follows the form `$name`. Variables are assigned with a special assignment operator: `:=`. We can rewrite the above to use a variable for `Release.Name`. - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - {{- $relname := .Release.Name -}} - {{- with .Values.favorite }} - drink: {{ .drink | default "tea" | quote }} - food: {{ .food | upper | quote }} - release: {{ $relname }} - {{- end }} -``` - -Notice that before we start the `with` block, we assign `$relname := .Release.Name`. Now inside of the `with` block, the `$relname` variable still points to the release name. - -Running that will produce this: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: viable-badger-configmap -data: - myvalue: "Hello World" - drink: "coffee" - food: "PIZZA" - release: viable-badger -``` - -Variables are particularly useful in `range` loops. They can be used on list-like objects to capture both the index and the value: - -```yaml - toppings: |- - {{- range $index, $topping := .Values.pizzaToppings }} - {{ $index }}: {{ $topping }} - {{- end }} - -``` - -Note that `range` comes first, then the variables, then the assignment operator, then the list. This will assign the integer index (starting from zero) to `$index` and the value to `$topping`. Running it will produce: - -```yaml - toppings: |- - 0: mushrooms - 1: cheese - 2: peppers - 3: onions -``` - -For data structures that have both a key and a value, we can use `range` to get both. For example, we can loop through `.Values.favorite` like this: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ .Release.Name }}-configmap -data: - myvalue: "Hello World" - {{- range $key, $val := .Values.favorite }} - {{ $key }}: {{ $val | quote }} - {{- end}} -``` - -Now on the first iteration, `$key` will be `drink` and `$val` will be `coffee`, and on the second, `$key` will be `food` and `$val` will be `pizza`. Running the above will generate this: - -```yaml -# Source: mychart/templates/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: eager-rabbit-configmap -data: - myvalue: "Hello World" - drink: "coffee" - food: "pizza" -``` - -Variables are normally not "global". They are scoped to the block in which they are declared. Earlier, we assigned `$relname` in the top level of the template. That variable will be in scope for the entire template. But in our last example, `$key` and `$val` will only be in scope inside of the `{{range...}}{{end}}` block. - -However, there is one variable that is always global - `$` - this -variable will always point to the root context. This can be very -useful when you are looping in a range need to know the chart's release -name. - -An example illustrating this: -```yaml -{{- range .Values.tlsSecrets }} -apiVersion: v1 -kind: Secret -metadata: - name: {{ .name }} - labels: - # Many helm templates would use `.` below, but that will not work, - # however `$` will work here - app: {{ template "fullname" $ }} - # I cannot reference .Chart.Name, but I can do $.Chart.Name - chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}" - release: "{{ $.Release.Name }}" - heritage: "{{ $.Release.Service }}" -type: kubernetes.io/tls -data: - tls.crt: {{ .certificate }} - tls.key: {{ .key }} ---- -{{- end }} -``` - -So far we have looked at just one template declared in just one file. But one of the powerful features of the Helm template language is its ability to declare multiple templates and use them together. We'll turn to that in the next section. diff --git a/docs/chart_template_guide/wrapping_up.md b/docs/chart_template_guide/wrapping_up.md deleted file mode 100755 index 1ed7c602a5c..00000000000 --- a/docs/chart_template_guide/wrapping_up.md +++ /dev/null @@ -1,20 +0,0 @@ -# Wrapping Up - -This guide is intended to give you, the chart developer, a strong understanding of how to use Helm's template language. The guide focuses on the technical aspects of template development. - -But there are many things this guide has not covered when it comes to the practical day-to-day development of charts. Here are some useful pointers to other documentation that will help you as you create new charts: - -- The [Kubernetes Charts project](https://github.com/kubernetes/charts) is an indispensable source of charts. That project is also sets the standard for best practices in chart development. -- The Kubernetes [User's Guide](http://kubernetes.io/docs/user-guide/) provides detailed examples of the various resource kinds that you can use, from ConfigMaps and Secrets to DaemonSets and Deployments. -- The Helm [Charts Guide](../charts.md) explains the workflow of using charts. -- The Helm [Chart Hooks Guide](../charts_hooks.md) explains how to create lifecycle hooks. -- The Helm [Charts Tips and Tricks](../charts_tips_and_tricks.md) article provides some useful tips for writing charts. -- The [Sprig documentation](https://github.com/Masterminds/sprig) documents more than sixty of the template functions. -- The [Go template docs](https://godoc.org/text/template) explain the template syntax in detail. -- The [Schelm tool](https://github.com/databus23/schelm) is a nice helper utility for debugging charts. - -Sometimes it's easier to ask a few questions and get answers from experienced developers. The best place to do that is in the Kubernetes `#Helm` Slack channel: - -- [Kubernetes Slack](https://slack.k8s.io/): `#helm` - -Finally, if you find errors or omissions in this document, want to suggest some new content, or would like to contribute, visit [The Helm Project](https://github.com/kubernetes/helm). diff --git a/docs/chart_template_guide/yaml_techniques.md b/docs/chart_template_guide/yaml_techniques.md deleted file mode 100644 index 44c41f903a3..00000000000 --- a/docs/chart_template_guide/yaml_techniques.md +++ /dev/null @@ -1,349 +0,0 @@ -# YAML Techniques - -Most of this guide has been focused on writing the template language. Here, -we'll look at the YAML format. YAML has some useful features that we, as -template authors, can use to make our templates less error prone and easier -to read. - -## Scalars and Collections - -According to the [YAML spec](http://yaml.org/spec/1.2/spec.html), there are two -types of collections, and many scalar types. - -The two types of collections are maps and sequences: - -```yaml -map: - one: 1 - two: 2 - three: 3 - -sequence: - - one - - two - - three -``` - -Scalar values are individual values (as opposed to collections) - -### Scalar Types in YAML - -In Helm's dialect of YAML, the scalar data type of a value is determined by a -complex set of rules, including the Kubernetes schema for resource definitions. -But when inferring types, the following rules tend to hold true. - -If an integer or float is an unquoted bare word, it is typically treated as -a numeric type: - -```yaml -count: 1 -size: 2.34 -``` - -But if they are quoted, they are treated as strings: - -```yaml -count: "1" # <-- string, not int -size: '2.34' # <-- string, not float -``` - -The same is true of booleans: - -```yaml -isGood: true # bool -answer: "true" # string -``` - -The word for an empty value is `null` (not `nil`). - -Note that `port: "80"` is valid YAML, and will pass through both the -template engine and the YAML parser, but will fail if Kubernetes expects -`port` to be an integer. - -In some cases, you can force a particular type inference using YAML node tags: - -```yaml -coffee: "yes, please" -age: !!str 21 -port: !!int "80" -``` - -In the above, `!!str` tells the parser that `age` is a string, even if it looks -like an int. And `port` is treated as an int, even though it is quoted. - - -## Strings in YAML - -Much of the data that we place in YAML documents are strings. YAML has more than -one way to represent a string. This section explains the ways and demonstrates -how to use some of them. - -There are three "inline" ways of declaring a string: - -```yaml -way1: bare words -way2: "double-quoted strings" -way3: 'single-quoted strings' -``` - -All inline styles must be on one line. - -- Bare words are unquoted, and are not escaped. For this reason, you have to - be careful what characters you use. -- Double-quoted strings can have specific characters escaped with `\`. For - example `"\"Hello\", she said"`. You can escape line breaks with `\n`. -- Single-quoted strings are "literal" strings, and do not use the `\` to - escape characters. The only escape sequence is `''`, which is decoded as - a single `'`. - -In addition to the one-line strings, you can declare multi-line strings: - -```yaml -coffee: | - Latte - Cappuccino - Espresso -``` - -The above will treat the value of `coffee` as a single string equivalent to -`Latte\nCappuccino\nEspresso\n`. - -Note that the first line after the `|` must be correctly indented. So we could -break the example above by doing this: - -```yaml -coffee: | - Latte - Cappuccino - Espresso - -``` - -Because `Latte` is incorrectly indented, we'd get an error like this: - -``` -Error parsing file: error converting YAML to JSON: yaml: line 7: did not find expected key -``` - -In templates, it is sometimes safer to put a fake "first line" of content in a -multi-line document just for protection from the above error: - -```yaml -coffee: | - # Commented first line - Latte - Cappuccino - Espresso - -``` - -Note that whatever that first line is, it will be preserved in the output of the -string. So if you are, for example, using this technique to inject a file's contents -into a ConfigMap, the comment should be of the type expected by whatever is -reading that entry. - -### Controlling Spaces in Multi-line Strings - -In the example above, we used `|` to indicate a multi-line string. But notice -that the content of our string was followed with a trailing `\n`. If we want -the YAML processor to strip off the trailing newline, we can add a `-` after the -`|`: - -```yaml -coffee: |- - Latte - Cappuccino - Espresso -``` - -Now the `coffee` value will be: `Latte\nCappuccino\nEspresso` (with no trailing -`\n`). - -Other times, we might want all trailing whitespace to be preserved. We can do -this with the `|+` notation: - -```yaml -coffee: |+ - Latte - Cappuccino - Espresso - - -another: value -``` - -Now the value of `coffee` will be `Latte\nCappuccino\nEspresso\n\n\n`. - -Indentation inside of a text block is preserved, and results in the preservation -of line breaks, too: - -``` -coffee: |- - Latte - 12 oz - 16 oz - Cappuccino - Espresso -``` - -In the above case, `coffee` will be `Latte\n 12 oz\n 16 oz\nCappuccino\nEspresso`. - -### Indenting and Templates - -When writing templates, you may find yourself wanting to inject the contents of -a file into the template. As we saw in previous chapters, there are two ways -of doing this: - -- Use `{{ .Files.Get "FILENAME" }}` to get the contents of a file in the chart. -- Use `{{ include "TEMPLATE" . }}` to render a template and then place its - contents into the chart. - -When inserting files into YAML, it's good to understand the multi-line rules above. -Often times, the easiest way to insert a static file is to do something like -this: - -```yaml -myfile: | -{{ .Files.Get "myfile.txt" | indent 2 }} -``` - -Note how we do the indentation above: `indent 2` tells the template engine to -indent every line in "myfile.txt" with two spaces. Note that we do not indent -that template line. That's because if we did, the file content of the first line -would be indented twice. - -### Folded Multi-line Strings - -Sometimes you want to represent a string in your YAML with multiple lines, but -want it to be treated as one long line when it is interpreted. This is called -"folding". To declare a folded block, use `>` instead of `|`: - -```yaml -coffee: > - Latte - Cappuccino - Espresso - - -``` - -The value of `coffee` above will be `Latte Cappuccino Espresso\n`. Note that all -but the last line feed will be converted to spaces. You can combine the whitespace -controls with the folded text marker, so `>-` will replace or trim all newlines. - -Note that in the folded syntax, indenting text will cause lines to be preserved. - -```yaml -coffee: >- - Latte - 12 oz - 16 oz - Cappuccino - Espresso -``` - -The above will produce `Latte\n 12 oz\n 16 oz\nCappuccino Espresso`. Note that -both the spacing and the newlines are still there. - -## Embedding Multiple Documents in One File - -It is possible to place more than one YAML documents into a single file. This -is done by prefixing a new document with `---` and ending the document with -`...` - -```yaml - ---- -document:1 -... ---- -document: 2 -... -``` - -In many cases, either the `---` or the `...` may be omitted. - -Some files in Helm cannot contain more than one doc. If, for example, more -than one document is provided inside of a `values.yaml` file, only the first -will be used. - -Template files, however, may have more than one document. When this happens, -the file (and all of its documents) is treated as one object during -template rendering. But then the resulting YAML is split into multiple -documents before it is fed to Kubernetes. - -We recommend only using multiple documents per file when it is absolutely -necessary. Having multiple documents in a file can be difficult to debug. - -## YAML is a Superset of JSON - -Because YAML is a superset of JSON, any valid JSON document _should_ be valid -YAML. - -```json -{ - "coffee": "yes, please", - "coffees": [ - "Latte", "Cappuccino", "Espresso" - ] -} -``` - -The above is another way of representing this: - -```yaml -coffee: yes, please -coffees: -- Latte -- Cappuccino -- Espresso -``` - -And the two can be mixed (with care): - -```yaml -coffee: "yes, please" -coffees: [ "Latte", "Cappuccino", "Espresso"] -``` - -All three of these should parse into the same internal representation. - -While this means that files such as `values.yaml` may contain JSON data, Helm -does not treat the file extension `.json` as a valid suffix. - -## YAML Anchors - -The YAML spec provides a way to store a reference to a value, and later -refer to that value by reference. YAML refers to this as "anchoring": - -```yaml -coffee: "yes, please" -favorite: &favoriteCoffee "Cappucino" -coffees: - - Latte - - *favoriteCoffee - - Espresso -``` - -In the above, `&favoriteCoffee` sets a reference to `Cappuccino`. Later, that -reference is used as `*favoriteCoffee`. So `coffees` becomes -`Latte, Cappuccino, Espresso`. - -While there are a few cases where anchors are useful, there is one aspect of -them that can cause subtle bugs: The first time the YAML is consumed, the -reference is expanded and then discarded. - -So if we were to decode and then re-encode the example above, the resulting -YAML would be: - -```YAML -coffee: yes, please -favorite: Cappucino -coffees: -- Latte -- Cappucino -- Espresso -``` - -Because Helm and Kubernetes often read, modify, and then rewrite YAML files, -the anchors will be lost. diff --git a/docs/chart_tests.md b/docs/chart_tests.md deleted file mode 100644 index d1cfe501713..00000000000 --- a/docs/chart_tests.md +++ /dev/null @@ -1,83 +0,0 @@ -# Chart Tests - -A chart contains a number of Kubernetes resources and components that work together. As a chart author, you may want to write some tests that validate that your chart works as expected when it is installed. These tests also help the chart consumer understand what your chart is supposed to do. - -A **test** in a helm chart lives under the `templates/` directory and is a pod definition that specifies a container with a given command to run. The container should exit successfully (exit 0) for a test to be considered a success. The pod definition must contain one of the helm test hook annotations: `helm.sh/hook: test-success` or `helm.sh/hook: test-failure`. - -Example tests: -- Validate that your configuration from the values.yaml file was properly injected. - - Make sure your username and password work correctly - - Make sure an incorrect username and password does not work -- Assert that your services are up and correctly load balancing -- etc. - -You can run the pre-defined tests in Helm on a release using the command `helm test `. For a chart consumer, this is a great way to sanity check that their release of a chart (or application) works as expected. - -## A Breakdown of the Helm Test Hooks - -In Helm, there are two test hooks: `test-success` and `test-failure` - -`test-success` indicates that test pod should complete successfully. In other words, the containers in the pod should exit 0. -`test-failure` is a way to assert that a test pod should not complete successfully. If the containers in the pod do not exit 0, that indicates success. - -## Example Test - -Here is an example of a helm test pod definition in an example mariadb chart: - -``` -mariadb/ - Chart.yaml - README.md - values.yaml - charts/ - templates/ - templates/tests/test-mariadb-connection.yaml -``` -In `wordpress/templates/tests/test-mariadb-connection.yaml`: -``` -apiVersion: v1 -kind: Pod -metadata: - name: "{{ .Release.Name }}-credentials-test" - annotations: - "helm.sh/hook": test-success -spec: - containers: - - name: {{ .Release.Name }}-credentials-test - image: {{ .Values.image }} - env: - - name: MARIADB_HOST - value: {{ template "mariadb.fullname" . }} - - name: MARIADB_PORT - value: "3306" - - name: WORDPRESS_DATABASE_NAME - value: {{ default "" .Values.mariadb.mariadbDatabase | quote }} - - name: WORDPRESS_DATABASE_USER - value: {{ default "" .Values.mariadb.mariadbUser | quote }} - - name: WORDPRESS_DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: {{ template "mariadb.fullname" . }} - key: mariadb-password - command: ["sh", "-c", "mysql --host=$MARIADB_HOST --port=$MARIADB_PORT --user=$WORDPRESS_DATABASE_USER --password=$WORDPRESS_DATABASE_PASSWORD"] - restartPolicy: Never -``` - -## Steps to Run a Test Suite on a Release -1. `$ helm install mariadb` -``` -NAME: quirky-walrus -LAST DEPLOYED: Mon Feb 13 13:50:43 2017 -NAMESPACE: default -STATUS: DEPLOYED -``` - -2. `$ helm test quirky-walrus` -``` -RUNNING: quirky-walrus-credentials-test -SUCCESS: quirky-walrus-credentials-test -``` - -## Notes -- You can define as many tests as you would like in a single yaml file or spread across several yaml files in the `templates/` directory -- You are welcome to nest your test suite under a `tests/` directory like `/templates/tests/` for more isolation diff --git a/docs/charts.md b/docs/charts.md deleted file mode 100644 index a19c1a477a4..00000000000 --- a/docs/charts.md +++ /dev/null @@ -1,856 +0,0 @@ -# Charts - -Helm uses a packaging format called _charts_. A chart is a collection of files -that describe a related set of Kubernetes resources. A single chart -might be used to deploy something simple, like a memcached pod, or -something complex, like a full web app stack with HTTP servers, -databases, caches, and so on. - -Charts are created as files laid out in a particular directory tree, -then they can be packaged into versioned archives to be deployed. - -This document explains the chart format, and provides basic guidance for -building charts with Helm. - -## The Chart File Structure - -A chart is organized as a collection of files inside of a directory. The -directory name is the name of the chart (without versioning information). Thus, -a chart describing WordPress would be stored in the `wordpress/` directory. - -Inside of this directory, Helm will expect a structure that matches this: - -``` -wordpress/ - Chart.yaml # A YAML file containing information about the chart - LICENSE # OPTIONAL: A plain text file containing the license for the chart - README.md # OPTIONAL: A human-readable README file - requirements.yaml # OPTIONAL: A YAML file listing dependencies for the chart - values.yaml # The default configuration values for this chart - charts/ # A directory containing any charts upon which this chart depends. - templates/ # A directory of templates that, when combined with values, - # will generate valid Kubernetes manifest files. - templates/NOTES.txt # OPTIONAL: A plain text file containing short usage notes -``` - -Helm reserves use of the `charts/` and `templates/` directories, and of -the listed file names. Other files will be left as they are. - -## The Chart.yaml File - -The `Chart.yaml` file is required for a chart. It contains the following fields: - -```yaml -name: The name of the chart (required) -version: A SemVer 2 version (required) -kubeVersion: A SemVer range of compatible Kubernetes versions (optional) -description: A single-sentence description of this project (optional) -keywords: - - A list of keywords about this project (optional) -home: The URL of this project's home page (optional) -sources: - - A list of URLs to source code for this project (optional) -maintainers: # (optional) - - name: The maintainer's name (required for each maintainer) - email: The maintainer's email (optional for each maintainer) - url: A URL for the maintainer (optional for each maintainer) -engine: gotpl # The name of the template engine (optional, defaults to gotpl) -icon: A URL to an SVG or PNG image to be used as an icon (optional). -appVersion: The version of the app that this contains (optional). This needn't be SemVer. -deprecated: Whether this chart is deprecated (optional, boolean) -tillerVersion: The version of Tiller that this chart requires. This should be expressed as a SemVer range: ">2.0.0" (optional) -``` - -If you are familiar with the `Chart.yaml` file format for Helm Classic, you will -notice that fields specifying dependencies have been removed. That is because -the new Chart format expresses dependencies using the `charts/` directory. - -Other fields will be silently ignored. - -### Charts and Versioning - -Every chart must have a version number. A version must follow the -[SemVer 2](http://semver.org/) standard. Unlike Helm Classic, Kubernetes -Helm uses version numbers as release markers. Packages in repositories -are identified by name plus version. - -For example, an `nginx` chart whose version field is set to `version: -1.2.3` will be named: - -``` -nginx-1.2.3.tgz -``` - -More complex SemVer 2 names are also supported, such as -`version: 1.2.3-alpha.1+ef365`. But non-SemVer names are explicitly -disallowed by the system. - -**NOTE:** Whereas Helm Classic and Deployment Manager were both -very GitHub oriented when it came to charts, Kubernetes Helm does not -rely upon or require GitHub or even Git. Consequently, it does not use -Git SHAs for versioning at all. - -The `version` field inside of the `Chart.yaml` is used by many of the -Helm tools, including the CLI and the Tiller server. When generating a -package, the `helm package` command will use the version that it finds -in the `Chart.yaml` as a token in the package name. The system assumes -that the version number in the chart package name matches the version number in -the `Chart.yaml`. Failure to meet this assumption will cause an error. - -### The appVersion field - -Note that the `appVersion` field is not related to the `version` field. It is -a way of specifying the version of the application. For example, the `drupal` -chart may have an `appVersion: 8.2.1`, indicating that the version of Drupal -included in the chart (by default) is `8.2.1`. This field is informational, and -has no impact on chart version calculations. - -### Deprecating Charts - -When managing charts in a Chart Repository, it is sometimes necessary to -deprecate a chart. The optional `deprecated` field in `Chart.yaml` can be used -to mark a chart as deprecated. If the **latest** version of a chart in the -repository is marked as deprecated, then the chart as a whole is considered to -be deprecated. The chart name can later be reused by publishing a newer version -that is not marked as deprecated. The workflow for deprecating charts, as -followed by the [kubernetes/charts](https://github.com/kubernetes/charts) -project is: - - Update chart's `Chart.yaml` to mark the chart as deprecated, bumping the - version - - Release the new chart version in the Chart Repository - - Remove the chart from the source repository (e.g. git) - -## Chart LICENSE, README and NOTES - -Charts can also contain files that describe the installation, configuration, usage and license of a -chart. A README for a chart should be formatted in Markdown (README.md), and should generally -contain: - -- A description of the application or service the chart provides -- Any prerequisites or requirements to run the chart -- Descriptions of options in `values.yaml` and default values -- Any other information that may be relevant to the installation or configuration of the chart - -The chart can also contain a short plain text `templates/NOTES.txt` file that will be printed out -after installation, and when viewing the status of a release. This file is evaluated as a -[template](#templates-and-values), and can be used to display usage notes, next steps, or any other -information relevant to a release of the chart. For example, instructions could be provided for -connecting to a database, or accessing a web UI. Since this file is printed to STDOUT when running -`helm install` or `helm status`, it is recommended to keep the content brief and point to the README -for greater detail. - -## Chart Dependencies - -In Helm, one chart may depend on any number of other charts. -These dependencies can be dynamically linked through the `requirements.yaml` -file or brought in to the `charts/` directory and managed manually. - -Although manually managing your dependencies has a few advantages some teams need, -the preferred method of declaring dependencies is by using a -`requirements.yaml` file inside of your chart. - -**Note:** The `dependencies:` section of the `Chart.yaml` from Helm -Classic has been completely removed. - - -### Managing Dependencies with `requirements.yaml` - -A `requirements.yaml` file is a simple file for listing your -dependencies. - -```yaml -dependencies: - - name: apache - version: 1.2.3 - repository: http://example.com/charts - - name: mysql - version: 3.2.1 - repository: http://another.example.com/charts -``` - -- The `name` field is the name of the chart you want. -- The `version` field is the version of the chart you want. -- The `repository` field is the full URL to the chart repository. Note - that you must also use `helm repo add` to add that repo locally. - -Once you have a dependencies file, you can run `helm dependency update` -and it will use your dependency file to download all the specified -charts into your `charts/` directory for you. - -```console -$ helm dep up foochart -Hang tight while we grab the latest from your chart repositories... -...Successfully got an update from the "local" chart repository -...Successfully got an update from the "stable" chart repository -...Successfully got an update from the "example" chart repository -...Successfully got an update from the "another" chart repository -Update Complete. Happy Helming! -Saving 2 charts -Downloading apache from repo http://example.com/charts -Downloading mysql from repo http://another.example.com/charts -``` - -When `helm dependency update` retrieves charts, it will store them as -chart archives in the `charts/` directory. So for the example above, one -would expect to see the following files in the charts directory: - -``` -charts/ - apache-1.2.3.tgz - mysql-3.2.1.tgz -``` - -Managing charts with `requirements.yaml` is a good way to easily keep -charts updated, and also share requirements information throughout a -team. - -#### Alias field in requirements.yaml - -In addition to the other fields above, each requirements entry may contain -the optional field `alias`. - -Adding an alias for a dependency chart would put -a chart in dependencies using alias as name of new dependency. - -One can use `alias` in cases where they need to access a chart -with other name(s). - -```yaml -# parentchart/requirements.yaml -dependencies: - - name: subchart - repository: http://localhost:10191 - version: 0.1.0 - alias: new-subchart-1 - - name: subchart - repository: http://localhost:10191 - version: 0.1.0 - alias: new-subchart-2 - - name: subchart - repository: http://localhost:10191 - version: 0.1.0 -``` - -In the above example we will get 3 dependencies in all for `parentchart` -``` -subchart -new-subchart-1 -new-subchart-2 -``` - -The manual way of achieving this is by copy/pasting the same chart in the -`charts/` directory multiple times with different names. - -#### Tags and Condition fields in requirements.yaml - -In addition to the other fields above, each requirements entry may contain -the optional fields `tags` and `condition`. - -All charts are loaded by default. If `tags` or `condition` fields are present, -they will be evaluated and used to control loading for the chart(s) they are applied to. - -Condition - The condition field holds one or more YAML paths (delimited by commas). -If this path exists in the top parent's values and resolves to a boolean value, -the chart will be enabled or disabled based on that boolean value. Only the first -valid path found in the list is evaluated and if no paths exist then the condition has no effect. - -Tags - The tags field is a YAML list of labels to associate with this chart. -In the top parent's values, all charts with tags can be enabled or disabled by -specifying the tag and a boolean value. - -```` -# parentchart/requirements.yaml -dependencies: - - name: subchart1 - repository: http://localhost:10191 - version: 0.1.0 - condition: subchart1.enabled, global.subchart1.enabled - tags: - - front-end - - subchart1 - - - name: subchart2 - repository: http://localhost:10191 - version: 0.1.0 - condition: subchart2.enabled,global.subchart2.enabled - tags: - - back-end - - subchart2 - -```` -```` -# parentchart/values.yaml - -subchart1: - enabled: true -tags: - front-end: false - back-end: true -```` - -In the above example all charts with the tag `front-end` would be disabled but since the -`subchart1.enabled` path evaluates to 'true' in the parent's values, the condition will override the -`front-end` tag and `subchart1` will be enabled. - -Since `subchart2` is tagged with `back-end` and that tag evaluates to `true`, `subchart2` will be -enabled. Also notes that although `subchart2` has a condition specified in `requirements.yaml`, there -is no corresponding path and value in the parent's values so that condition has no effect. - -##### Using the CLI with Tags and Conditions - -The `--set` parameter can be used as usual to alter tag and condition values. - -```` -helm install --set tags.front-end=true --set subchart2.enabled=false - -```` - -##### Tags and Condition Resolution - - - * **Conditions (when set in values) always override tags.** The first condition - path that exists wins and subsequent ones for that chart are ignored. - * Tags are evaluated as 'if any of the chart's tags are true then enable the chart'. - * Tags and conditions values must be set in the top parent's values. - * The `tags:` key in values must be a top level key. Globals and nested `tags:` tables - are not currently supported. - -#### Importing Child Values via requirements.yaml - -In some cases it is desirable to allow a child chart's values to propagate to the parent chart and be -shared as common defaults. An additional benefit of using the `exports` format is that it will enable future -tooling to introspect user-settable values. - -The keys containing the values to be imported can be specified in the parent chart's `requirements.yaml` file -using a YAML list. Each item in the list is a key which is imported from the child chart's `exports` field. - -To import values not contained in the `exports` key, use the [child-parent](#using-the-child-parent-format) format. -Examples of both formats are described below. - -##### Using the exports format - -If a child chart's `values.yaml` file contains an `exports` field at the root, its contents may be imported -directly into the parent's values by specifying the keys to import as in the example below: - -```yaml -# parent's requirements.yaml file - ... - import-values: - - data -``` -```yaml -# child's values.yaml file -... -exports: - data: - myint: 99 -``` - -Since we are specifying the key `data` in our import list, Helm looks in the `exports` field of the child -chart for `data` key and imports its contents. - -The final parent values would contain our exported field: - -```yaml -# parent's values file -... -myint: 99 - -``` - -Please note the parent key `data` is not contained in the parent's final values. If you need to specify the -parent key, use the 'child-parent' format. - -##### Using the child-parent format - -To access values that are not contained in the `exports` key of the child chart's values, you will need to -specify the source key of the values to be imported (`child`) and the destination path in the parent chart's -values (`parent`). - -The `import-values` in the example below instructs Helm to take any values found at `child:` path and copy them -to the parent's values at the path specified in `parent:` - -```yaml -# parent's requirements.yaml file -dependencies: - - name: subchart1 - repository: http://localhost:10191 - version: 0.1.0 - ... - import-values: - - child: default.data - parent: myimports -``` -In the above example, values found at `default.data` in the subchart1's values will be imported -to the `myimports` key in the parent chart's values as detailed below: - -```yaml -# parent's values.yaml file - -myimports: - myint: 0 - mybool: false - mystring: "helm rocks!" - -``` -```yaml -# subchart1's values.yaml file - -default: - data: - myint: 999 - mybool: true - -``` -The parent chart's resulting values would be: - -```yaml -# parent's final values - -myimports: - myint: 999 - mybool: true - mystring: "helm rocks!" - -``` - -The parent's final values now contains the `myint` and `mybool` fields imported from subchart1. - -### Managing Dependencies manually via the `charts/` directory - -If more control over dependencies is desired, these dependencies can -be expressed explicitly by copying the dependency charts into the -`charts/` directory. - -A dependency can be either a chart archive (`foo-1.2.3.tgz`) or an -unpacked chart directory. But its name cannot start with `_` or `.`. -Such files are ignored by the chart loader. - -For example, if the WordPress chart depends on the Apache chart, the -Apache chart (of the correct version) is supplied in the WordPress -chart's `charts/` directory: - -``` -wordpress: - Chart.yaml - requirements.yaml - # ... - charts/ - apache/ - Chart.yaml - # ... - mysql/ - Chart.yaml - # ... -``` - -The example above shows how the WordPress chart expresses its dependency -on Apache and MySQL by including those charts inside of its `charts/` -directory. - -**TIP:** _To drop a dependency into your `charts/` directory, use the -`helm fetch` command_ - -### Operational aspects of using dependencies - -The above sections explain how to specify chart dependencies, but how does this affect -chart installation using `helm install` and `helm upgrade`? - -Suppose that a chart named "A" creates the following Kubernetes objects - -- namespace "A-Namespace" -- statefulset "A-StatefulSet" -- service "A-Service" - -Furthermore, A is dependent on chart B that creates objects - -- namespace "B-Namespace" -- replicaset "B-ReplicaSet" -- service "B-Service" - -After installation/upgrade of chart A a single Helm release is created/modified. The release will -create/update all of the above Kubernetes objects in the following order: - -- A-Namespace -- B-Namespace -- A-StatefulSet -- B-ReplicaSet -- A-Service -- B-Service - -This is because when Helm installs/upgrades charts, -the Kubernetes objects from the charts and all its dependencies are - -- aggregrated into a single set; then -- sorted by type followed by name; and then -- created/updated in that order. - -Hence a single release is created with all the objects for the chart and its dependencies. - -The install order of Kubernetes types is given by the enumeration InstallOrder in kind_sorter.go -(see [the Helm source file](https://github.com/kubernetes/helm/blob/master/pkg/tiller/kind_sorter.go#L26)). - -## Templates and Values - -Helm Chart templates are written in the -[Go template language](https://golang.org/pkg/text/template/), with the -addition of 50 or so add-on template -functions [from the Sprig library](https://github.com/Masterminds/sprig) and a -few other [specialized functions](charts_tips_and_tricks.md). - -All template files are stored in a chart's `templates/` folder. When -Helm renders the charts, it will pass every file in that directory -through the template engine. - -Values for the templates are supplied two ways: - - - Chart developers may supply a file called `values.yaml` inside of a - chart. This file can contain default values. - - Chart users may supply a YAML file that contains values. This can be - provided on the command line with `helm install`. - -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 -(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 -apiVersion: v1 -kind: ReplicationController -metadata: - name: deis-database - namespace: deis - labels: - heritage: deis -spec: - replicas: 1 - selector: - app: deis-database - template: - metadata: - labels: - app: deis-database - spec: - serviceAccount: deis-database - containers: - - name: deis-database - image: {{.Values.imageRegistry}}/postgres:{{.Values.dockerTag}} - imagePullPolicy: {{.Values.pullPolicy}} - ports: - - containerPort: 5432 - env: - - name: DATABASE_STORAGE - value: {{default "minio" .Values.storage}} -``` - -The above example, based loosely on [https://github.com/deis/charts](https://github.com/deis/charts), is a template for a Kubernetes replication controller. -It can use the following four template values (usually defined in a -`values.yaml` file): - -- `imageRegistry`: The source registry for the Docker image. -- `dockerTag`: The tag for the docker image. -- `pullPolicy`: The Kubernetes pull policy. -- `storage`: The storage backend, whose default is set to `"minio"` - -All of these values are defined by the template author. Helm does not -require or dictate parameters. - -To see many working charts, check out the [Kubernetes Charts -project](https://github.com/kubernetes/charts) - -### Predefined Values - -Values that are supplied via a `values.yaml` file (or via the `--set` -flag) are accessible from the `.Values` object in a template. But there -are other pre-defined pieces of data you can access in your templates. - -The following values are pre-defined, are available to every template, and -cannot be overridden. As with all values, the names are _case -sensitive_. - -- `Release.Name`: The name of the release (not the chart) -- `Release.Time`: The time the chart release was last updated. This will - match the `Last Released` time on a Release object. -- `Release.Namespace`: The namespace the chart was released to. -- `Release.Service`: The service that conducted the release. Usually - this is `Tiller`. -- `Release.IsUpgrade`: This is set to true if the current operation is an upgrade or rollback. -- `Release.IsInstall`: This is set to true if the current operation is an - install. -- `Release.Revision`: The revision number. It begins at 1, and increments with - each `helm upgrade`. -- `Chart`: The contents of the `Chart.yaml`. Thus, the chart version is - obtainable as `Chart.Version` and the maintainers are in - `Chart.Maintainers`. -- `Files`: A map-like object containing all non-special files in the chart. This - will not give you access to templates, but will give you access to additional - files that are present (unless they are excluded using `.helmignore`). Files can be - accessed using `{{index .Files "file.name"}}` or using the `{{.Files.Get name}}` or - `{{.Files.GetString name}}` functions. You can also access the contents of the file - as `[]byte` using `{{.Files.GetBytes}}` -- `Capabilities`: A map-like object that contains information about the versions - of Kubernetes (`{{.Capabilities.KubeVersion}}`, Tiller - (`{{.Capabilities.TillerVersion}}`, and the supported Kubernetes API versions - (`{{.Capabilities.APIVersions.Has "batch/v1"`) - -**NOTE:** Any unknown Chart.yaml fields will be dropped. They will not -be accessible inside of the `Chart` object. Thus, Chart.yaml cannot be -used to pass arbitrarily structured data into the template. The values -file can be used for that, though. - -### Values files - -Considering the template in the previous section, a `values.yaml` file -that supplies the necessary values would look like this: - -```yaml -imageRegistry: "quay.io/deis" -dockerTag: "latest" -pullPolicy: "Always" -storage: "s3" -``` - -A values file is formatted in YAML. A chart may include a default -`values.yaml` file. The Helm install command allows a user to override -values by supplying additional YAML values: - -```console -$ helm install --values=myvals.yaml wordpress -``` - -When values are passed in this way, they will be merged into the default -values file. For example, consider a `myvals.yaml` file that looks like -this: - -```yaml -storage: "gcs" -``` - -When this is merged with the `values.yaml` in the chart, the resulting -generated content will be: - -```yaml -imageRegistry: "quay.io/deis" -dockerTag: "latest" -pullPolicy: "Always" -storage: "gcs" -``` - -Note that only the last field was overridden. - -**NOTE:** The default values file included inside of a chart _must_ be named -`values.yaml`. But files specified on the command line can be named -anything. - -**NOTE:** If the `--set` flag is used on `helm install` or `helm upgrade`, those -values are simply converted to YAML on the client side. - -**NOTE:** If any required entries in the values file exist, they can be declared -as required in the chart template by using the ['required' function](charts_tips_and_tricks.md) - -Any of these values are then accessible inside of templates using the -`.Values` object: - -```yaml -apiVersion: v1 -kind: ReplicationController -metadata: - name: deis-database - namespace: deis - labels: - heritage: deis -spec: - replicas: 1 - selector: - app: deis-database - template: - metadata: - labels: - app: deis-database - spec: - serviceAccount: deis-database - containers: - - name: deis-database - image: {{.Values.imageRegistry}}/postgres:{{.Values.dockerTag}} - imagePullPolicy: {{.Values.pullPolicy}} - ports: - - containerPort: 5432 - env: - - name: DATABASE_STORAGE - value: {{default "minio" .Values.storage}} - -``` - -### Scope, Dependencies, and Values - -Values files can declare values for the top-level chart, as well as for -any of the charts that are included in that chart's `charts/` directory. -Or, to phrase it differently, a values file can supply values to the -chart as well as to any of its dependencies. For example, the -demonstration WordPress chart above has both `mysql` and `apache` as -dependencies. The values file could supply values to all of these -components: - -```yaml -title: "My WordPress Site" # Sent to the WordPress template - -mysql: - max_connections: 100 # Sent to MySQL - password: "secret" - -apache: - port: 8080 # Passed to Apache -``` - -Charts at a higher level have access to all of the variables defined -beneath. So the WordPress chart can access the MySQL password as -`.Values.mysql.password`. But lower level charts cannot access things in -parent charts, so MySQL will not be able to access the `title` property. Nor, -for that matter, can it access `apache.port`. - -Values are namespaced, but namespaces are pruned. So for the WordPress -chart, it can access the MySQL password field as `.Values.mysql.password`. But -for the MySQL chart, the scope of the values has been reduced and the -namespace prefix removed, so it will see the password field simply as -`.Values.password`. - -#### Global Values - -As of 2.0.0-Alpha.2, Helm supports special "global" value. Consider -this modified version of the previous example: - -```yaml -title: "My WordPress Site" # Sent to the WordPress template - -global: - app: MyWordPress - -mysql: - max_connections: 100 # Sent to MySQL - password: "secret" - -apache: - port: 8080 # Passed to Apache -``` - -The above adds a `global` section with the value `app: MyWordPress`. -This value is available to _all_ charts as `.Values.global.app`. - -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: - -```yaml -title: "My WordPress Site" # Sent to the WordPress template - -global: - app: MyWordPress - -mysql: - global: - app: MyWordPress - max_connections: 100 # Sent to MySQL - password: "secret" - -apache: - global: - app: MyWordPress - port: 8080 # Passed to Apache -``` - -This provides a way of sharing one top-level variable with all -subcharts, which is useful for things like setting `metadata` properties -like labels. - -If a subchart declares a global variable, that global will be passed -_downward_ (to the subchart's subcharts), but not _upward_ to the parent -chart. There is no way for a subchart to influence the values of the -parent chart. - -Also, global variables of parent charts take precedence over the global variables from subcharts. - -### References - -When it comes to writing templates and values files, there are several -standard references that will help you out. - -- [Go templates](https://godoc.org/text/template) -- [Extra template functions](https://godoc.org/github.com/Masterminds/sprig) -- [The YAML format](http://yaml.org/spec/) - -## Using Helm to Manage Charts - -The `helm` tool has several commands for working with charts. - -It can create a new chart for you: - -```console -$ helm create mychart -Created mychart/ -``` - -Once you have edited a chart, `helm` can package it into a chart archive -for you: - -```console -$ helm package mychart -Archived mychart-0.1.-.tgz -``` - -You can also use `helm` to help you find issues with your chart's -formatting or information: - -```console -$ helm lint mychart -No issues found -``` - -## Chart Repositories - -A _chart repository_ is an HTTP server that houses one or more packaged -charts. While `helm` can be used to manage local chart directories, when -it comes to sharing charts, the preferred mechanism is a chart -repository. - -Any HTTP server that can serve YAML files and tar files and can answer -GET requests can be used as a repository server. - -Helm comes with built-in package server for developer testing (`helm -serve`). The Helm team has tested other servers, including Google Cloud -Storage with website mode enabled, and S3 with website mode enabled. - -A repository is characterized primarily by the presence of a special -file called `index.yaml` that has a list of all of the packages supplied -by the repository, together with metadata that allows retrieving and -verifying those packages. - -On the client side, repositories are managed with the `helm repo` -commands. However, Helm does not provide tools for uploading charts to -remote repository servers. This is because doing so would add -substantial requirements to an implementing server, and thus raise the -barrier for setting up a repository. - -## Chart Starter Packs - -The `helm create` command takes an optional `--starter` option that lets you -specify a "starter chart". - -Starters are just regular charts, but are located in `$HELM_HOME/starters`. -As a chart developer, you may author charts that are specifically designed -to be used as starters. Such charts should be designed with the following -considerations in mind: - -- The `Chart.yaml` will be overwritten by the generator. -- Users will expect to modify such a chart's contents, so documentation - should indicate how users can do so. -- All occurrences of `` will be replaced with the specified chart - name so that starter charts can be used as templates. - -Currently the only way to add a chart to `$HELM_HOME/starters` is to manually -copy it there. In your chart's documentation, you may want to explain that -process. diff --git a/docs/charts_hooks.md b/docs/charts_hooks.md deleted file mode 100644 index 4142f2ce0db..00000000000 --- a/docs/charts_hooks.md +++ /dev/null @@ -1,198 +0,0 @@ -# Hooks - -Helm provides a _hook_ mechanism to allow chart developers to intervene -at certain points in a release's life cycle. For example, you can use -hooks to: - -- Load a ConfigMap or Secret during install before any other charts are - loaded. -- Execute a Job to back up a database before installing a new chart, - and then execute a second job after the upgrade in order to restore - data. -- Run a Job before deleting a release to gracefully take a service out - of rotation before removing it. - -Hooks work like regular templates, but they have special annotations -that cause Helm to utilize them differently. In this section, we cover -the basic usage pattern for hooks. - -## The Available Hooks - -The following hooks are defined: - -- pre-install: Executes after templates are rendered, but before any - resources are created in Kubernetes. -- post-install: Executes after all resources are loaded into Kubernetes -- pre-delete: Executes on a deletion request before any resources are - deleted from Kubernetes. -- post-delete: Executes on a deletion request after all of the release's - resources have been deleted. -- pre-upgrade: Executes on an upgrade request after templates are - rendered, but before any resources are loaded into Kubernetes (e.g. - before a Kubernetes apply operation). -- post-upgrade: Executes on an upgrade after all resources have been - upgraded. -- pre-rollback: Executes on a rollback request after templates are - rendered, but before any resources have been rolled back. -- post-rollback: Executes on a rollback request after all resources - have been modified. - -## Hooks and the Release Lifecycle - -Hooks allow you, the chart developer, an opportunity to perform -operations at strategic points in a release lifecycle. For example, -consider the lifecycle for a `helm install`. By default, the lifecycle -looks like this: - -1. User runs `helm install foo` -2. Chart is loaded into Tiller -3. After some verification, Tiller renders the `foo` templates -4. Tiller loads the resulting resources into Kubernetes -5. Tiller returns the release name (and other data) to the client -6. The client exits - -Helm defines two hooks for the `install` lifecycle: `pre-install` and -`post-install`. If the developer of the `foo` chart implements both -hooks, the lifecycle is altered like this: - -1. User runs `helm install foo` -2. Chart is loaded into Tiller -3. After some verification, Tiller renders the `foo` templates -4. Tiller prepares to execute the `pre-install` hooks (loading hook resources into - Kubernetes) -5. Tiller sorts hooks by weight (assigning a weight of 0 by default) and by name for those hooks with the same weight in ascending order. -6. Tiller then loads the hook with the lowest weight first (negative to positive) -7. Tiller waits until the hook is "Ready" -8. Tiller loads the resulting resources into Kubernetes. Note that if the `--wait` -flag is set, Tiller will wait until all resources are in a ready state -and will not run the `post-install` hook until they are ready. -9. Tiller executes the `post-install` hook (loading hook resources) -10. Tiller waits until the hook is "Ready" -11. Tiller returns the release name (and other data) to the client -12. The client exits - -What does it mean to wait until a hook is ready? This depends on the -resource declared in the hook. If the resources is a `Job` kind, Tiller -will wait until the job successfully runs to completion. And if the job -fails, the release will fail. This is a _blocking operation_, so the -Helm client will pause while the Job is run. - -For all other kinds, as soon as Kubernetes marks the resource as loaded -(added or updated), the resource is considered "Ready". When many -resources are declared in a hook, the resources are executed serially. If they -have hook weights (see below), they are executed in weighted order. Otherwise, -ordering is not guaranteed. (In Helm 2.3.0 and after, they are sorted -alphabetically. That behavior, though, is not considered binding and could change -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 not managed with corresponding 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 -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 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 - -Hooks are just Kubernetes manifest files with special annotations in the -`metadata` section. Because they are template files, you can use all of -the normal template features, including reading `.Values`, `.Release`, -and `.Template`. - -For example, this template, stored in `templates/post-install-job.yaml`, -declares a job to be run on `post-install`: - -```yaml -apiVersion: batch/v1 -kind: Job -metadata: - name: "{{.Release.Name}}" - 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 - "helm.sh/hook-weight": "-5" - "helm.sh/hook-delete-policy": hook-succeeded -spec: - template: - metadata: - name: "{{.Release.Name}}" - labels: - heritage: {{.Release.Service | quote }} - release: {{.Release.Name | quote }} - chart: "{{.Chart.Name}}-{{.Chart.Version}}" - spec: - restartPolicy: Never - containers: - - name: post-install-job - image: "alpine:3.3" - command: ["/bin/sleep","{{default "10" .Values.sleepyTime}}"] - -``` - -What makes this template a hook is the annotation: - -``` - annotations: - "helm.sh/hook": post-install -``` - -One resource can implement multiple hooks: - -``` - annotations: - "helm.sh/hook": post-install,post-upgrade -``` - -Similarly, there is no limit to the number of different resources that -may implement a given hook. For example, one could declare both a secret -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 possible to define a weight for a hook which will help build a -deterministic executing order. Weights are defined using the following annotation: - -``` - annotations: - "helm.sh/hook-weight": "5" -``` - -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 -``` - -You can choose one or more defined annotation values: -* `"hook-succeeded"` specifies Tiller should delete the hook after the hook is successfully executed. -* `"hook-failed"` specifies Tiller should delete the hook if the hook failed during execution. -* `"before-hook-creation"` specifies Tiller should delete the previous hook before the new hook is launched. - -### Automatically delete hook from previous release - -When helm release being updated it is possible, that hook resource already exists in cluster. By default helm will try to create resource and fail with `"... already exists"` error. - -One might choose `"helm.sh/hook-delete-policy": "before-hook-creation"` over `"helm.sh/hook-delete-policy": "hook-succeeded,hook-failed"` because: - -* It is convenient to keep failed hook job resource in kubernetes for example for manual debug. -* It may be necessary to keep succeeded hook resource in kubernetes for some reason. -* At the same time it is not desirable to do manual resource deletion before helm release upgrade. - -`"helm.sh/hook-delete-policy": "before-hook-creation"` annotation on hook causes tiller to remove the hook from previous release if there is one before the new hook is launched and can be used with another policy. diff --git a/docs/charts_tips_and_tricks.md b/docs/charts_tips_and_tricks.md deleted file mode 100644 index 484d8b9369e..00000000000 --- a/docs/charts_tips_and_tricks.md +++ /dev/null @@ -1,250 +0,0 @@ -# Chart Development Tips and Tricks - -This guide covers some of the tips and tricks Helm chart developers have -learned while building production-quality charts. - -## Know Your Template Functions - -Helm uses [Go templates](https://godoc.org/text/template) for templating -your resource files. While Go ships several built-in functions, we have -added many others. - -First, we added almost all of the functions in the -[Sprig library](https://godoc.org/github.com/Masterminds/sprig). We removed two -for security reasons: `env` and `expandenv` (which would have given chart authors -access to Tiller's environment). - -We also added two special template functions: `include` and `required`. The `include` -function allows you to bring in another template, and then pass the results to other -template functions. - -For example, this template snippet includes a template called `mytpl`, then -lowercases the result, then wraps that in double quotes. - -```yaml -value: {{include "mytpl" . | lower | quote}} -``` - -The `required` function allows you to declare a particular -values entry as required for template rendering. If the value is empty, the template -rendering will fail with a user submitted error message. - -The following example of the `required` function declares an entry for .Values.who -is required, and will print an error message when that entry is missing: - -```yaml -value: {{required "A valid .Values.who entry required!" .Values.who }} -``` - -## Quote Strings, Don't Quote Integers - -When you are working with string data, you are always safer quoting the -strings than leaving them as bare words: - -``` -name: {{.Values.MyName | quote }} -``` - -But when working with integers _do not quote the values._ That can, in -many cases, cause parsing errors inside of Kubernetes. - -``` -port: {{ .Values.Port }} -``` - -This remark does not apply to env variables values which are expected to be string, even if they represent integers: - -``` -env: - -name: HOST - value: "http://host" - -name: PORT - value: "1234" -``` - -## Using the 'include' Function - -Go provides a way of including one template in another using a built-in -`template` directive. However, the built-in function cannot be used in -Go template pipelines. - -To make it possible to include a template, and then perform an operation -on that template's output, Helm has a special `include` function: - -``` -{{ include "toYaml" $value | indent 2 }} -``` - -The above includes a template called `toYaml`, passes it `$value`, and -then passes the output of that template to the `indent` function. - -Because YAML ascribes significance to indentation levels and whitespace, -this is one great way to include snippets of code, but handle -indentation in a relevant context. - -## Using the 'required' function - -Go provides a way for setting template options to control behavior -when a map is indexed with a key that's not present in the map. This -is typically set with template.Options("missingkey=option"), where option -can be default, zero, or error. While setting this option to error will -stop execution with an error, this would apply to every missing key in the -map. There may be situations where a chart developer wants to enforce this -behavior for select values in the values.yml file. - -The `required` function gives developers the ability to declare a value entry -as required for template rendering. If the entry is empty in values.yml, the -template will not render and will return an error message supplied by the -developer. - -For example: - -``` -{{ required "A valid foo is required!" .Values.foo }} -``` - -The above will render the template when .Values.foo is defined, but will fail -to render and exit when .Values.foo is undefined. - -## Creating Image Pull Secrets -Image pull secrets are essentially a combination of _registry_, _username_, and _password_. You may need them in an application you are deploying, but to create them requires running _base64_ a couple of times. We can write a helper template to compose the Docker configuration file for use as the Secret's payload. Here is an example: - -First, assume that the credentials are defined in the `values.yaml` file like so: -``` -imageCredentials: - registry: quay.io - username: someone - password: sillyness -``` - -We then define our helper template as follows: -``` -{{- define "imagePullSecret" }} -{{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .Values.imageCredentials.registry (printf "%s:%s" .Values.imageCredentials.username .Values.imageCredentials.password | b64enc) | b64enc }} -{{- end }} -``` - -Finally, we use the helper template in a larger template to create the Secret manifest: -``` -apiVersion: v1 -kind: Secret -metadata: - name: myregistrykey -type: kubernetes.io/dockerconfigjson -data: - .dockerconfigjson: {{ template "imagePullSecret" . }} -``` - -## Automatically Roll Deployments When ConfigMaps or Secrets change - -Often times configmaps or secrets are injected as configuration -files in containers. -Depending on the application a restart may be required should those -be updated with a subsequent `helm upgrade`, but if the -deployment spec itself didn't change the application keeps running -with the old configuration resulting in an inconsistent deployment. - -The `sha256sum` function can be used to ensure a deployment's -annotation section is updated if another file changes: - -```yaml -kind: Deployment -spec: - template: - metadata: - annotations: - checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} -[...] -``` - -See also the `helm upgrade --recreate-pods` flag for a slightly -different way of addressing this issue. - -## Tell Tiller Not To Delete a Resource - -Sometimes there are resources that should not be deleted when Helm runs a -`helm delete`. Chart developers can add an annotation to a resource to prevent -it from being deleted. - -```yaml -kind: Secret -metadata: - annotations: - "helm.sh/resource-policy": keep -[...] -``` - -(Quotation marks are required) - -The annotation `"helm.sh/resource-policy": keep` instructs Tiller to skip this -resource during a `helm delete` operation. _However_, this resource becomes -orphaned. Helm will no longer manage it in any way. This can lead to problems -if using `helm install --replace` on a release that has already been deleted, but -has kept resources. - -## Using "Partials" and Template Includes - -Sometimes you want to create some reusable parts in your chart, whether -they're blocks or template partials. And often, it's cleaner to keep -these in their own files. - -In the `templates/` directory, any file that begins with an -underscore(`_`) is not expected to output a Kubernetes manifest file. So -by convention, helper templates and partials are placed in a -`_helpers.tpl` file. - -## Complex Charts with Many Dependencies - -Many of the charts in the [official charts repository](https://github.com/kubernetes/charts) -are "building blocks" for creating more advanced applications. But charts may be -used to create instances of large-scale applications. In such cases, a single -umbrella chart may have multiple subcharts, each of which functions as a piece -of the whole. - -The current best practice for composing a complex application from discrete parts -is to create a top-level umbrella chart that -exposes the global configurations, and then use the `charts/` subdirectory to -embed each of the components. - -Two strong design patterns are illustrated by these projects: - -**SAP's [OpenStack chart](https://github.com/sapcc/openstack-helm):** This chart -installs a full OpenStack IaaS on Kubernetes. All of the charts are collected -together in one GitHub repository. - -**Deis's [Workflow](https://github.com/deis/workflow/tree/master/charts/workflow):** -This chart exposes the entire Deis PaaS system with one chart. But it's different -from the SAP chart in that this umbrella chart is built from each component, and -each component is tracked in a different Git repository. Check out the -`requirements.yaml` file to see how this chart is composed by their CI/CD -pipeline. - -Both of these charts illustrate proven techniques for standing up complex environments -using Helm. - -## YAML is a Superset of JSON - -According to the YAML specification, YAML is a superset of JSON. That -means that any valid JSON structure ought to be valid in YAML. - -This has an advantage: Sometimes template developers may find it easier -to express a datastructure with a JSON-like syntax rather than deal with -YAML's whitespace sensitivity. - -As a best practice, templates should follow a YAML-like syntax _unless_ -the JSON syntax substantially reduces the risk of a formatting issue. - -## Be Careful with Generating Random Values - -There are functions in Helm that allow you to generate random data, -cryptographic keys, and so on. These are fine to use. But be aware that -during upgrades, templates are re-executed. When a template run -generates data that differs from the last run, that will trigger an -update of that resource. - -## Upgrade a release idempotently - -In order to use the same command when installing and upgrading a release, use the following command: -```shell -helm upgrade --install --values -``` diff --git a/docs/developers.md b/docs/developers.md deleted file mode 100644 index e18c28d5dcd..00000000000 --- a/docs/developers.md +++ /dev/null @@ -1,237 +0,0 @@ -# Developers Guide - -This guide explains how to set up your environment for developing on -Helm and Tiller. - -## Prerequisites - -- The latest version of Go -- The latest version of Glide -- A Kubernetes cluster w/ kubectl (optional) -- The gRPC toolchain -- Git - -## Building Helm/Tiller - -We use Make to build our programs. The simplest way to get started is: - -```console -$ make bootstrap build -``` - -NOTE: This will fail if not running from the path `$GOPATH/src/k8s.io/helm`. The -directory `k8s.io` should not be a symlink or `build` will not find the relevant -packages. - -This will build both Helm and Tiller. `make bootstrap` will attempt to -install certain tools if they are missing. - -To run all the tests (without running the tests for `vendor/`), run -`make test`. - -To run Helm and Tiller locally, you can run `bin/helm` or `bin/tiller`. - -- Helm and Tiller are known to run on macOS and most Linuxes, including - Alpine. -- Tiller must have access to a Kubernetes cluster. It learns about the - cluster by examining the Kube config files that `kubectl` uses. - -### Man pages - -Man pages and Markdown documentation are already pre-built in `docs/`. You may -regenerate documentation using `make docs`. - -To expose the Helm man pages to your `man` client, you can put the files in your -`$MANPATH`: - -``` -$ export MANPATH=$GOPATH/src/k8s.io/helm/docs/man:$MANPATH -$ man helm -``` - -## gRPC and Protobuf - -Helm and Tiller communicate using gRPC. To get started with gRPC, you will need to... - -- Install `protoc` for compiling protobuf files. Releases are - [here](https://github.com/google/protobuf/releases) -- Run Helm's `make bootstrap` to generate the `protoc-gen-go` plugin and - place it in `bin/`. - -Note that you need to be on protobuf 3.2.0 (`protoc --version`). The -version of `protoc-gen-go` is tied to the version of gRPC used in -Kubernetes. So the plugin is maintained locally. - -While the gRPC and ProtoBuf specs remain silent on indentation, we -require that the indentation style matches the Go format specification. -Namely, protocol buffers should use tab-based indentation and rpc -declarations should follow the style of Go function declarations. - -### The Helm API (HAPI) - -We use gRPC as an API layer. See `pkg/proto/hapi` for the generated Go code, -and `_proto` for the protocol buffer definitions. - -To regenerate the Go files from the protobuf source, `make protoc`. - -## Docker Images - -To build Docker images, use `make docker-build`. - -Pre-build images are already available in the official Kubernetes Helm -GCR registry. - -## Running a Local Cluster - -For development, we highly recommend using the -[Kubernetes Minikube](https://github.com/kubernetes/minikube) -developer-oriented distribution. Once this is installed, you can use -`helm init` to install into the cluster. Note that version of tiller you're using for -development may not be available in Google Cloud Container Registry. If you're getting -image pull errors, you can override the version of Tiller. Example: - -```console -helm init --tiller-image=gcr.io/kubernetes-helm/tiller:2.7.2 -``` - -Or use the latest version: - -```console -helm init --canary-image -``` - -For developing on Tiller, it is sometimes more expedient to run Tiller locally -instead of packaging it into an image and running it in-cluster. You can do -this by telling the Helm client to us a local instance. - -```console -$ make build -$ bin/tiller -``` - -And to configure the Helm client, use the `--host` flag or export the `HELM_HOST` -environment variable: - -```console -$ export HELM_HOST=localhost:44134 -$ helm install foo -``` - -(Note that you do not need to use `helm init` when you are running Tiller directly) - -Tiller should run on any >= 1.3 Kubernetes cluster. - -## Contribution Guidelines - -We welcome contributions. This project has set up some guidelines in -order to ensure that (a) code quality remains high, (b) the project -remains consistent, and (c) contributions follow the open source legal -requirements. Our intent is not to burden contributors, but to build -elegant and high-quality open source code so that our users will benefit. - -Make sure you have read and understood the main CONTRIBUTING guide: - -https://github.com/kubernetes/helm/blob/master/CONTRIBUTING.md - -### Structure of the Code - -The code for the Helm project is organized as follows: - -- The individual programs are located in `cmd/`. Code inside of `cmd/` - is not designed for library re-use. -- Shared libraries are stored in `pkg/`. -- The raw ProtoBuf files are stored in `_proto/hapi` (where `hapi` stands for - the Helm Application Programming Interface). -- The Go files generated from the `proto` definitions are stored in `pkg/proto`. -- The `scripts/` directory contains a number of utility scripts. Most of these - are used by the CI/CD pipeline. -- The `rootfs/` folder is used for Docker-specific files. -- The `docs/` folder is used for documentation and examples. - -Go dependencies are managed with -[Glide](https://github.com/Masterminds/glide) and stored in the -`vendor/` directory. - -### Git Conventions - -We use Git for our version control system. The `master` branch is the -home of the current development candidate. Releases are tagged. - -We accept changes to the code via GitHub Pull Requests (PRs). One -workflow for doing this is as follows: - -1. Go to your `$GOPATH/src/k8s.io` directory and `git clone` the - `github.com/kubernetes/helm` repository. -2. Fork that repository into your GitHub account -3. Add your repository as a remote for `$GOPATH/src/k8s.io/helm` -4. Create a new working branch (`git checkout -b feat/my-feature`) and - do your work on that branch. -5. When you are ready for us to review, push your branch to GitHub, and - then open a new pull request with us. - -For Git commit messages, we follow the [Semantic Commit Messages](http://karma-runner.github.io/0.13/dev/git-commit-msg.html): - -``` -fix(helm): add --foo flag to 'helm install' - -When 'helm install --foo bar' is run, this will print "foo" in the -output regardless of the outcome of the installation. - -Closes #1234 -``` - -Common commit types: - -- fix: Fix a bug or error -- feat: Add a new feature -- docs: Change documentation -- test: Improve testing -- ref: refactor existing code - -Common scopes: - -- helm: The Helm CLI -- tiller: The Tiller server -- proto: Protobuf definitions -- pkg/lint: The lint package. Follow a similar convention for any - package -- `*`: two or more scopes - -Read more: -- The [Deis Guidelines](https://github.com/deis/workflow/blob/master/src/contributing/submitting-a-pull-request.md) - were the inspiration for this section. -- Karma Runner [defines](http://karma-runner.github.io/0.13/dev/git-commit-msg.html) the semantic commit message idea. - -### Go Conventions - -We follow the Go coding style standards very closely. Typically, running -`go fmt` will make your code beautiful for you. - -We also typically follow the conventions recommended by `go lint` and -`gometalinter`. Run `make test-style` to test the style conformance. - -Read more: - -- Effective Go [introduces formatting](https://golang.org/doc/effective_go.html#formatting). -- The Go Wiki has a great article on [formatting](https://github.com/golang/go/wiki/CodeReviewComments). - -### Protobuf Conventions - -Because this project is largely Go code, we format our Protobuf files as -closely to Go as possible. There are currently no real formatting rules -or guidelines for Protobuf, but as they emerge, we may opt to follow -those instead. - -Standards: -- Tabs for indentation, not spaces. -- Spacing rules follow Go conventions (curly braces at line end, spaces - around operators). - -Conventions: -- Files should specify their package with `option go_package = "...";` -- Comments should translate into good Go code comments (since `protoc` - copies comments into the destination source code file). -- RPC functions are defined in the same file as their request/response - messages. -- Deprecated RPCs, messages, and fields are marked deprecated in the comments (`// UpdateFoo - DEPRECATED updates a foo.`). diff --git a/docs/examples/README.md b/docs/examples/README.md deleted file mode 100644 index 723040ca84b..00000000000 --- a/docs/examples/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Helm Examples - -This directory contains example charts to help you get started with -chart development. - -## Alpine - -The `alpine` chart is very simple, and is a good starting point. - -It simply deploys a single pod running Alpine Linux. - -## Nginx - -The `nginx` chart shows how to compose several resources into one chart, -and it illustrates more complex template usage. - -It deploys a `deployment` (which creates a `replica set`), a `config -map`, and a `service`. The replica set starts an nginx pod. The config -map stores the files that the nginx server can serve. diff --git a/docs/examples/alpine/Chart.yaml b/docs/examples/alpine/Chart.yaml deleted file mode 100644 index f4b660d4f82..00000000000 --- a/docs/examples/alpine/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: alpine -description: Deploy a basic Alpine Linux pod -version: 0.1.0 -home: https://github.com/kubernetes/helm -sources: - - https://github.com/kubernetes/helm -appVersion: 3.3 diff --git a/docs/examples/alpine/README.md b/docs/examples/alpine/README.md deleted file mode 100644 index 3e354724c8a..00000000000 --- a/docs/examples/alpine/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Alpine: A simple Helm chart - -Run a single pod of Alpine Linux. - -The `templates/` directory contains a very simple pod resource with a -couple of parameters. - -The `values.yaml` file contains the default values for the -`alpine-pod.yaml` template. - -You can install this example using `helm install docs/examples/alpine`. diff --git a/docs/examples/alpine/templates/_helpers.tpl b/docs/examples/alpine/templates/_helpers.tpl deleted file mode 100644 index 3e9c25bed07..00000000000 --- a/docs/examples/alpine/templates/_helpers.tpl +++ /dev/null @@ -1,16 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "alpine.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -*/}} -{{- define "alpine.fullname" -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} diff --git a/docs/examples/alpine/templates/alpine-pod.yaml b/docs/examples/alpine/templates/alpine-pod.yaml deleted file mode 100644 index da9caef781b..00000000000 --- a/docs/examples/alpine/templates/alpine-pod.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: {{ template "alpine.fullname" . }} - labels: - # The "heritage" label is used to track which tool deployed a given chart. - # It is useful for admins who want to see what releases a particular tool - # is responsible for. - heritage: {{ .Release.Service }} - # The "release" convention makes it easy to tie a release to all of the - # Kubernetes resources that were created as part of that release. - release: {{ .Release.Name }} - # This makes it easy to audit chart usage. - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - app: {{ template "alpine.name" . }} -spec: - # This shows how to use a simple value. This will look for a passed-in value called restartPolicy. - restartPolicy: {{ .Values.restartPolicy }} - containers: - - name: waiter - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - command: ["/bin/sleep", "9000"] diff --git a/docs/examples/alpine/values.yaml b/docs/examples/alpine/values.yaml deleted file mode 100644 index afe8cc6c0f6..00000000000 --- a/docs/examples/alpine/values.yaml +++ /dev/null @@ -1,6 +0,0 @@ -image: - repository: alpine - tag: 3.3 - pullPolicy: IfNotPresent - -restartPolicy: Never diff --git a/docs/examples/nginx/.helmignore b/docs/examples/nginx/.helmignore deleted file mode 100644 index 435b756d885..00000000000 --- a/docs/examples/nginx/.helmignore +++ /dev/null @@ -1,5 +0,0 @@ -# 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/docs/examples/nginx/Chart.yaml b/docs/examples/nginx/Chart.yaml deleted file mode 100644 index 807455210a2..00000000000 --- a/docs/examples/nginx/Chart.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: nginx -description: A basic NGINX HTTP server -version: 0.1.0 -kubeVersion: ">=1.2.0" -keywords: - - http - - nginx - - www - - web -home: https://github.com/kubernetes/helm -sources: - - https://hub.docker.com/_/nginx/ -maintainers: - - name: technosophos - email: mbutcher@deis.com diff --git a/docs/examples/nginx/README.md b/docs/examples/nginx/README.md deleted file mode 100644 index e7a02e578fd..00000000000 --- a/docs/examples/nginx/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# nginx: An advanced example chart - -This Helm chart provides examples of some of Helm's more powerful -features. - -**This is not a production-grade chart. It is an example.** - -The chart installs a simple nginx server according to the following -pattern: - -- A `ConfigMap` is used to store the files the server will serve. - ([templates/configmap.yaml](templates/configmap.yaml)) -- A `Deployment` is used to create a Replica Set of nginx pods. - ([templates/deployment.yaml](templates/deployment.yaml)) -- A `Service` is used to create a gateway to the pods running in the - replica set ([templates/service.yaml](templates/service.yaml)) - -The [values.yaml](values.yaml) exposes a few of the configuration options in the -charts, though there are some that are not exposed there (like -`.image`). - -The [templates/_helpers.tpl](templates/_helpers.tpl) file contains helper templates. The leading -underscore (`_`) on the filename is semantic. It tells the template renderer -that this file does not contain a manifest. That file declares some -templates that are used elsewhere in the chart. - -Helpers (usually called "partials" in template languages) are an -advanced way for developers to structure their templates for optimal -reuse. - -You can deploy this chart with `helm install docs/examples/nginx`. Or -you can see how this chart would render with `helm install --dry-run ---debug docs/examples/nginx`. diff --git a/docs/examples/nginx/templates/_helpers.tpl b/docs/examples/nginx/templates/_helpers.tpl deleted file mode 100644 index 2ec6ba75757..00000000000 --- a/docs/examples/nginx/templates/_helpers.tpl +++ /dev/null @@ -1,16 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "nginx.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -*/}} -{{- define "nginx.fullname" -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} diff --git a/docs/examples/nginx/templates/configmap.yaml b/docs/examples/nginx/templates/configmap.yaml deleted file mode 100644 index b90d6c0c72c..00000000000 --- a/docs/examples/nginx/templates/configmap.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# This is a simple example of using a config map to create a single page static site. -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ template "nginx.fullname" . }} - labels: - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - app: {{ template "nginx.name" . }} -data: - # When the config map is mounted as a volume, these will be created as files. - index.html: {{ .Values.index | quote }} - test.txt: test diff --git a/docs/examples/nginx/templates/deployment.yaml b/docs/examples/nginx/templates/deployment.yaml deleted file mode 100644 index 5fa2633eab7..00000000000 --- a/docs/examples/nginx/templates/deployment.yaml +++ /dev/null @@ -1,57 +0,0 @@ -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - # This uses a "fullname" template (see _helpers) - # Basing names on .Release.Name means that the same chart can be installed - # multiple times into the same namespace. - name: {{ template "nginx.fullname" . }} - labels: - # The "heritage" label is used to track which tool deployed a given chart. - # It is useful for admins who want to see what releases a particular tool - # is responsible for. - heritage: {{ .Release.Service }} - # The "release" convention makes it easy to tie a release to all of the - # Kubernetes resources that were created as part of that release. - release: {{ .Release.Name }} - # This makes it easy to audit chart usage. - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - app: {{ template "nginx.name" . }} -spec: - replicas: {{ .Values.replicaCount }} - template: - metadata: -{{- if .Values.podAnnotations }} - # Allows custom annotations to be specified - annotations: -{{ toYaml .Values.podAnnotations | indent 8 }} -{{- end }} - labels: - app: {{ template "nginx.name" . }} - release: {{ .Release.Name }} - spec: - containers: - - name: {{ template "nginx.name" . }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: 80 - protocol: TCP - # This (and the volumes section below) mount the config map as a volume. - volumeMounts: - - mountPath: /usr/share/nginx/html - name: wwwdata-volume - resources: -# Allow chart users to specify resources. Usually, no default should be set, so this is left to be a conscious -# choice to the chart users and avoids that charts don't run out of the box on, e. g., Minikube when high resource -# requests are specified by default. -{{ toYaml .Values.resources | indent 12 }} - {{- if .Values.nodeSelector }} - nodeSelector: - # Node selectors can be important on mixed Windows/Linux clusters. -{{ toYaml .Values.nodeSelector | indent 8 }} - {{- end }} - volumes: - - name: wwwdata-volume - configMap: - name: {{ template "nginx.fullname" . }} diff --git a/docs/examples/nginx/templates/post-install-job.yaml b/docs/examples/nginx/templates/post-install-job.yaml deleted file mode 100644 index 9ec90cd0aa2..00000000000 --- a/docs/examples/nginx/templates/post-install-job.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ template "nginx.fullname" . }} - labels: - # The "heritage" label is used to track which tool deployed a given chart. - # It is useful for admins who want to see what releases a particular tool - # is responsible for. - heritage: {{ .Release.Service }} - # The "release" convention makes it easy to tie a release to all of the - # Kubernetes resources that were created as part of that release. - release: {{ .Release.Name }} - # This makes it easy to audit chart usage. - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - app: {{ template "nginx.name" . }} - 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 "nginx.fullname" . }} - labels: - release: {{ .Release.Name }} - app: {{ template "nginx.name" . }} - 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: {{ .Values.restartPolicy }} - containers: - - name: post-install-job - image: "alpine:3.3" - # All we're going to do is sleep for a while, then exit. - command: ["/bin/sleep", "{{ .Values.sleepyTime }}"] diff --git a/docs/examples/nginx/templates/pre-install-secret.yaml b/docs/examples/nginx/templates/pre-install-secret.yaml deleted file mode 100644 index 6392f9684a4..00000000000 --- a/docs/examples/nginx/templates/pre-install-secret.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# 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: {{ template "nginx.fullname" . }} - labels: - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - app: {{ template "nginx.name" . }} - # 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/templates/service-test.yaml b/docs/examples/nginx/templates/service-test.yaml deleted file mode 100644 index 3913ead9ccc..00000000000 --- a/docs/examples/nginx/templates/service-test.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ template "nginx.fullname" . }}-service-test" - labels: - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - app: {{ template "nginx.name" . }} - annotations: - "helm.sh/hook": test-success -spec: - containers: - - name: curl - image: radial/busyboxplus:curl - command: ['curl'] - args: ['{{ template "nginx.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/docs/examples/nginx/templates/service.yaml b/docs/examples/nginx/templates/service.yaml deleted file mode 100644 index 1481e34f01a..00000000000 --- a/docs/examples/nginx/templates/service.yaml +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: -{{- if .Values.service.annotations }} - annotations: -{{ toYaml .Values.service.annotations | indent 4 }} -{{- end }} - labels: - app: {{ template "nginx.name" . }} - chart: {{ .Chart.Name }}-{{ .Chart.Version }} - heritage: {{ .Release.Service }} - release: {{ .Release.Name }} - name: {{ template "nginx.fullname" . }} -spec: -# Provides options for the service so chart users have the full choice - type: "{{ .Values.service.type }}" - clusterIP: "{{ .Values.service.clusterIP }}" -{{- if .Values.service.externalIPs }} - externalIPs: -{{ toYaml .Values.service.externalIPs | indent 4 }} -{{- end }} -{{- if .Values.service.loadBalancerIP }} - loadBalancerIP: "{{ .Values.service.loadBalancerIP }}" -{{- end }} -{{- if .Values.service.loadBalancerSourceRanges }} - loadBalancerSourceRanges: -{{ toYaml .Values.service.loadBalancerSourceRanges | indent 4 }} -{{- end }} - ports: - - name: http - port: {{ .Values.service.port }} - protocol: TCP - targetPort: http - {{- if (and (eq .Values.service.type "NodePort") (not (empty .Values.service.nodePort))) }} - nodePort: {{ .Values.service.nodePort }} - {{- end }} - selector: - app: {{ template "nginx.name" . }} - release: {{ .Release.Name }} diff --git a/docs/examples/nginx/values.yaml b/docs/examples/nginx/values.yaml deleted file mode 100644 index b40208ccec0..00000000000 --- a/docs/examples/nginx/values.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# Default values for nginx. -# This is a YAML-formatted file. -# Declare name/value pairs to be passed into your templates. - -replicaCount: 1 -restartPolicy: Never - -# Evaluated by the post-install hook -sleepyTime: "10" - -index: >- -

Hello

-

This is a test

- -image: - repository: nginx - tag: 1.11.0 - pullPolicy: IfNotPresent - -service: - annotations: {} - clusterIP: "" - externalIPs: [] - loadBalancerIP: "" - loadBalancerSourceRanges: [] - type: ClusterIP - port: 8888 - nodePort: "" - -podAnnotations: {} - -resources: {} - -nodeSelector: {} diff --git a/docs/glossary.md b/docs/glossary.md deleted file mode 100644 index 87580726897..00000000000 --- a/docs/glossary.md +++ /dev/null @@ -1,170 +0,0 @@ -# Helm Glossary - -Helm uses a few special terms to describe components of the -architecture. - -## Chart - -A Helm package that contains information sufficient for installing a set -of Kubernetes resources into a Kubernetes cluster. - -Charts contain a `Chart.yaml` file as well as templates, default values -(`values.yaml`), and dependencies. - -Charts are developed in a well-defined directory structure, and then -packaged into an archive format called a _chart archive_. - -## Chart Archive - -A _chart archive_ is a tarred and gzipped (and optionally signed) chart. - -## Chart Dependency (Subcharts) - -Charts may depend upon other charts. There are two ways a dependency may -occur: - -- Soft dependency: A chart may simply not function without another chart - being installed in a cluster. Helm does not provide tooling for this - case. In this case, dependencies may be managed separately. -- Hard dependency: A chart may contain (inside of its `charts/` - directory) another chart upon which it depends. In this case, - installing the chart will install all of its dependencies. In this - case, a chart and its dependencies are managed as a collection. - -When a chart is packaged (via `helm package`) all of its hard dependencies -are bundled with it. - -## Chart Version - -Charts are versioned according to the [SemVer 2 -spec](http://semver.org). A version number is required on every chart. - -## Chart.yaml - -Information about a chart is stored in a special file called -`Chart.yaml`. Every chart must have this file. - -## Helm (and helm) - -Helm is the package manager for Kubernetes. As an operating system -package manager makes it easy to install tools on an OS, Helm makes it -easy to install applications and resources into Kubernetes clusters. - -While _Helm_ is the name of the project, the command line client is also -named `helm`. By convention, when speaking of the project, _Helm_ is -capitalized. When speaking of the client, _helm_ is in lowercase. - -## Helm Home (HELM_HOME) - -The Helm client stores information in a local directory referred to as -_helm home_. By default, this is in the `$HOME/.helm` directory. - -This directory contains configuration and cache data, and is created by -`helm init`. - -## Kube Config (KUBECONFIG) - -The Helm client learns about Kubernetes clusters by using files in the _Kube -config_ file format. By default, Helm attempts to find this file in the -place where `kubectl` creates it (`$HOME/.kube/config`). - -## Lint (Linting) - -To _lint_ a chart is to validate that it follows the conventions and -requirements of the Helm chart standard. Helm provides tools to do this, -notably the `helm lint` command. - -## Provenance (Provenance file) - -Helm charts may be accompanied by a _provenance file_ which provides -information about where the chart came from and what it contains. - -Provenance files are one part of the Helm security story. A provenance contains -a cryptographic hash of the chart archive file, the Chart.yaml data, and -a signature block (an OpenPGP "clearsign" block). When coupled with a -keychain, this provides chart users with the ability to: - -- Validate that a chart was signed by a trusted party -- Validate that the chart file has not been tampered with -- Validate the contents of a chart metadata (`Chart.yaml`) -- Quickly match a chart to its provenance data - -Provenance files have the `.prov` extension, and can be served from a -chart repository server or any other HTTP server. - -## Release - -When a chart is installed, Tiller (the Helm server) creates a _release_ -to track that installation. - -A single chart may be installed many times into the same cluster, and -create many different releases. For example, one can install three -PostgreSQL databases by running `helm install` three times with a -different release name. - -(Prior to 2.0.0-Alpha.1, releases were called _deployments_. But this -caused confusion with the Kubernetes _Deployment_ kind.) - -## Release Number (Release Version) - -A single release can be updated multiple times. A sequential counter is -used to track releases as they change. After a first `helm install`, a -release will have _release number_ 1. Each time a release is upgraded or -rolled back, the release number will be incremented. - -## Rollback - -A release can be upgraded to a newer chart or configuration. But since -release history is stored, a release can also be _rolled back_ to a -previous release number. This is done with the `helm rollback` command. - -Importantly, a rolled back release will receive a new release number. - -Operation | Release Number -----------|--------------- -install | release 1 -upgrade | release 2 -upgrade | release 3 -rollback 1| release 4 (but running the same config as release 1) - -The above table illustrates how release numbers increment across -install, upgrade, and rollback. - -## Tiller - -Tiller is the in-cluster component of Helm. It interacts directly with -the Kubernetes API server to install, upgrade, query, and remove -Kubernetes resources. It also stores the objects that represent -releases. - -## Repository (Repo, Chart Repository) - -Helm charts may be stored on dedicated HTTP servers called _chart -repositories_ (_repositories_, or just _repos_). - -A chart repository server is a simple HTTP server that can serve an -`index.yaml` file that describes a batch of charts, and provides -information on where each chart can be downloaded from. (Many chart -repositories serve the charts as well as the `index.yaml` file.) - -A Helm client can point to zero or more chart repositories. By default, -Helm clients point to the `stable` official Kubernetes chart -repository. - -## Values (Values Files, values.yaml) - -Values provide a way to override template defaults with your own -information. - -Helm Charts are "parameterized", which means the chart developer may -expose configuration that can be overridden at installation time. For -example, a chart may expose a `username` field that allows setting a -user name for a service. - -These exposed variables are called _values_ in Helm parlance. - -Values can be set during `helm install` and `helm upgrade` operations, -either by passing them in directly, or by uploading a `values.yaml` -file. - - diff --git a/docs/helm/helm.md b/docs/helm/helm.md deleted file mode 100644 index 8592cad7c60..00000000000 --- a/docs/helm/helm.md +++ /dev/null @@ -1,71 +0,0 @@ -## helm - -The Helm package manager for Kubernetes. - -### Synopsis - - -The Kubernetes package manager - -To begin working with Helm, run the 'helm init' command: - - $ helm init - -This will install Tiller to your running Kubernetes cluster. -It will also set up any necessary local configuration. - -Common actions from this point include: - -- helm search: search for charts -- helm fetch: download a chart to your local directory to view -- helm install: upload the chart to Kubernetes -- helm list: list releases of charts - -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-system") - $KUBECONFIG set an alternative Kubernetes configuration file (default "~/.kube/config") - - -### Options - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm completion](helm_completion.md) - Generate autocompletions script for the specified shell (bash or zsh) -* [helm create](helm_create.md) - create a new chart with the given name -* [helm delete](helm_delete.md) - given a release name, delete the release from Kubernetes -* [helm dependency](helm_dependency.md) - manage a chart's dependencies -* [helm fetch](helm_fetch.md) - download a chart from a repository and (optionally) unpack it in local directory -* [helm get](helm_get.md) - download a named release -* [helm history](helm_history.md) - fetch release history -* [helm home](helm_home.md) - displays the location of HELM_HOME -* [helm init](helm_init.md) - initialize Helm on both client and server -* [helm inspect](helm_inspect.md) - inspect a chart -* [helm install](helm_install.md) - install a chart archive -* [helm lint](helm_lint.md) - examines a chart for possible issues -* [helm list](helm_list.md) - list releases -* [helm package](helm_package.md) - package a chart directory into a chart archive -* [helm plugin](helm_plugin.md) - add, list, or remove Helm plugins -* [helm repo](helm_repo.md) - add, list, remove, update, and index chart repositories -* [helm reset](helm_reset.md) - uninstalls Tiller from a cluster -* [helm rollback](helm_rollback.md) - roll back a release to a previous revision -* [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 14-Mar-2018 diff --git a/docs/helm/helm_completion.md b/docs/helm/helm_completion.md deleted file mode 100644 index 994205d881e..00000000000 --- a/docs/helm/helm_completion.md +++ /dev/null @@ -1,38 +0,0 @@ -## helm completion - -Generate autocompletions script for the specified shell (bash or zsh) - -### Synopsis - - - -Generate autocompletions script for Helm for the specified shell (bash or zsh). - -This command can generate shell autocompletions. e.g. - - $ helm completion bash - -Can be sourced as such - - $ source <(helm completion bash) - - -``` -helm completion SHELL -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_create.md b/docs/helm/helm_create.md deleted file mode 100644 index 6e0f3de78b3..00000000000 --- a/docs/helm/helm_create.md +++ /dev/null @@ -1,57 +0,0 @@ -## helm create - -create a new chart with the given name - -### Synopsis - - - -This command creates a chart directory along with the common files and -directories used in a chart. - -For example, 'helm create foo' will create a directory structure that looks -something like this: - - foo/ - | - |- .helmignore # Contains patterns to ignore when packaging Helm charts. - | - |- Chart.yaml # Information about your chart - | - |- values.yaml # The default values for your templates - | - |- charts/ # Charts that this chart depends on - | - |- templates/ # The template files - -'helm create' takes a path for an argument. If directories in the given path -do not exist, Helm will attempt to create them as it goes. If the given -destination exists and there are files in that directory, conflicting files -will be overwritten, but other files will be left alone. - - -``` -helm create NAME -``` - -### Options - -``` - -p, --starter string the named Helm starter scaffold -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_delete.md b/docs/helm/helm_delete.md deleted file mode 100644 index 5d41cd7eaf5..00000000000 --- a/docs/helm/helm_delete.md +++ /dev/null @@ -1,48 +0,0 @@ -## helm delete - -given a release name, delete the release from Kubernetes - -### Synopsis - - - -This command takes a release name, and then deletes the release from Kubernetes. -It removes all of the resources associated with the last release of the chart. - -Use the '--dry-run' flag to see which releases will be deleted without actually -deleting them. - - -``` -helm delete [flags] RELEASE_NAME [...] -``` - -### Options - -``` - --dry-run simulate a delete - --no-hooks prevent hooks from running during deletion - --purge remove the release from the store and make its name free for later use - --timeout int time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks) (default 300) - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_dependency.md b/docs/helm/helm_dependency.md deleted file mode 100644 index 34d49e20a09..00000000000 --- a/docs/helm/helm_dependency.md +++ /dev/null @@ -1,74 +0,0 @@ -## helm dependency - -manage a chart's dependencies - -### Synopsis - - - -Manage the dependencies of a chart. - -Helm charts store their dependencies in 'charts/'. For chart developers, it is -often easier to manage a single dependency file ('requirements.yaml') -which declares all dependencies. - -The dependency commands operate on that file, making it easy to synchronize -between the desired dependencies and the actual dependencies stored in the -'charts/' directory. - -A 'requirements.yaml' file is a YAML file in which developers can declare chart -dependencies, along with the location of the chart and the desired version. -For example, this requirements file declares two dependencies: - - # requirements.yaml - dependencies: - - name: nginx - version: "1.2.3" - repository: "https://example.com/charts" - - name: memcached - version: "3.2.1" - repository: "https://another.example.com/charts" - -The 'name' should be the name of a chart, where that name must match the name -in that chart's 'Chart.yaml' file. - -The 'version' field should contain a semantic version or version range. - -The 'repository' URL should point to a Chart Repository. Helm expects that by -appending '/index.yaml' to the URL, it should be able to retrieve the chart -repository's index. Note: 'repository' can be an alias. The alias must start -with 'alias:' or '@'. - -Starting from 2.2.0, repository can be defined as the path to the directory of -the dependency charts stored locally. The path should start with a prefix of -"file://". For example, - - # requirements.yaml - dependencies: - - name: nginx - version: "1.2.3" - repository: "file://../dependency_chart/nginx" - -If the dependency chart is retrieved locally, it is not required to have the -repository added to helm by "helm add repo". Version matching is also supported -for this case. - - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm](helm.md) - The Helm package manager for Kubernetes. -* [helm dependency build](helm_dependency_build.md) - rebuild the charts/ directory based on the requirements.lock file -* [helm dependency list](helm_dependency_list.md) - list the dependencies for the given chart -* [helm dependency update](helm_dependency_update.md) - update charts/ based on the contents of requirements.yaml - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_dependency_build.md b/docs/helm/helm_dependency_build.md deleted file mode 100644 index 0413a9a8531..00000000000 --- a/docs/helm/helm_dependency_build.md +++ /dev/null @@ -1,44 +0,0 @@ -## helm dependency build - -rebuild the charts/ directory based on the requirements.lock file - -### Synopsis - - - -Build out the charts/ directory from the requirements.lock file. - -Build is used to reconstruct a chart's dependencies to the state specified in -the lock file. This will not re-negotiate dependencies, as 'helm dependency update' -does. - -If no lock file is found, 'helm dependency build' will mirror the behavior -of 'helm dependency update'. - - -``` -helm dependency build [flags] CHART -``` - -### Options - -``` - --keyring string keyring containing public keys (default "~/.gnupg/pubring.gpg") - --verify verify the packages against signatures -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm dependency](helm_dependency.md) - manage a chart's dependencies - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_dependency_list.md b/docs/helm/helm_dependency_list.md deleted file mode 100644 index b4343081c3a..00000000000 --- a/docs/helm/helm_dependency_list.md +++ /dev/null @@ -1,36 +0,0 @@ -## helm dependency list - -list the dependencies for the given chart - -### Synopsis - - - -List all of the dependencies declared in a chart. - -This can take chart archives and chart directories as input. It will not alter -the contents of a chart. - -This will produce an error if the chart cannot be loaded. It will emit a warning -if it cannot find a requirements.yaml. - - -``` -helm dependency list [flags] CHART -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm dependency](helm_dependency.md) - manage a chart's dependencies - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_dependency_update.md b/docs/helm/helm_dependency_update.md deleted file mode 100644 index 3c90ff77975..00000000000 --- a/docs/helm/helm_dependency_update.md +++ /dev/null @@ -1,49 +0,0 @@ -## helm dependency update - -update charts/ based on the contents of requirements.yaml - -### Synopsis - - - -Update the on-disk dependencies to mirror the requirements.yaml file. - -This command verifies that the required charts, as expressed in 'requirements.yaml', -are present in 'charts/' and are at an acceptable version. It will pull down -the latest charts that satisfy the dependencies, and clean up old dependencies. - -On successful update, this will generate a lock file that can be used to -rebuild the requirements to an exact version. - -Dependencies are not required to be represented in 'requirements.yaml'. For that -reason, an update command will not remove charts unless they are (a) present -in the requirements.yaml file, but (b) at the wrong version. - - -``` -helm dependency update [flags] CHART -``` - -### Options - -``` - --keyring string keyring containing public keys (default "~/.gnupg/pubring.gpg") - --skip-refresh do not refresh the local repository cache - --verify verify the packages against signatures -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm dependency](helm_dependency.md) - manage a chart's dependencies - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_fetch.md b/docs/helm/helm_fetch.md deleted file mode 100644 index 1ddef65fa07..00000000000 --- a/docs/helm/helm_fetch.md +++ /dev/null @@ -1,58 +0,0 @@ -## helm fetch - -download a chart from a repository and (optionally) unpack it in local directory - -### Synopsis - - - -Retrieve a package from a package repository, and download it locally. - -This is useful for fetching packages to inspect, modify, or repackage. It can -also be used to perform cryptographic verification of a chart without installing -the chart. - -There are options for unpacking the chart after download. This will create a -directory for the chart and uncompress into that directory. - -If the --verify flag is specified, the requested chart MUST have a provenance -file, and MUST pass the verification process. Failure in any part of this will -result in an error, and the chart will not be saved locally. - - -``` -helm fetch [flags] [chart URL | repo/chartname] [...] -``` - -### Options - -``` - --ca-file string verify certificates of HTTPS-enabled servers using this CA bundle - --cert-file string identify HTTPS client using this SSL certificate file - -d, --destination string location to write the chart. If this and tardir are specified, tardir is appended to this (default ".") - --devel use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored. - --key-file string identify HTTPS client using this SSL key file - --keyring string keyring containing public keys (default "~/.gnupg/pubring.gpg") - --password string chart repository password - --prov fetch the provenance file, but don't perform verification - --repo string chart repository url where to locate the requested chart - --untar if set to true, will untar the chart after downloading it - --untardir string if untar is specified, this flag specifies the name of the directory into which the chart is expanded (default ".") - --username string chart repository username - --verify verify the package against its signature - --version string specific version of a chart. Without this, the latest version is fetched -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm](helm.md) - The Helm package manager for Kubernetes. diff --git a/docs/helm/helm_get.md b/docs/helm/helm_get.md deleted file mode 100644 index 9cd70e52020..00000000000 --- a/docs/helm/helm_get.md +++ /dev/null @@ -1,53 +0,0 @@ -## helm get - -download a named release - -### Synopsis - - - -This command shows the details of a named release. - -It can be used to get extended information about the release, including: - - - The values used to generate the release - - The chart used to generate the release - - The generated manifest file - -By default, this prints a human readable collection of information about the -chart, the supplied values, and the generated manifest file. - - -``` -helm get [flags] RELEASE_NAME -``` - -### Options - -``` - --revision int32 get the named release with revision - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm](helm.md) - The Helm package manager for Kubernetes. -* [helm get hooks](helm_get_hooks.md) - download all hooks for a named release -* [helm get manifest](helm_get_manifest.md) - download the manifest for a named release -* [helm get values](helm_get_values.md) - download the values file for a named release - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_get_hooks.md b/docs/helm/helm_get_hooks.md deleted file mode 100644 index 85fa5d04ba6..00000000000 --- a/docs/helm/helm_get_hooks.md +++ /dev/null @@ -1,43 +0,0 @@ -## helm get hooks - -download all hooks for a named release - -### Synopsis - - - -This command downloads hooks for a given release. - -Hooks are formatted in YAML and separated by the YAML '---\n' separator. - - -``` -helm get hooks [flags] RELEASE_NAME -``` - -### Options - -``` - --revision int32 get the named release with revision - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm get](helm_get.md) - download a named release - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_get_manifest.md b/docs/helm/helm_get_manifest.md deleted file mode 100644 index a00c1be568a..00000000000 --- a/docs/helm/helm_get_manifest.md +++ /dev/null @@ -1,45 +0,0 @@ -## helm get manifest - -download the manifest for a named release - -### Synopsis - - - -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. - - -``` -helm get manifest [flags] RELEASE_NAME -``` - -### Options - -``` - --revision int32 get the named release with revision - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm get](helm_get.md) - download a named release - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_get_values.md b/docs/helm/helm_get_values.md deleted file mode 100644 index d8944b4757b..00000000000 --- a/docs/helm/helm_get_values.md +++ /dev/null @@ -1,42 +0,0 @@ -## helm get values - -download the values file for a named release - -### Synopsis - - - -This command downloads a values file for a given release. - - -``` -helm get values [flags] RELEASE_NAME -``` - -### Options - -``` - -a, --all dump all (computed) values - --revision int32 get the named release with revision - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm get](helm_get.md) - download a named release - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_history.md b/docs/helm/helm_history.md deleted file mode 100755 index ac51a899450..00000000000 --- a/docs/helm/helm_history.md +++ /dev/null @@ -1,55 +0,0 @@ -## helm history - -fetch release history - -### Synopsis - - - -History prints historical revisions for a given release. - -A default maximum of 256 revisions will be returned. Setting '--max' -configures the maximum length of the revision list returned. - -The historical release set is printed as a formatted table, e.g: - - $ helm history angry-bird --max=4 - REVISION UPDATED STATUS CHART DESCRIPTION - 1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Initial install - 2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Upgraded successfully - 3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Rolled back to 2 - 4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 Upgraded successfully - - -``` -helm history [flags] RELEASE_NAME -``` - -### Options - -``` - --col-width uint specifies the max column width of output (default 60) - --max int32 maximum number of revision to include in history (default 256) - -o, --output string prints the output in the specified format (json|table|yaml) (default "table") - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 14-Mar-2018 diff --git a/docs/helm/helm_home.md b/docs/helm/helm_home.md deleted file mode 100644 index bdccd756f4d..00000000000 --- a/docs/helm/helm_home.md +++ /dev/null @@ -1,31 +0,0 @@ -## helm home - -displays the location of HELM_HOME - -### Synopsis - - - -This command displays the location of HELM_HOME. This is where -any helm configuration files live. - - -``` -helm home -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_init.md b/docs/helm/helm_init.md deleted file mode 100644 index 5374488af69..00000000000 --- a/docs/helm/helm_init.md +++ /dev/null @@ -1,74 +0,0 @@ -## helm init - -initialize Helm on both client and server - -### Synopsis - - - -This command installs Tiller (the Helm server-side component) onto your -Kubernetes Cluster and sets up local configuration in $HELM_HOME (default ~/.helm/). - -As with the rest of the Helm commands, 'helm init' discovers Kubernetes clusters -by reading $KUBECONFIG (default '~/.kube/config') and using the default context. - -To set up just a local environment, use '--client-only'. That will configure -$HELM_HOME, but not attempt to connect to a Kubernetes cluster and install the Tiller -deployment. - -When installing Tiller, 'helm init' will attempt to install the latest released -version. You can specify an alternative image with '--tiller-image'. For those -frequently working on the latest code, the flag '--canary-image' will install -the latest pre-release version of Tiller (e.g. the HEAD commit in the GitHub -repository on the master branch). - -To dump a manifest containing the Tiller deployment YAML, combine the -'--dry-run' and '--debug' flags. - - -``` -helm init -``` - -### Options - -``` - --canary-image use the canary Tiller image - -c, --client-only if set does not install Tiller - --dry-run do not install local or remote - --force-upgrade force upgrade of Tiller to the current helm version - --history-max int limit the maximum number of revisions saved per release. Use 0 for no limit. - --local-repo-url string URL for local repository (default "http://127.0.0.1:8879/charts") - --net-host install Tiller with net=host - --node-selectors string labels to specify the node on which Tiller is installed (app=tiller,helm=rocks) - -o, --output OutputFormat skip installation and output Tiller's manifest in specified format (json or yaml) - --override stringArray override values for the Tiller Deployment manifest (can specify multiple or separate values with commas: key1=val1,key2=val2) - --replicas int amount of tiller instances to run on the cluster (default 1) - --service-account string name of service account - --skip-refresh do not refresh (download) the local repository cache - --stable-repo-url string URL for stable repository (default "https://kubernetes-charts.storage.googleapis.com") - -i, --tiller-image string override Tiller image - --tiller-tls install Tiller with TLS enabled - --tiller-tls-cert string path to TLS certificate file to install with Tiller - --tiller-tls-key string path to TLS key file to install with Tiller - --tiller-tls-verify install Tiller with TLS enabled and to verify remote certificates - --tls-ca-cert string path to CA root certificate - --upgrade upgrade if Tiller is already installed - --wait block until Tiller is running and ready to receive requests -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_inspect.md b/docs/helm/helm_inspect.md deleted file mode 100644 index e46b3dbf45e..00000000000 --- a/docs/helm/helm_inspect.md +++ /dev/null @@ -1,50 +0,0 @@ -## helm inspect - -inspect a chart - -### Synopsis - - - -This command inspects a chart and displays information. It takes a chart reference -('stable/drupal'), a full path to a directory or packaged chart, or a URL. - -Inspect prints the contents of the Chart.yaml file and the values.yaml file. - - -``` -helm inspect [CHART] -``` - -### Options - -``` - --ca-file string chart repository url where to locate the requested chart - --cert-file string verify certificates of HTTPS-enabled servers using this CA bundle - --key-file string identify HTTPS client using this SSL key file - --keyring string path to the keyring containing public verification keys (default "~/.gnupg/pubring.gpg") - --password string chart repository password where to locate the requested chart - --repo string chart repository url where to locate the requested chart - --username string chart repository username where to locate the requested chart - --verify verify the provenance data for this chart - --version string version of the chart. By default, the newest chart is shown -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm](helm.md) - The Helm package manager for Kubernetes. -* [helm inspect chart](helm_inspect_chart.md) - shows inspect chart -* [helm inspect readme](helm_inspect_readme.md) - shows inspect readme -* [helm inspect values](helm_inspect_values.md) - shows inspect values - -###### Auto generated by spf13/cobra on 14-Mar-2018 diff --git a/docs/helm/helm_inspect_chart.md b/docs/helm/helm_inspect_chart.md deleted file mode 100644 index cd1328b594d..00000000000 --- a/docs/helm/helm_inspect_chart.md +++ /dev/null @@ -1,45 +0,0 @@ -## helm inspect chart - -shows inspect chart - -### Synopsis - - - -This command inspects a chart (directory, file, or URL) and displays the contents -of the Charts.yaml file - - -``` -helm inspect chart [CHART] -``` - -### Options - -``` - --ca-file string chart repository url where to locate the requested chart - --cert-file string verify certificates of HTTPS-enabled servers using this CA bundle - --key-file string identify HTTPS client using this SSL key file - --keyring string path to the keyring containing public verification keys (default "~/.gnupg/pubring.gpg") - --password string chart repository password where to locate the requested chart - --repo string chart repository url where to locate the requested chart - --username string chart repository username where to locate the requested chart - --verify verify the provenance data for this chart - --version string version of the chart. By default, the newest chart is shown -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm inspect](helm_inspect.md) - inspect a chart - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_inspect_readme.md b/docs/helm/helm_inspect_readme.md deleted file mode 100644 index 9dd9ebd43b8..00000000000 --- a/docs/helm/helm_inspect_readme.md +++ /dev/null @@ -1,43 +0,0 @@ -## helm inspect readme - -shows inspect readme - -### Synopsis - - - -This command inspects a chart (directory, file, or URL) and displays the contents -of the README file - - -``` -helm inspect readme [CHART] -``` - -### Options - -``` - --ca-file string chart repository url where to locate the requested chart - --cert-file string verify certificates of HTTPS-enabled servers using this CA bundle - --key-file string identify HTTPS client using this SSL key file - --keyring string path to the keyring containing public verification keys (default "~/.gnupg/pubring.gpg") - --repo string chart repository url where to locate the requested chart - --verify verify the provenance data for this chart - --version string version of the chart. By default, the newest chart is shown -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm inspect](helm_inspect.md) - inspect a chart - -###### Auto generated by spf13/cobra on 14-Mar-2018 diff --git a/docs/helm/helm_inspect_values.md b/docs/helm/helm_inspect_values.md deleted file mode 100644 index 6a907cc7dd7..00000000000 --- a/docs/helm/helm_inspect_values.md +++ /dev/null @@ -1,45 +0,0 @@ -## helm inspect values - -shows inspect values - -### Synopsis - - - -This command inspects a chart (directory, file, or URL) and displays the contents -of the values.yaml file - - -``` -helm inspect values [CHART] -``` - -### Options - -``` - --ca-file string chart repository url where to locate the requested chart - --cert-file string verify certificates of HTTPS-enabled servers using this CA bundle - --key-file string identify HTTPS client using this SSL key file - --keyring string path to the keyring containing public verification keys (default "~/.gnupg/pubring.gpg") - --password string chart repository password where to locate the requested chart - --repo string chart repository url where to locate the requested chart - --username string chart repository username where to locate the requested chart - --verify verify the provenance data for this chart - --version string version of the chart. By default, the newest chart is shown -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm inspect](helm_inspect.md) - inspect a chart - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_install.md b/docs/helm/helm_install.md deleted file mode 100644 index 25ccea1bd03..00000000000 --- a/docs/helm/helm_install.md +++ /dev/null @@ -1,120 +0,0 @@ -## helm install - -install a chart archive - -### Synopsis - - - -This command installs a chart archive. - -The install argument must be a chart reference, a path to a packaged chart, -a path to an unpacked chart directory or a URL. - -To override values in a chart, use either the '--values' flag and pass in a file -or use the '--set' flag and pass configuration from the command line, to force -a string value use '--set-string'. - - $ helm install -f myvalues.yaml ./redis - -or - - $ helm install --set name=prod ./redis - -or - - $ helm install --set-string long_int=1234567890 ./redis - -You can specify the '--values'/'-f' flag multiple times. The priority will be given to the -last (right-most) file specified. For example, if both myvalues.yaml and override.yaml -contained a key called 'Test', the value set in override.yaml would take precedence: - - $ helm install -f myvalues.yaml -f override.yaml ./redis - -You can specify the '--set' flag multiple times. The priority will be given to the -last (right-most) set specified. For example, if both 'bar' and 'newbar' values are -set for a key called 'foo', the 'newbar' value would take precedence: - - $ helm install --set foo=bar --set foo=newbar ./redis - - -To check the generated manifests of a release without installing the chart, -the '--debug' and '--dry-run' flags can be combined. This will still require a -round-trip to the Tiller server. - -If --verify is set, the chart MUST have a provenance file, and the provenance -file MUST pass all verification steps. - -There are five different ways you can express the chart you want to install: - -1. By chart reference: helm install stable/mariadb -2. By path to a packaged chart: helm install ./nginx-1.2.3.tgz -3. By path to an unpacked chart directory: helm install ./nginx -4. By absolute URL: helm install https://example.com/charts/nginx-1.2.3.tgz -5. By chart reference and repo url: helm install --repo https://example.com/charts/ nginx - -CHART REFERENCES - -A chart reference is a convenient way of reference a chart in a chart repository. - -When you use a chart reference with a repo prefix ('stable/mariadb'), Helm will look in the local -configuration for a chart repository named 'stable', and will then look for a -chart in that repository whose name is 'mariadb'. It will install the latest -version of that chart unless you also supply a version number with the -'--version' flag. - -To see the list of chart repositories, use 'helm repo list'. To search for -charts in a repository, use 'helm search'. - - -``` -helm install [CHART] -``` - -### Options - -``` - --ca-file string verify certificates of HTTPS-enabled servers using this CA bundle - --cert-file string identify HTTPS client using this SSL certificate file - --dep-up run helm dependency update before installing the chart - --devel use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored. - --dry-run simulate an install - --key-file string identify HTTPS client using this SSL key file - --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. Defaults to the current kube config namespace. - --no-hooks prevent hooks from running during install - --password string chart repository password where to locate the requested chart - --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 - --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --timeout int time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks) (default 300) - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote - --username string chart repository username where to locate the requested chart - -f, --values valueFiles specify values in a YAML file or a URL(can specify multiple) (default []) - --verify verify the package before installing it - --version string specify the exact chart version to install. If this is not specified, the latest version is installed - --wait if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 20-Mar-2018 diff --git a/docs/helm/helm_lint.md b/docs/helm/helm_lint.md deleted file mode 100644 index 596edf2bb68..00000000000 --- a/docs/helm/helm_lint.md +++ /dev/null @@ -1,45 +0,0 @@ -## helm lint - -examines a chart for possible issues - -### Synopsis - - - -This command takes a path to a chart and runs a series of tests to verify that -the chart is well-formed. - -If the linter encounters things that will cause the chart to fail installation, -it will emit [ERROR] messages. If it encounters issues that break with convention -or recommendation, it will emit [WARNING] messages. - - -``` -helm lint [flags] PATH -``` - -### Options - -``` - --namespace string namespace to install the release into (only used if --install is set) (default "default") - --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --strict fail on lint warnings - -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 "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 9-Mar-2018 diff --git a/docs/helm/helm_list.md b/docs/helm/helm_list.md deleted file mode 100755 index 1d5bf7ea2a1..00000000000 --- a/docs/helm/helm_list.md +++ /dev/null @@ -1,76 +0,0 @@ -## helm list - -list releases - -### Synopsis - - - -This command lists all of the releases. - -By default, it lists only releases that are deployed or failed. Flags like -'--deleted' and '--all' will alter this behavior. Such flags can be combined: -'--deleted --failed'. - -By default, items are sorted alphabetically. Use the '-d' flag to sort by -release date. - -If an argument is provided, it will be treated as a filter. Filters are -regular expressions (Perl compatible) that are applied to the list of releases. -Only items that match the filter will be returned. - - $ helm list 'ara[a-z]+' - NAME UPDATED CHART - maudlin-arachnid Mon May 9 16:07:08 2016 alpine-0.1.0 - -If no results are found, 'helm list' will exit 0, but with no output (or in -the case of no '-q' flag, only headers). - -By default, up to 256 items may be returned. To limit this, use the '--max' flag. -Setting '--max' to 0 will not return all results. Rather, it will return the -server's default, which may be much higher than 256. Pairing the '--max' -flag with the '--offset' flag allows you to page through results. - - -``` -helm list [flags] [FILTER] -``` - -### Options - -``` - -a, --all show all releases, not just the ones marked DEPLOYED - --col-width uint specifies the max column width of output (default 60) - -d, --date sort by release date - --deleted show deleted releases - --deleting show releases that are currently being deleted - --deployed show deployed releases. If no other is specified, this will be automatically enabled - --failed show failed releases - -m, --max int maximum number of releases to fetch (default 256) - --namespace string show releases within a specific namespace - -o, --offset string next release name in the list, used to offset from start value - --pending show pending releases - -r, --reverse reverse the sort order - -q, --short output short (quiet) listing format - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_package.md b/docs/helm/helm_package.md deleted file mode 100644 index 957f9c3b293..00000000000 --- a/docs/helm/helm_package.md +++ /dev/null @@ -1,53 +0,0 @@ -## helm package - -package a chart directory into a chart archive - -### Synopsis - - - -This command packages a chart into a versioned chart archive file. If a path -is given, this will look at that path for a chart (which must contain a -Chart.yaml file) and then package that directory. - -If no path is given, this will look in the present working directory for a -Chart.yaml file, and (if found) build the current directory into a chart. - -Versioned chart archives are used by Helm package repositories. - - -``` -helm package [flags] [CHART_PATH] [...] -``` - -### Options - -``` - --app-version string set the appVersion on the chart to this version - -u, --dependency-update update dependencies from "requirements.yaml" to dir "charts/" before packaging - -d, --destination string location to write the chart. (default ".") - --key string name of the key to use when signing. Used if --sign is true - --keyring string location of a public keyring (default "~/.gnupg/pubring.gpg") - --save save packaged chart to local chart repository (default true) - --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --sign use a PGP private key to sign this package - -f, --values valueFiles specify values in a YAML file or a URL(can specify multiple) (default []) - --version string set the version on the chart to this semver version -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 6-Apr-2018 diff --git a/docs/helm/helm_plugin.md b/docs/helm/helm_plugin.md deleted file mode 100644 index cc42aa4dcae..00000000000 --- a/docs/helm/helm_plugin.md +++ /dev/null @@ -1,30 +0,0 @@ -## helm plugin - -add, list, or remove Helm plugins - -### Synopsis - - - -Manage client-side Helm plugins. - - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm](helm.md) - The Helm package manager for Kubernetes. -* [helm plugin install](helm_plugin_install.md) - install one or more Helm plugins -* [helm plugin list](helm_plugin_list.md) - list installed Helm plugins -* [helm plugin remove](helm_plugin_remove.md) - remove one or more Helm plugins -* [helm plugin update](helm_plugin_update.md) - update one or more Helm plugins - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_plugin_install.md b/docs/helm/helm_plugin_install.md deleted file mode 100644 index 196ca97dda5..00000000000 --- a/docs/helm/helm_plugin_install.md +++ /dev/null @@ -1,39 +0,0 @@ -## helm plugin install - -install one or more Helm plugins - -### Synopsis - - - -This command allows you to install a plugin from a url to a VCS repo or a local path. - -Example usage: - $ helm plugin install https://github.com/technosophos/helm-template - - -``` -helm plugin install [options] ... -``` - -### Options - -``` - --version string specify a version constraint. If this is not specified, the latest version is installed -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm plugin](helm_plugin.md) - add, list, or remove Helm plugins - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_plugin_list.md b/docs/helm/helm_plugin_list.md deleted file mode 100644 index ddfd04ee610..00000000000 --- a/docs/helm/helm_plugin_list.md +++ /dev/null @@ -1,28 +0,0 @@ -## helm plugin list - -list installed Helm plugins - -### Synopsis - - -list installed Helm plugins - -``` -helm plugin list -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm plugin](helm_plugin.md) - add, list, or remove Helm plugins - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_plugin_remove.md b/docs/helm/helm_plugin_remove.md deleted file mode 100644 index 8543a367a99..00000000000 --- a/docs/helm/helm_plugin_remove.md +++ /dev/null @@ -1,28 +0,0 @@ -## helm plugin remove - -remove one or more Helm plugins - -### Synopsis - - -remove one or more Helm plugins - -``` -helm plugin remove ... -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm plugin](helm_plugin.md) - add, list, or remove Helm plugins - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_plugin_update.md b/docs/helm/helm_plugin_update.md deleted file mode 100644 index 9e5e205f090..00000000000 --- a/docs/helm/helm_plugin_update.md +++ /dev/null @@ -1,28 +0,0 @@ -## helm plugin update - -update one or more Helm plugins - -### Synopsis - - -update one or more Helm plugins - -``` -helm plugin update ... -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm plugin](helm_plugin.md) - add, list, or remove Helm plugins - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_repo.md b/docs/helm/helm_repo.md deleted file mode 100644 index 4109ceca438..00000000000 --- a/docs/helm/helm_repo.md +++ /dev/null @@ -1,35 +0,0 @@ -## helm repo - -add, list, remove, update, and index chart repositories - -### Synopsis - - - -This command consists of multiple subcommands to interact with chart repositories. - -It can be used to add, remove, list, and index chart repositories. -Example usage: - $ helm repo add [NAME] [REPO_URL] - - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm](helm.md) - The Helm package manager for Kubernetes. -* [helm repo add](helm_repo_add.md) - add a chart repository -* [helm repo index](helm_repo_index.md) - generate an index file given a directory containing packaged charts -* [helm repo list](helm_repo_list.md) - list chart repositories -* [helm repo remove](helm_repo_remove.md) - remove a chart repository -* [helm repo update](helm_repo_update.md) - update information of available charts locally from chart repositories - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_repo_add.md b/docs/helm/helm_repo_add.md deleted file mode 100644 index 456ffa27ebd..00000000000 --- a/docs/helm/helm_repo_add.md +++ /dev/null @@ -1,39 +0,0 @@ -## helm repo add - -add a chart repository - -### Synopsis - - -add a chart repository - -``` -helm repo add [flags] [NAME] [URL] -``` - -### Options - -``` - --ca-file string verify certificates of HTTPS-enabled servers using this CA bundle - --cert-file string identify HTTPS client using this SSL certificate file - --key-file string identify HTTPS client using this SSL key file - --no-update raise error if repo is already registered - --password string chart repository password - --username string chart repository username -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm repo](helm_repo.md) - add, list, remove, update, and index chart repositories - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_repo_index.md b/docs/helm/helm_repo_index.md deleted file mode 100644 index 14b412b2918..00000000000 --- a/docs/helm/helm_repo_index.md +++ /dev/null @@ -1,44 +0,0 @@ -## helm repo index - -generate an index file given a directory containing packaged charts - -### Synopsis - - - -Read the current directory and generate an index file based on the charts found. - -This tool is used for creating an 'index.yaml' file for a chart repository. To -set an absolute URL to the charts, use '--url' flag. - -To merge the generated index with an existing index file, use the '--merge' -flag. In this case, the charts found in the current directory will be merged -into the existing index, with local charts taking priority over existing charts. - - -``` -helm repo index [flags] [DIR] -``` - -### Options - -``` - --merge string merge the generated index into the given index - --url string url of chart repository -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm repo](helm_repo.md) - add, list, remove, update, and index chart repositories - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_repo_list.md b/docs/helm/helm_repo_list.md deleted file mode 100644 index 858ef957f7e..00000000000 --- a/docs/helm/helm_repo_list.md +++ /dev/null @@ -1,28 +0,0 @@ -## helm repo list - -list chart repositories - -### Synopsis - - -list chart repositories - -``` -helm repo list [flags] -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm repo](helm_repo.md) - add, list, remove, update, and index chart repositories - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_repo_remove.md b/docs/helm/helm_repo_remove.md deleted file mode 100644 index 801bc3c3fea..00000000000 --- a/docs/helm/helm_repo_remove.md +++ /dev/null @@ -1,28 +0,0 @@ -## helm repo remove - -remove a chart repository - -### Synopsis - - -remove a chart repository - -``` -helm repo remove [flags] [NAME] -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm repo](helm_repo.md) - add, list, remove, update, and index chart repositories - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_repo_update.md b/docs/helm/helm_repo_update.md deleted file mode 100644 index 897ed24b7f3..00000000000 --- a/docs/helm/helm_repo_update.md +++ /dev/null @@ -1,34 +0,0 @@ -## helm repo update - -update information of available charts locally from chart repositories - -### Synopsis - - - -Update gets the latest information about charts from the respective chart repositories. -Information is cached locally, where it is used by commands like 'helm search'. - -'helm update' is the deprecated form of 'helm repo update'. It will be removed in -future releases. - - -``` -helm repo update -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --tiller-namespace string namespace of Tiller (default "kube-system") -``` - -### SEE ALSO -* [helm repo](helm_repo.md) - add, list, remove, update, and index chart repositories - -###### Auto generated by spf13/cobra on 8-Mar-2018 diff --git a/docs/helm/helm_reset.md b/docs/helm/helm_reset.md deleted file mode 100644 index ed68b1b8476..00000000000 --- a/docs/helm/helm_reset.md +++ /dev/null @@ -1,44 +0,0 @@ -## helm reset - -uninstalls Tiller from a cluster - -### Synopsis - - - -This command uninstalls Tiller (the Helm server-side component) from your -Kubernetes Cluster and optionally deletes local configuration in -$HELM_HOME (default ~/.helm/) - - -``` -helm reset -``` - -### Options - -``` - -f, --force forces Tiller uninstall even if there are releases installed, or if Tiller is not in ready state. Releases are not deleted.) - --remove-helm-home if set deletes $HELM_HOME - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_rollback.md b/docs/helm/helm_rollback.md deleted file mode 100644 index 4b6dcbbb2f0..00000000000 --- a/docs/helm/helm_rollback.md +++ /dev/null @@ -1,50 +0,0 @@ -## helm rollback - -roll back a release to a previous revision - -### Synopsis - - - -This command rolls back a release to a previous revision. - -The first argument of the rollback command is the name of a release, and the -second is a revision (version) number. To see revision numbers, run -'helm history RELEASE'. - - -``` -helm rollback [flags] [RELEASE] [REVISION] -``` - -### Options - -``` - --dry-run simulate a rollback - --force force resource update through delete/recreate if needed - --no-hooks prevent hooks from running during rollback - --recreate-pods performs pods restart for the resource if applicable - --timeout int time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks) (default 300) - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote - --wait if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_search.md b/docs/helm/helm_search.md deleted file mode 100644 index f59814b9a54..00000000000 --- a/docs/helm/helm_search.md +++ /dev/null @@ -1,41 +0,0 @@ -## helm search - -search for a keyword in charts - -### Synopsis - - - -Search reads through all of the repositories configured on the system, and -looks for matches. - -Repositories are managed with 'helm repo' commands. - - -``` -helm search [keyword] -``` - -### Options - -``` - -r, --regexp use regular expressions for searching - -v, --version string search using semantic versioning constraints - -l, --versions show the long listing, with each version of each chart on its own line -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_serve.md b/docs/helm/helm_serve.md deleted file mode 100644 index 90ebb6da9aa..00000000000 --- a/docs/helm/helm_serve.md +++ /dev/null @@ -1,49 +0,0 @@ -## helm serve - -start a local http web server - -### Synopsis - - - -This command starts a local chart repository server that serves charts from a local directory. - -The new server will provide HTTP access to a repository. By default, it will -scan all of the charts in '$HELM_HOME/repository/local' and serve those over -the local IPv4 TCP port (default '127.0.0.1:8879'). - -This command is intended to be used for educational and testing purposes only. -It is best to rely on a dedicated web server or a cloud-hosted solution like -Google Cloud Storage for production use. - -See https://github.com/kubernetes/helm/blob/master/docs/chart_repository.md#hosting-chart-repositories -for more information on hosting chart repositories in a production setting. - - -``` -helm serve -``` - -### Options - -``` - --address string address to listen on (default "127.0.0.1:8879") - --repo-path string local directory path from which to serve charts - --url string external URL of chart repository -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_status.md b/docs/helm/helm_status.md deleted file mode 100644 index 02ec0ad6650..00000000000 --- a/docs/helm/helm_status.md +++ /dev/null @@ -1,49 +0,0 @@ -## helm status - -displays the status of the named release - -### Synopsis - - - -This command shows the status of a named release. -The status consists of: -- last deployment time -- k8s namespace in which the release lives -- state of the release (can be: UNKNOWN, DEPLOYED, DELETED, SUPERSEDED, FAILED or DELETING) -- list of resources that this release consists of, sorted by kind -- details on last test suite run, if applicable -- additional notes provided by the chart - - -``` -helm status [flags] RELEASE_NAME -``` - -### Options - -``` - -o, --output string output the status in the specified format (json or yaml) - --revision int32 if set, display the status of the named release with revision - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_template.md b/docs/helm/helm_template.md deleted file mode 100644 index 3a4e9ce4a10..00000000000 --- a/docs/helm/helm_template.md +++ /dev/null @@ -1,54 +0,0 @@ -## 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 - --kube-version string kubernetes version used as Capabilities.KubeVersion.Major/Minor (default "1.9") - -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 - --output-dir string writes the executed templates to files in output-dir instead of stdout - --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --set-string stringArray set STRING 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 "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 9-Mar-2018 diff --git a/docs/helm/helm_test.md b/docs/helm/helm_test.md deleted file mode 100644 index 062244e73e4..00000000000 --- a/docs/helm/helm_test.md +++ /dev/null @@ -1,45 +0,0 @@ -## helm test - -test a release - -### Synopsis - - - -The test command runs the tests for a release. - -The argument this command takes is the name of a deployed release. -The tests to be run are defined in the chart that was installed. - - -``` -helm test [RELEASE] -``` - -### Options - -``` - --cleanup delete test pods upon completion - --timeout int time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks) (default 300) - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_upgrade.md b/docs/helm/helm_upgrade.md deleted file mode 100644 index c2882265e05..00000000000 --- a/docs/helm/helm_upgrade.md +++ /dev/null @@ -1,84 +0,0 @@ -## helm upgrade - -upgrade a release - -### Synopsis - - - -This command upgrades a release to a new version of a chart. - -The upgrade arguments must be a release and chart. The chart -argument can be either: a chart reference('stable/mariadb'), a path to a chart directory, -a packaged chart, or a fully qualified URL. For chart references, the latest -version will be specified unless the '--version' flag is set. - -To override values in a chart, use either the '--values' flag and pass in a file -or use the '--set' flag and pass configuration from the command line, to force string -values, use '--set-string'. - -You can specify the '--values'/'-f' flag multiple times. The priority will be given to the -last (right-most) file specified. For example, if both myvalues.yaml and override.yaml -contained a key called 'Test', the value set in override.yaml would take precedence: - - $ helm upgrade -f myvalues.yaml -f override.yaml redis ./redis - -You can specify the '--set' flag multiple times. The priority will be given to the -last (right-most) set specified. For example, if both 'bar' and 'newbar' values are -set for a key called 'foo', the 'newbar' value would take precedence: - - $ helm upgrade --set foo=bar --set foo=newbar redis ./redis - - -``` -helm upgrade [RELEASE] [CHART] -``` - -### Options - -``` - --ca-file string verify certificates of HTTPS-enabled servers using this CA bundle - --cert-file string identify HTTPS client using this SSL certificate file - --devel use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored. - --dry-run simulate an upgrade - --force force resource update through delete/recreate if needed - -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). Defaults to the current kube config namespace - --no-hooks disable pre/post upgrade hooks - --password string chart repository password where to locate the requested chart - --recreate-pods performs pods restart for the resource if applicable - --repo string chart repository url where to locate the requested chart - --reset-values when upgrading, reset the values to the ones built into the chart - --reuse-values when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored. - --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --timeout int time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks) (default 300) - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote - --username string chart repository username where to locate the requested chart - -f, --values valueFiles specify values in a YAML file or a URL(can specify multiple) (default []) - --verify verify the provenance of the chart before upgrading - --version string specify the exact chart version to use. If this is not specified, the latest version is used - --wait if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 4-Apr-2018 diff --git a/docs/helm/helm_verify.md b/docs/helm/helm_verify.md deleted file mode 100644 index bc53439377e..00000000000 --- a/docs/helm/helm_verify.md +++ /dev/null @@ -1,43 +0,0 @@ -## helm verify - -verify that a chart at the given path has been signed and is valid - -### Synopsis - - - -Verify that the given chart has a valid provenance file. - -Provenance files provide crytographic verification that a chart has not been -tampered with, and was packaged by a trusted provider. - -This command can be used to verify a local chart. Several other commands provide -'--verify' flags that run the same validation. To generate a signed package, use -the 'helm package --sign' command. - - -``` -helm verify [flags] PATH -``` - -### Options - -``` - --keyring string keyring containing public keys (default "~/.gnupg/pubring.gpg") -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/helm/helm_version.md b/docs/helm/helm_version.md deleted file mode 100644 index 1f48cceba5f..00000000000 --- a/docs/helm/helm_version.md +++ /dev/null @@ -1,58 +0,0 @@ -## helm version - -print the client/server version information - -### Synopsis - - - -Show the client and server versions for Helm and tiller. - -This will print a representation of the client and server versions of Helm and -Tiller. The output will look something like this: - -Client: &version.Version{SemVer:"v2.0.0", GitCommit:"ff52399e51bb880526e9cd0ed8386f6433b74da1", GitTreeState:"clean"} -Server: &version.Version{SemVer:"v2.0.0", GitCommit:"b0c113dfb9f612a9add796549da66c0d294508a3", GitTreeState:"clean"} - -- SemVer is the semantic version of the release. -- GitCommit is the SHA for the commit that this version was built from. -- GitTreeState is "clean" if there are no local code changes when this binary was - built, and "dirty" if the binary was built from locally modified code. - -To print just the client version, use '--client'. To print just the server version, -use '--server'. - - -``` -helm version -``` - -### Options - -``` - -c, --client client version only - -s, --server server version only - --short print the version number - --template string template for version string format - --tls enable TLS for request - --tls-ca-cert string path to TLS CA certificate file (default "$HELM_HOME/ca.pem") - --tls-cert string path to TLS certificate file (default "$HELM_HOME/cert.pem") - --tls-key string path to TLS key file (default "$HELM_HOME/key.pem") - --tls-verify enable TLS for request and verify remote -``` - -### Options inherited from parent commands - -``` - --debug enable verbose output - --home string location of your Helm config. Overrides $HELM_HOME (default "~/.helm") - --host string address of Tiller. Overrides $HELM_HOST - --kube-context string name of the kubeconfig context to use - --tiller-connection-timeout int the duration (in seconds) Helm will wait to establish a connection to tiller (default 300) - --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 8-Mar-2018 diff --git a/docs/history.md b/docs/history.md deleted file mode 100644 index 71e63c6b29d..00000000000 --- a/docs/history.md +++ /dev/null @@ -1,29 +0,0 @@ -## The History of the Project - -Kubernetes Helm is the merged result of [Helm -Classic](https://github.com/helm/helm) and the Kubernetes port of GCS Deployment -Manager. The project was jointly started by Google and Deis, though it -is now part of the CNCF. Many companies now contribute regularly to Helm. - -Differences from Helm Classic: - -- Helm now has both a client (`helm`) and a server (`tiller`). The - server runs inside of Kubernetes, and manages your resources. -- Helm's chart format has changed for the better: - - Dependencies are immutable and stored inside of a chart's `charts/` - directory. - - Charts are strongly versioned using [SemVer 2](http://semver.org/spec/v2.0.0.html) - - Charts can be loaded from directories or from chart archive files - - Helm supports Go templates without requiring you to run `generate` - or `template` commands. - - Helm makes it easy to configure your releases -- and share the - configuration with the rest of your team. -- Helm chart repositories now use plain HTTP(S) instead of Git/GitHub. - There is no longer any GitHub dependency. - - A chart server is a simple HTTP server - - Charts are referenced by version - - The `helm serve` command will run a local chart server, though you - can easily use object storage (S3, GCS) or a regular web server. - - And you can still load charts from a local directory. -- The Helm workspace is gone. You can now work anywhere on your - filesystem that you want to work. diff --git a/docs/images/create-a-bucket.png b/docs/images/create-a-bucket.png deleted file mode 100644 index f7f5dbfdc32..00000000000 Binary files a/docs/images/create-a-bucket.png and /dev/null differ diff --git a/docs/images/create-a-gh-page-button.png b/docs/images/create-a-gh-page-button.png deleted file mode 100644 index b9d43a7059b..00000000000 Binary files a/docs/images/create-a-gh-page-button.png and /dev/null differ diff --git a/docs/images/edit-permissions.png b/docs/images/edit-permissions.png deleted file mode 100644 index b1e39f4b204..00000000000 Binary files a/docs/images/edit-permissions.png and /dev/null differ diff --git a/docs/images/make-bucket-public.png b/docs/images/make-bucket-public.png deleted file mode 100644 index f8a5e17f02b..00000000000 Binary files a/docs/images/make-bucket-public.png and /dev/null differ diff --git a/docs/images/nothing.png b/docs/images/nothing.png deleted file mode 100644 index f41c11bf3e1..00000000000 Binary files a/docs/images/nothing.png and /dev/null differ diff --git a/docs/images/set-a-gh-page.png b/docs/images/set-a-gh-page.png deleted file mode 100644 index ee9dccd6935..00000000000 Binary files a/docs/images/set-a-gh-page.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 4ca93bd1f34..00000000000 --- a/docs/index.md +++ /dev/null @@ -1,37 +0,0 @@ -# Helm Documentation - -- [Quick Start](quickstart.md) - Read me first! -- [Installing Helm](install.md) - Install Helm and Tiller - - [Kubernetes Distribution Notes](kubernetes_distros.md) - - [Frequently Asked Questions](install_faq.md) -- [Using Helm](using_helm.md) - Learn the Helm tools - - [Plugins](plugins.md) - - [Role-based Access Control](rbac.md) - - [TLS/SSL for Helm and Tiller](tiller_ssl.md) - Use Helm-to-Tiller encryption -- [Developing Charts](charts.md) - An introduction to chart development - - [Chart Lifecycle Hooks](charts_hooks.md) - - [Chart Tips and Tricks](charts_tips_and_tricks.md) - - [Chart Repository Guide](chart_repository.md) - - [Syncing your Chart Repository](chart_repository_sync_example.md) - - [Signing Charts](provenance.md) - - [Writing Tests for Charts](chart_tests.md) -- [Chart Template Developer's Guide](chart_template_guide/index.md) - Master Helm templates - - [Getting Started with Templates](chart_template_guide/getting_started.md) - - [Built-in Objects](chart_template_guide/builtin_objects.md) - - [Values Files](chart_template_guide/values_files.md) - - [Functions and Pipelines](chart_template_guide/functions_and_pipelines.md) - - [Flow Control (if/else, with, range, whitespace management)](chart_template_guide/control_structures.md) - - [Variables](chart_template_guide/variables.md) - - [Named Templates (Partials)](chart_template_guide/named_templates.md) - - [Accessing Files Inside Templates](chart_template_guide/accessing_files.md) - - [Creating a NOTES.txt File](chart_template_guide/notes_files.md) - - [Subcharts and Global Values](chart_template_guide/subcharts_and_globals.md) - - [Debugging Templates](chart_template_guide/debugging.md) - - [Wrapping Up](chart_template_guide/wrapping_up.md) - - [Appendix A: YAML Techniques](chart_template_guide/yaml_techniques.md) - - [Appendix B: Go Data Types](chart_template_guide/data_types.md) -- [Related Projects](related.md) - More Helm tools, articles, and plugins -- [Architecture](architecture.md) - Overview of the Helm/Tiller design -- [Developers](developers.md) - About the developers -- [History](history.md) - A brief history of the project -- [Glossary](glossary.md) - Decode the Helm vocabulary diff --git a/docs/install.md b/docs/install.md deleted file mode 100755 index 17905a805f3..00000000000 --- a/docs/install.md +++ /dev/null @@ -1,356 +0,0 @@ -# Installing Helm - -There are two parts to Helm: The Helm client (`helm`) and the Helm -server (Tiller). This guide shows how to install the client, and then -proceeds to show two ways to install the server. - -**IMPORTANT**: If you are responsible for ensuring your cluster is a controlled environment, especially when resources are shared, it is strongly recommended installing Tiller using a secured configuration. For guidance, see [Securing your Helm Installation](securing_installation.md). - -## Installing the Helm Client - -The Helm client can be installed either from source, or from pre-built binary -releases. - -### From the Binary Releases - -Every [release](https://github.com/kubernetes/helm/releases) of Helm -provides binary releases for a variety of OSes. These binary versions -can be manually downloaded and installed. - -1. Download your [desired version](https://github.com/kubernetes/helm/releases) -2. Unpack it (`tar -zxvf helm-v2.0.0-linux-amd64.tgz`) -3. Find the `helm` binary in the unpacked directory, and move it to its - desired destination (`mv linux-amd64/helm /usr/local/bin/helm`) - -From there, you should be able to run the client: `helm help`. - -### From Homebrew (macOS) - -Members of the Kubernetes community have contributed a Helm formula build to -Homebrew. This formula is generally up to date. - -``` -brew install kubernetes-helm -``` - -(Note: There is also a formula for emacs-helm, which is a different -project.) - -### From Chocolatey (Windows) - -Members of the Kubernetes community have contributed a [Helm package](https://chocolatey.org/packages/kubernetes-helm) build to -[Chocolatey](https://chocolatey.org/). This package is generally up to date. - -``` -choco install kubernetes-helm -``` - -## From Script - -Helm now has an installer script that will automatically grab the latest version -of the Helm client and [install it locally](https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get). - -You can fetch that script, and then execute it locally. It's well documented so -that you can read through it and understand what it is doing before you run it. - -``` -$ curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh -$ chmod 700 get_helm.sh -$ ./get_helm.sh -``` - -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 - -"Canary" builds are versions of the Helm software that are built from -the latest master branch. They are not official releases, and may not be -stable. However, they offer the opportunity to test the cutting edge -features. - -Canary Helm binaries are stored in the [Kubernetes Helm GCS bucket](https://kubernetes-helm.storage.googleapis.com). -Here are links to the common builds: - -- [Linux AMD64](https://kubernetes-helm.storage.googleapis.com/helm-canary-linux-amd64.tar.gz) -- [macOS AMD64](https://kubernetes-helm.storage.googleapis.com/helm-canary-darwin-amd64.tar.gz) -- [Experimental Windows AMD64](https://kubernetes-helm.storage.googleapis.com/helm-canary-windows-amd64.zip) - -### From Source (Linux, macOS) - -Building Helm from source is slightly more work, but is the best way to -go if you want to test the latest (pre-release) Helm version. - -You must have a working Go environment with -[glide](https://github.com/Masterminds/glide) installed. - -```console -$ cd $GOPATH -$ mkdir -p src/k8s.io -$ cd src/k8s.io -$ git clone https://github.com/kubernetes/helm.git -$ cd helm -$ make bootstrap build -``` - -The `bootstrap` target will attempt to install dependencies, rebuild the -`vendor/` tree, and validate configuration. - -The `build` target will compile `helm` and place it in `bin/helm`. -Tiller is also compiled, and is placed in `bin/tiller`. - -## Installing Tiller - -Tiller, the server portion of Helm, typically runs inside of your -Kubernetes cluster. But for development, it can also be run locally, and -configured to talk to a remote Kubernetes cluster. - -### Easy In-Cluster Installation - -The easiest way to install `tiller` into the cluster is simply to run -`helm init`. This will validate that `helm`'s local environment is set -up correctly (and set it up if necessary). Then it will connect to -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 pods --namespace -kube-system` and see Tiller running. - -You can explicitly tell `helm init` to... - -- Install the canary build with the `--canary-image` flag -- Install a particular image (version) with `--tiller-image` -- Install to a particular cluster with `--kube-context` -- Install into a particular namespace with `--tiller-namespace` - -Once Tiller is installed, running `helm version` should show you both -the client and server version. (If it shows only the client version, -`helm` cannot yet connect to the server. Use `kubectl` to see if any -`tiller` pods are running.) - -Helm will look for Tiller in the `kube-system` namespace unless -`--tiller-namespace` or `TILLER_NAMESPACE` is set. - -### Installing Tiller Canary Builds - -Canary images are built from the `master` branch. They may not be -stable, but they offer you the chance to test out the latest features. - -The easiest way to install a canary image is to use `helm init` with the -`--canary-image` flag: - -```console -$ helm init --canary-image -``` - -This will use the most recently built container image. You can always -uninstall Tiller by deleting the Tiller deployment from the -`kube-system` namespace using `kubectl`. - -### Running Tiller Locally - -For development, it is sometimes easier to work on Tiller locally, and -configure it to connect to a remote Kubernetes cluster. - -The process of building Tiller is explained above. - -Once `tiller` has been built, simply start it: - -```console -$ bin/tiller -Tiller running on :44134 -``` - -When Tiller is running locally, it will attempt to connect to the -Kubernetes cluster that is configured by `kubectl`. (Run `kubectl config -view` to see which cluster that is.) - -You must tell `helm` to connect to this new local Tiller host instead of -connecting to the one in-cluster. There are two ways to do this. The -first is to specify the `--host` option on the command line. The second -is to set the `$HELM_HOST` environment variable. - -```console -$ export HELM_HOST=localhost:44134 -$ helm version # Should connect to localhost. -Client: &version.Version{SemVer:"v2.0.0-alpha.4", GitCommit:"db...", GitTreeState:"dirty"} -Server: &version.Version{SemVer:"v2.0.0-alpha.4", GitCommit:"a5...", GitTreeState:"dirty"} -``` - -Importantly, even when running locally, Tiller will store release -configuration in ConfigMaps inside of Kubernetes. - -## Upgrading Tiller - -As of Helm 2.2.0, Tiller can be upgraded using `helm init --upgrade`. - -For older versions of Helm, or for manual upgrades, you can use `kubectl` to modify -the Tiller image: - -```console -$ export TILLER_TAG=v2.0.0-beta.1 # Or whatever version you want -$ kubectl --namespace=kube-system set image deployments/tiller-deploy tiller=gcr.io/kubernetes-helm/tiller:$TILLER_TAG -deployment "tiller-deploy" image updated -``` - -Setting `TILLER_TAG=canary` will get the latest snapshot of master. - -## Deleting or Reinstalling Tiller - -Because Tiller stores its data in Kubernetes ConfigMaps, you can safely -delete and re-install Tiller without worrying about losing any data. The -recommended way of deleting Tiller is with `kubectl delete deployment -tiller-deploy --namespace kube-system`, or more concisely `helm reset`. - -Tiller can then be re-installed from the client with: - -```console -$ helm init -``` - -## Advanced Usage - -`helm init` provides additional flags for modifying Tiller's deployment -manifest before it is installed. - -### Using `--node-selectors` - -The `--node-selectors` flag allows us to specify the node labels required -for scheduling the Tiller pod. - -The example below will create the specified label under the nodeSelector -property. - -``` -helm init --node-selectors "beta.kubernetes.io/os"="linux" -``` - -The installed deployment manifest will contain our node selector label. - -``` -... -spec: - template: - spec: - nodeSelector: - beta.kubernetes.io/os: linux -... -``` - - -### Using `--override` - -`--override` allows you to specify properties of Tiller's -deployment manifest. Unlike the `--set` command used elsewhere in Helm, -`helm init --override` manipulates the specified properties of the final -manifest (there is no "values" file). Therefore you may specify any valid -value for any valid property in the deployment manifest. - -#### Override annotation - -In the example below we use `--override` to add the revision property and set -its value to 1. - -``` -helm init --override metadata.annotations."deployment\.kubernetes\.io/revision"="1" -``` -Output: - -``` -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - annotations: - deployment.kubernetes.io/revision: "1" -... -``` - -#### Override affinity - -In the example below we set properties for node affinity. Multiple -`--override` commands may be combined to modify different properties of the -same list item. - -``` -helm init --override "spec.template.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].weight"="1" --override "spec.template.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].preference.matchExpressions[0].key"="e2e-az-name" -``` - -The specified properties are combined into the -"preferredDuringSchedulingIgnoredDuringExecution" property's first -list item. - -``` -... -spec: - strategy: {} - template: - ... - spec: - affinity: - nodeAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - preference: - matchExpressions: - - key: e2e-az-name - operator: "" - weight: 1 -... -``` - -### Using `--output` - -The `--output` flag allows us skip the installation of Tiller's deployment -manifest and simply output the deployment manifest to stdout in either -JSON or YAML format. The output may then be modified with tools like `jq` -and installed manually with `kubectl`. - -In the example below we execute `helm init` with the `--output json` flag. - -``` -helm init --output json -``` - -The Tiller installation is skipped and the manifest is output to stdout -in JSON format. - -``` -"apiVersion": "extensions/v1beta1", -"kind": "Deployment", -"metadata": { - "creationTimestamp": null, - "labels": { - "app": "helm", - "name": "tiller" - }, - "name": "tiller-deploy", - "namespace": "kube-system" -}, -... -``` - -### Storage backends -By default, `tiller` stores release information in `ConfigMaps` in the namespace -where it is running. As of Helm 2.7.0, there is now a beta storage backend that -uses `Secrets` for storing release information. This was added for additional -security in protecting charts in conjunction with the release of `Secret` -encryption in Kubernetes. - -To enable the secrets backend, you'll need to init Tiller with the following -options: - -```shell -helm init --override 'spec.template.spec.containers[0].command'='{/tiller,--storage=secret}' -``` - -Currently, if you want to switch from the default backend to the secrets -backend, you'll have to do the migration for this on your own. When this backend -graduates from beta, there will be a more official path of migration - -## Conclusion - -In most cases, installation is as simple as getting a pre-built `helm` binary -and running `helm init`. This document covers additional cases for those -who want to do more sophisticated things with Helm. - -Once you have the Helm Client and Tiller successfully installed, you can -move on to using Helm to manage charts. diff --git a/docs/install_faq.md b/docs/install_faq.md deleted file mode 100644 index f2eae5b48b9..00000000000 --- a/docs/install_faq.md +++ /dev/null @@ -1,232 +0,0 @@ -# Installation: Frequently Asked Questions - -This section tracks some of the more frequently encountered issues with installing -or getting started with Helm. - -**We'd love your help** making this document better. To add, correct, or remove -information, [file an issue](https://github.com/kubernetes/helm/issues) or -send us a pull request. - -## Downloading - -I want to know more about my downloading options. - -**Q: I can't get to GitHub releases of the newest Helm. Where are they?** - -A: We no longer use GitHub releases. Binaries are now stored in a -[GCS public bucket](https://kubernetes-helm.storage.googleapis.com). - -**Q: Why aren't there Debian/Fedora/... native packages of Helm?** - -We'd love to provide these or point you toward a trusted provider. If you're -interested in helping, we'd love it. This is how the Homebrew formula was -started. - -**Q: Why do you provide a `curl ...|bash` script?** - -A: There is a script in our repository (`scripts/get`) that can be executed as -a `curl ..|bash` script. The transfers are all protected by HTTPS, and the script -does some auditing of the packages it fetches. However, the script has all the -usual dangers of any shell script. - -We provide it because it is useful, but we suggest that users carefully read the -script first. What we'd really like, though, are better packaged releases of -Helm. - -## Installing - -I'm trying to install Helm/Tiller, but something is not right. - -**Q: How do I put the Helm client files somewhere other than ~/.helm?** - -Set the `$HELM_HOME` environment variable, and then run `helm init`: - -```console -export HELM_HOME=/some/path -helm init --client-only -``` - -Note that if you have existing repositories, you will need to re-add them -with `helm repo add...`. - -**Q: How do I configure Helm, but not install Tiller?** - -A: By default, `helm init` will ensure that the local `$HELM_HOME` is configured, -and then install Tiller on your cluster. To locally configure, but not install -Tiller, use `helm init --client-only`. - -**Q: How do I manually install Tiller on the cluster?** - -A: Tiller is installed as a Kubernetes `deployment`. You can get the manifest -by running `helm init --dry-run --debug`, and then manually install it with -`kubectl`. It is suggested that you do not remove or change the labels on that -deployment, as they are sometimes used by supporting scripts and tools. - -**Q: Why do I get `Error response from daemon: target is unknown` during Tiller install?** - -A: Users have reported being unable to install Tiller on Kubernetes instances that -are using Docker 1.13.0. The root cause of this was a bug in Docker that made -that one version incompatible with images pushed to the Docker registry by -earlier versions of Docker. - -This [issue](https://github.com/docker/docker/issues/30083) was fixed shortly -after the release, and is available in Docker 1.13.1-RC1 and later. - -## Getting Started - -I successfully installed Helm/Tiller but I can't use it. - -**Q: Trying to use Helm, I get the error "client transport was broken"** - -``` -E1014 02:26:32.885226 16143 portforward.go:329] an error occurred forwarding 37008 -> 44134: error forwarding port 44134 to pod tiller-deploy-2117266891-e4lev_kube-system, uid : unable to do port forwarding: socat not found. -2016/10/14 02:26:32 transport: http2Client.notifyError got notified that the client transport was broken EOF. -Error: transport is closing -``` - -A: This is usually a good indication that Kubernetes is not set up to allow port forwarding. - -Typically, the missing piece is `socat`. If you are running CoreOS, we have been -told that it may have been misconfigured on installation. The CoreOS team -recommends reading this: - -- https://coreos.com/kubernetes/docs/latest/kubelet-wrapper.html - -Here are a few resolved issues that may help you get started: - -- https://github.com/kubernetes/helm/issues/1371 -- https://github.com/kubernetes/helm/issues/966 - -**Q: Trying to use Helm, I get the error "lookup XXXXX on 8.8.8.8:53: no such host"** - -``` -Error: Error forwarding ports: error upgrading connection: dial tcp: lookup kube-4gb-lon1-02 on 8.8.8.8:53: no such host -``` - -A: We have seen this issue with Ubuntu and Kubeadm in multi-node clusters. The -issue is that the nodes expect certain DNS records to be obtainable via global -DNS. Until this is resolved upstream, you can work around the issue as -follows. On each of the control plane nodes: - -1) Add entries to `/etc/hosts`, mapping your hostnames to their public IPs -2) Install `dnsmasq` (e.g. `apt install -y dnsmasq`) -3) Remove the k8s api server container (kubelet will recreate it) -4) Then `systemctl restart docker` (or reboot the node) for it to pick up the /etc/resolv.conf changes - -See this issue for more information: https://github.com/kubernetes/helm/issues/1455 - -**Q: On GKE (Google Container Engine) I get "No SSH tunnels currently open"** - -``` -Error: Error forwarding ports: error upgrading connection: No SSH tunnels currently open. Were the targets able to accept an ssh-key for user "gke-[redacted]"? -``` - -Another variation of the error message is: - - -``` -Unable to connect to the server: x509: certificate signed by unknown authority - -``` - -A: The issue is that your local Kubernetes config file must have the correct credentials. - -When you create a cluster on GKE, it will give you credentials, including SSL -certificates and certificate authorities. These need to be stored in a Kubernetes -config file (Default: `~/.kube/config` so that `kubectl` and `helm` can access -them. - -**Q: When I run a Helm command, I get an error about the tunnel or proxy** - -A: Helm uses the Kubernetes proxy service to connect to the Tiller server. -If the command `kubectl proxy` does not work for you, neither will Helm. -Typically, the error is related to a missing `socat` service. - -**Q: Tiller crashes with a panic** - -When I run a command on Helm, Tiller crashes with an error like this: - -``` -Tiller is listening on :44134 -Probes server is listening on :44135 -Storage driver is ConfigMap -Cannot initialize Kubernetes connection: the server has asked for the client to provide credentials 2016-12-20 15:18:40.545739 I | storage.go:37: Getting release "bailing-chinchilla" (v1) from storage -panic: runtime error: invalid memory address or nil pointer dereference -[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x8053d5] - -goroutine 77 [running]: -panic(0x1abbfc0, 0xc42000a040) - /usr/local/go/src/runtime/panic.go:500 +0x1a1 -k8s.io/helm/vendor/k8s.io/kubernetes/pkg/client/unversioned.(*ConfigMaps).Get(0xc4200c6200, 0xc420536100, 0x15, 0x1ca7431, 0x6, 0xc42016b6a0) - /home/ubuntu/.go_workspace/src/k8s.io/helm/vendor/k8s.io/kubernetes/pkg/client/unversioned/configmap.go:58 +0x75 -k8s.io/helm/pkg/storage/driver.(*ConfigMaps).Get(0xc4201d6190, 0xc420536100, 0x15, 0xc420536100, 0x15, 0xc4205360c0) - /home/ubuntu/.go_workspace/src/k8s.io/helm/pkg/storage/driver/cfgmaps.go:69 +0x62 -k8s.io/helm/pkg/storage.(*Storage).Get(0xc4201d61a0, 0xc4205360c0, 0x12, 0xc400000001, 0x12, 0x0, 0xc420200070) - /home/ubuntu/.go_workspace/src/k8s.io/helm/pkg/storage/storage.go:38 +0x160 -k8s.io/helm/pkg/tiller.(*ReleaseServer).uniqName(0xc42002a000, 0x0, 0x0, 0xc42016b800, 0xd66a13, 0xc42055a040, 0xc420558050, 0xc420122001) - /home/ubuntu/.go_workspace/src/k8s.io/helm/pkg/tiller/release_server.go:577 +0xd7 -k8s.io/helm/pkg/tiller.(*ReleaseServer).prepareRelease(0xc42002a000, 0xc42027c1e0, 0xc42002a001, 0xc42016bad0, 0xc42016ba08) - /home/ubuntu/.go_workspace/src/k8s.io/helm/pkg/tiller/release_server.go:630 +0x71 -k8s.io/helm/pkg/tiller.(*ReleaseServer).InstallRelease(0xc42002a000, 0x7f284c434068, 0xc420250c00, 0xc42027c1e0, 0x0, 0x31a9, 0x31a9) - /home/ubuntu/.go_workspace/src/k8s.io/helm/pkg/tiller/release_server.go:604 +0x78 -k8s.io/helm/pkg/proto/hapi/services._ReleaseService_InstallRelease_Handler(0x1c51f80, 0xc42002a000, 0x7f284c434068, 0xc420250c00, 0xc42027c190, 0x0, 0x0, 0x0, 0x0, 0x0) - /home/ubuntu/.go_workspace/src/k8s.io/helm/pkg/proto/hapi/services/tiller.pb.go:747 +0x27d -k8s.io/helm/vendor/google.golang.org/grpc.(*Server).processUnaryRPC(0xc4202f3ea0, 0x28610a0, 0xc420078000, 0xc420264690, 0xc420166150, 0x288cbe8, 0xc420250bd0, 0x0, 0x0) - /home/ubuntu/.go_workspace/src/k8s.io/helm/vendor/google.golang.org/grpc/server.go:608 +0xc50 -k8s.io/helm/vendor/google.golang.org/grpc.(*Server).handleStream(0xc4202f3ea0, 0x28610a0, 0xc420078000, 0xc420264690, 0xc420250bd0) - /home/ubuntu/.go_workspace/src/k8s.io/helm/vendor/google.golang.org/grpc/server.go:766 +0x6b0 -k8s.io/helm/vendor/google.golang.org/grpc.(*Server).serveStreams.func1.1(0xc420124710, 0xc4202f3ea0, 0x28610a0, 0xc420078000, 0xc420264690) - /home/ubuntu/.go_workspace/src/k8s.io/helm/vendor/google.golang.org/grpc/server.go:419 +0xab -created by k8s.io/helm/vendor/google.golang.org/grpc.(*Server).serveStreams.func1 - /home/ubuntu/.go_workspace/src/k8s.io/helm/vendor/google.golang.org/grpc/server.go:420 +0xa3 -``` - -A: Check your security settings for Kubernetes. - -A panic in Tiller is almost always the result of a failure to negotiate with the -Kubernetes API server (at which point Tiller can no longer do anything useful, so -it panics and exits). - -Often, this is a result of authentication failing because the Pod in which Tiller -is running does not have the right token. - -To fix this, you will need to change your Kubernetes configuration. Make sure -that `--service-account-private-key-file` from `controller-manager` and -`--service-account-key-file` from apiserver point to the _same_ x509 RSA key. - - -## Upgrading - -My Helm used to work, then I upgrade. Now it is broken. - -**Q: After upgrade, I get the error "Client version is incompatible". What's wrong?** - -Tiller and Helm have to negotiate a common version to make sure that they can safely -communicate without breaking API assumptions. That error means that the version -difference is too great to safely continue. Typically, you need to upgrade -Tiller manually for this. - -The [Installation Guide](install.md) has definitive information about safely -upgrading Helm and Tiller. - -The rules for version numbers are as follows: - -- Pre-release versions are incompatible with everything else. `Alpha.1` is incompatible with `Alpha.2`. -- Patch revisions _are compatible_: 1.2.3 is compatible with 1.2.4 -- Minor revisions _are not compatible_: 1.2.0 is not compatible with 1.3.0, - though we may relax this constraint in the future. -- Major revisions _are not compatible_: 1.0.0 is not compatible with 2.0.0. - -## Uninstalling - -I am trying to remove stuff. - -**Q: When I delete the Tiller deployment, how come all the releases are still there?** - -Releases are stored in ConfigMaps inside of the `kube-system` namespace. You will -have to manually delete them to get rid of the record, or use ```helm delete --purge```. - -**Q: I want to delete my local Helm. Where are all its files?** - -Along with the `helm` binary, Helm stores some files in `$HELM_HOME`, which is -located by default in `~/.helm`. diff --git a/docs/kubernetes_distros.md b/docs/kubernetes_distros.md deleted file mode 100644 index 8b80519ec3c..00000000000 --- a/docs/kubernetes_distros.md +++ /dev/null @@ -1,51 +0,0 @@ -# Kubernetes Distribution Guide - -This document captures information about using Helm in specific Kubernetes -environments. - -We are trying to add more details to this document. Please contribute via Pull -Requests if you can. - -## MiniKube - -Helm is tested and known to work with [minikube](https://github.com/kubernetes/minikube). -It requires no additional configuration. - -## `scripts/local-cluster` and Hyperkube - -Hyperkube configured via `scripts/local-cluster.sh` is known to work. For raw -Hyperkube you may need to do some manual configuration. - -## GKE - -Google's GKE hosted Kubernetes platform is known to work with Helm, and requires -no additional configuration. - -## Ubuntu with 'kubeadm' - -Kubernetes bootstrapped with `kubeadm` is known to work on the following Linux -distributions: - -- Ubuntu 16.04 -- Fedora release 25 - -Some versions of Helm (v2.0.0-beta2) require you to `export KUBECONFIG=/etc/kubernetes/admin.conf` -or create a `~/.kube/config`. - -## Container Linux by CoreOS - -Helm requires that kubelet have access to a copy of the `socat` program to proxy connections to the Tiller API. On Container Linux the Kubelet runs inside of a [hyperkube](https://github.com/kubernetes/kubernetes/tree/master/cluster/images/hyperkube) container image that has socat. So, even though Container Linux doesn't ship `socat` the container filesystem running kubelet does have socat. To learn more read the [Kubelet Wrapper](https://coreos.com/kubernetes/docs/latest/kubelet-wrapper.html) docs. - -## Openshift - -Helm works straightforward on OpenShift Online, OpenShift Dedicated, OpenShift Container Platform (version >= 3.6) or OpenShift Origin (version >= 3.6). To learn more read [this blog](https://blog.openshift.com/getting-started-helm-openshift/) post. - -## Platform9 - -Helm Client and Helm Server (Tiller) are pre-installed with [Platform9 Managed Kubernetes](https://platform9.com/managed-kubernetes/?utm_source=helm_distro_notes). Platform9 provides access to all official Helm charts through the App Catalog UI and native Kubernetes CLI. Additional repositories can be manually added. Further details are available in this [Platform9 App Catalog article](https://platform9.com/support/deploying-kubernetes-apps-platform9-managed-kubernetes/?utm_source=helm_distro_notes). - -## DC/OS - -Helm (both client and server) has been tested and is working on Mesospheres DC/OS 1.11 Kubernetes platform, and requires -no additional configuration. - diff --git a/docs/logos/helm_logo_transparent.png b/docs/logos/helm_logo_transparent.png deleted file mode 100644 index cd5526281f0..00000000000 Binary files a/docs/logos/helm_logo_transparent.png and /dev/null differ diff --git a/docs/man/man1/helm.1 b/docs/man/man1/helm.1 deleted file mode 100644 index 4128830f4a9..00000000000 --- a/docs/man/man1/helm.1 +++ /dev/null @@ -1,85 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm \- The Helm package manager for Kubernetes. - - -.SH SYNOPSIS -.PP -\fBhelm\fP - - -.SH DESCRIPTION -.PP -The Kubernetes package manager - -.PP -To begin working with Helm, run the 'helm init' command: - -.PP -.RS - -.nf -$ helm init - -.fi -.RE - -.PP -This will install Tiller to your running Kubernetes cluster. -It will also set up any necessary local configuration. - -.PP -Common actions from this point include: -.IP \(bu 2 -helm search: search for charts -.IP \(bu 2 -helm fetch: download a chart to your local directory to view -.IP \(bu 2 -helm install: upload the chart to Kubernetes -.IP \(bu 2 -helm list: list releases of charts - -.PP -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") - $KUBECONFIG set an alternative Kubernetes configuration file (default "\~/.kube/config") - - -.SH OPTIONS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-completion(1)\fP, \fBhelm\-create(1)\fP, \fBhelm\-delete(1)\fP, \fBhelm\-dependency(1)\fP, \fBhelm\-fetch(1)\fP, \fBhelm\-get(1)\fP, \fBhelm\-history(1)\fP, \fBhelm\-home(1)\fP, \fBhelm\-init(1)\fP, \fBhelm\-inspect(1)\fP, \fBhelm\-install(1)\fP, \fBhelm\-lint(1)\fP, \fBhelm\-list(1)\fP, \fBhelm\-package(1)\fP, \fBhelm\-plugin(1)\fP, \fBhelm\-repo(1)\fP, \fBhelm\-reset(1)\fP, \fBhelm\-rollback(1)\fP, \fBhelm\-search(1)\fP, \fBhelm\-serve(1)\fP, \fBhelm\-status(1)\fP, \fBhelm\-test(1)\fP, \fBhelm\-upgrade(1)\fP, \fBhelm\-verify(1)\fP, \fBhelm\-version(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_completion.1 b/docs/man/man1/helm_completion.1 deleted file mode 100644 index 83217073f6c..00000000000 --- a/docs/man/man1/helm_completion.1 +++ /dev/null @@ -1,74 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-completion \- Generate autocompletions script for the specified shell (bash or zsh) - - -.SH SYNOPSIS -.PP -\fBhelm completion SHELL\fP - - -.SH DESCRIPTION -.PP -Generate autocompletions script for Helm for the specified shell (bash or zsh). - -.PP -This command can generate shell autocompletions. e.g. - -.PP -.RS - -.nf -$ helm completion bash - -.fi -.RE - -.PP -Can be sourced as such - -.PP -.RS - -.nf -$ source <(helm completion bash) - -.fi -.RE - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_create.1 b/docs/man/man1/helm_create.1 deleted file mode 100644 index 3a1f4b26f4b..00000000000 --- a/docs/man/man1/helm_create.1 +++ /dev/null @@ -1,86 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-create \- create a new chart with the given name - - -.SH SYNOPSIS -.PP -\fBhelm create NAME\fP - - -.SH DESCRIPTION -.PP -This command creates a chart directory along with the common files and -directories used in a chart. - -.PP -For example, 'helm create foo' will create a directory structure that looks -something like this: - -.PP -.RS - -.nf -foo/ - | - |\- .helmignore # Contains patterns to ignore when packaging Helm charts. - | - |\- Chart.yaml # Information about your chart - | - |\- values.yaml # The default values for your templates - | - |\- charts/ # Charts that this chart depends on - | - |\- templates/ # The template files - -.fi -.RE - -.PP -\&'helm create' takes a path for an argument. If directories in the given path -do not exist, Helm will attempt to create them as it goes. If the given -destination exists and there are files in that directory, conflicting files -will be overwritten, but other files will be left alone. - - -.SH OPTIONS -.PP -\fB\-p\fP, \fB\-\-starter\fP="" - the named Helm starter scaffold - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_delete.1 b/docs/man/man1/helm_delete.1 deleted file mode 100644 index ecce68398f0..00000000000 --- a/docs/man/man1/helm_delete.1 +++ /dev/null @@ -1,93 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-delete \- given a release name, delete the release from Kubernetes - - -.SH SYNOPSIS -.PP -\fBhelm delete [flags] RELEASE\_NAME [...]\fP - - -.SH DESCRIPTION -.PP -This command takes a release name, and then deletes the release from Kubernetes. -It removes all of the resources associated with the last release of the chart. - -.PP -Use the '\-\-dry\-run' flag to see which releases will be deleted without actually -deleting them. - - -.SH OPTIONS -.PP -\fB\-\-dry\-run\fP[=false] - simulate a delete - -.PP -\fB\-\-no\-hooks\fP[=false] - prevent hooks from running during deletion - -.PP -\fB\-\-purge\fP[=false] - remove the release from the store and make its name free for later use - -.PP -\fB\-\-timeout\fP=300 - time in seconds to wait for any individual kubernetes operation (like Jobs for hooks) - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_dependency.1 b/docs/man/man1/helm_dependency.1 deleted file mode 100644 index fd4fc195e74..00000000000 --- a/docs/man/man1/helm_dependency.1 +++ /dev/null @@ -1,117 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-dependency \- manage a chart's dependencies - - -.SH SYNOPSIS -.PP -\fBhelm dependency update|build|list\fP - - -.SH DESCRIPTION -.PP -Manage the dependencies of a chart. - -.PP -Helm charts store their dependencies in 'charts/'. For chart developers, it is -often easier to manage a single dependency file ('requirements.yaml') -which declares all dependencies. - -.PP -The dependency commands operate on that file, making it easy to synchronize -between the desired dependencies and the actual dependencies stored in the -'charts/' directory. - -.PP -A 'requirements.yaml' file is a YAML file in which developers can declare chart -dependencies, along with the location of the chart and the desired version. -For example, this requirements file declares two dependencies: - -.PP -.RS - -.nf -# requirements.yaml -dependencies: -\- name: nginx - version: "1.2.3" - repository: "https://example.com/charts" -\- name: memcached - version: "3.2.1" - repository: "https://another.example.com/charts" - -.fi -.RE - -.PP -The 'name' should be the name of a chart, where that name must match the name -in that chart's 'Chart.yaml' file. - -.PP -The 'version' field should contain a semantic version or version range. - -.PP -The 'repository' URL should point to a Chart Repository. Helm expects that by -appending '/index.yaml' to the URL, it should be able to retrieve the chart -repository's index. Note: 'repository' cannot be a repository alias. It must be -a URL. - -.PP -Starting from 2.2.0, repository can be defined as the path to the directory of -the dependency charts stored locally. The path should start with a prefix of -"file://". For example, - -.PP -.RS - -.nf -# requirements.yaml -dependencies: -\- name: nginx - version: "1.2.3" - repository: "file://../dependency\_chart/nginx" - -.fi -.RE - -.PP -If the dependency chart is retrieved locally, it is not required to have the -repository added to helm by "helm add repo". Version matching is also supported -for this case. - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP, \fBhelm\-dependency\-build(1)\fP, \fBhelm\-dependency\-list(1)\fP, \fBhelm\-dependency\-update(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_dependency_build.1 b/docs/man/man1/helm_dependency_build.1 deleted file mode 100644 index adc225a815b..00000000000 --- a/docs/man/man1/helm_dependency_build.1 +++ /dev/null @@ -1,69 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-dependency\-build \- rebuild the charts/ directory based on the requirements.lock file - - -.SH SYNOPSIS -.PP -\fBhelm dependency build [flags] CHART\fP - - -.SH DESCRIPTION -.PP -Build out the charts/ directory from the requirements.lock file. - -.PP -Build is used to reconstruct a chart's dependencies to the state specified in -the lock file. This will not re\-negotiate dependencies, as 'helm dependency update' -does. - -.PP -If no lock file is found, 'helm dependency build' will mirror the behavior -of 'helm dependency update'. - - -.SH OPTIONS -.PP -\fB\-\-keyring\fP="~/.gnupg/pubring.gpg" - keyring containing public keys - -.PP -\fB\-\-verify\fP[=false] - verify the packages against signatures - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-dependency(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_dependency_list.1 b/docs/man/man1/helm_dependency_list.1 deleted file mode 100644 index 30a686bb41b..00000000000 --- a/docs/man/man1/helm_dependency_list.1 +++ /dev/null @@ -1,58 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-dependency\-list \- list the dependencies for the given chart - - -.SH SYNOPSIS -.PP -\fBhelm dependency list [flags] CHART\fP - - -.SH DESCRIPTION -.PP -List all of the dependencies declared in a chart. - -.PP -This can take chart archives and chart directories as input. It will not alter -the contents of a chart. - -.PP -This will produce an error if the chart cannot be loaded. It will emit a warning -if it cannot find a requirements.yaml. - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-dependency(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_dependency_update.1 b/docs/man/man1/helm_dependency_update.1 deleted file mode 100644 index 02d9ec94bdc..00000000000 --- a/docs/man/man1/helm_dependency_update.1 +++ /dev/null @@ -1,78 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-dependency\-update \- update charts/ based on the contents of requirements.yaml - - -.SH SYNOPSIS -.PP -\fBhelm dependency update [flags] CHART\fP - - -.SH DESCRIPTION -.PP -Update the on\-disk dependencies to mirror the requirements.yaml file. - -.PP -This command verifies that the required charts, as expressed in 'requirements.yaml', -are present in 'charts/' and are at an acceptable version. It will pull down -the latest charts that satisfy the dependencies, and clean up old dependencies. - -.PP -On successful update, this will generate a lock file that can be used to -rebuild the requirements to an exact version. - -.PP -Dependencies are not required to be represented in 'requirements.yaml'. For that -reason, an update command will not remove charts unless they are (a) present -in the requirements.yaml file, but (b) at the wrong version. - - -.SH OPTIONS -.PP -\fB\-\-keyring\fP="~/.gnupg/pubring.gpg" - keyring containing public keys - -.PP -\fB\-\-skip\-refresh\fP[=false] - do not refresh the local repository cache - -.PP -\fB\-\-verify\fP[=false] - verify the packages against signatures - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-dependency(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_fetch.1 b/docs/man/man1/helm_fetch.1 deleted file mode 100644 index 9a8ad185c1b..00000000000 --- a/docs/man/man1/helm_fetch.1 +++ /dev/null @@ -1,114 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-fetch \- download a chart from a repository and (optionally) unpack it in local directory - - -.SH SYNOPSIS -.PP -\fBhelm fetch [flags] [chart URL | repo/chartname] [...]\fP - - -.SH DESCRIPTION -.PP -Retrieve a package from a package repository, and download it locally. - -.PP -This is useful for fetching packages to inspect, modify, or repackage. It can -also be used to perform cryptographic verification of a chart without installing -the chart. - -.PP -There are options for unpacking the chart after download. This will create a -directory for the chart and uncomparess into that directory. - -.PP -If the \-\-verify flag is specified, the requested chart MUST have a provenance -file, and MUST pass the verification process. Failure in any part of this will -result in an error, and the chart will not be saved locally. - - -.SH OPTIONS -.PP -\fB\-\-ca\-file\fP="" - verify certificates of HTTPS\-enabled servers using this CA bundle - -.PP -\fB\-\-cert\-file\fP="" - identify HTTPS client using this SSL certificate file - -.PP -\fB\-d\fP, \fB\-\-destination\fP="." - location to write the chart. If this and tardir are specified, tardir is appended to this - -.PP -\fB\-\-devel\fP[=false] - use development versions, too. Equivalent to version '>0.0.0\-a'. If \-\-version is set, this is ignored. - -.PP -\fB\-\-key\-file\fP="" - identify HTTPS client using this SSL key file - -.PP -\fB\-\-keyring\fP="~/.gnupg/pubring.gpg" - keyring containing public keys - -.PP -\fB\-\-prov\fP[=false] - fetch the provenance file, but don't perform verification - -.PP -\fB\-\-repo\fP="" - chart repository url where to locate the requested chart - -.PP -\fB\-\-untar\fP[=false] - if set to true, will untar the chart after downloading it - -.PP -\fB\-\-untardir\fP="." - if untar is specified, this flag specifies the name of the directory into which the chart is expanded - -.PP -\fB\-\-verify\fP[=false] - verify the package against its signature - -.PP -\fB\-\-version\fP="" - specific version of a chart. Without this, the latest version is fetched - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_get.1 b/docs/man/man1/helm_get.1 deleted file mode 100644 index e680f49dcdd..00000000000 --- a/docs/man/man1/helm_get.1 +++ /dev/null @@ -1,89 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-get \- download a named release - - -.SH SYNOPSIS -.PP -\fBhelm get [flags] RELEASE\_NAME\fP - - -.SH DESCRIPTION -.PP -This command shows the details of a named release. - -.PP -It can be used to get extended information about the release, including: -.IP \(bu 2 -The values used to generate the release -.IP \(bu 2 -The chart used to generate the release -.IP \(bu 2 -The generated manifest file - -.PP -By default, this prints a human readable collection of information about the -chart, the supplied values, and the generated manifest file. - - -.SH OPTIONS -.PP -\fB\-\-revision\fP=0 - get the named release with revision - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP, \fBhelm\-get\-hooks(1)\fP, \fBhelm\-get\-manifest(1)\fP, \fBhelm\-get\-values(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_get_hooks.1 b/docs/man/man1/helm_get_hooks.1 deleted file mode 100644 index 34e460d1933..00000000000 --- a/docs/man/man1/helm_get_hooks.1 +++ /dev/null @@ -1,59 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-get\-hooks \- download all hooks for a named release - - -.SH SYNOPSIS -.PP -\fBhelm get hooks [flags] RELEASE\_NAME\fP - - -.SH DESCRIPTION -.PP -This command downloads hooks for a given release. - -.PP -Hooks are formatted in YAML and separated by the YAML '\-\-\-\\n' separator. - - -.SH OPTIONS -.PP -\fB\-\-revision\fP=0 - get the named release with revision - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-get(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_get_manifest.1 b/docs/man/man1/helm_get_manifest.1 deleted file mode 100644 index 7132db38e5f..00000000000 --- a/docs/man/man1/helm_get_manifest.1 +++ /dev/null @@ -1,61 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-get\-manifest \- download the manifest for a named release - - -.SH SYNOPSIS -.PP -\fBhelm get manifest [flags] RELEASE\_NAME\fP - - -.SH DESCRIPTION -.PP -This command fetches the generated manifest for a given release. - -.PP -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. - - -.SH OPTIONS -.PP -\fB\-\-revision\fP=0 - get the named release with revision - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-get(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_get_values.1 b/docs/man/man1/helm_get_values.1 deleted file mode 100644 index 349f97c1443..00000000000 --- a/docs/man/man1/helm_get_values.1 +++ /dev/null @@ -1,60 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-get\-values \- download the values file for a named release - - -.SH SYNOPSIS -.PP -\fBhelm get values [flags] RELEASE\_NAME\fP - - -.SH DESCRIPTION -.PP -This command downloads a values file for a given release. - - -.SH OPTIONS -.PP -\fB\-a\fP, \fB\-\-all\fP[=false] - dump all (computed) values - -.PP -\fB\-\-revision\fP=0 - get the named release with revision - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-get(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_history.1 b/docs/man/man1/helm_history.1 deleted file mode 100644 index 40789ef9279..00000000000 --- a/docs/man/man1/helm_history.1 +++ /dev/null @@ -1,97 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-history \- fetch release history - - -.SH SYNOPSIS -.PP -\fBhelm history [flags] RELEASE\_NAME\fP - - -.SH DESCRIPTION -.PP -History prints historical revisions for a given release. - -.PP -A default maximum of 256 revisions will be returned. Setting '\-\-max' -configures the maximum length of the revision list returned. - -.PP -The historical release set is printed as a formatted table, e.g: - -.PP -.RS - -.nf -$ helm history angry\-bird \-\-max=4 -REVISION UPDATED STATUS CHART DESCRIPTION -1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine\-0.1.0 Initial install -2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine\-0.1.0 Upgraded successfully -3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine\-0.1.0 Rolled back to 2 -4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine\-0.1.0 Upgraded successfully - -.fi -.RE - - -.SH OPTIONS -.PP -\fB\-\-max\fP=256 - maximum number of revision to include in history - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_home.1 b/docs/man/man1/helm_home.1 deleted file mode 100644 index 77024d53ebf..00000000000 --- a/docs/man/man1/helm_home.1 +++ /dev/null @@ -1,51 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-home \- displays the location of HELM\_HOME - - -.SH SYNOPSIS -.PP -\fBhelm home\fP - - -.SH DESCRIPTION -.PP -This command displays the location of HELM\_HOME. This is where -any helm configuration files live. - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_init.1 b/docs/man/man1/helm_init.1 deleted file mode 100644 index 74871ebe811..00000000000 --- a/docs/man/man1/helm_init.1 +++ /dev/null @@ -1,135 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-init \- initialize Helm on both client and server - - -.SH SYNOPSIS -.PP -\fBhelm init\fP - - -.SH DESCRIPTION -.PP -This command installs Tiller (the helm server side component) onto your -Kubernetes Cluster and sets up local configuration in $HELM\_HOME (default \~/.helm/) - -.PP -As with the rest of the Helm commands, 'helm init' discovers Kubernetes clusters -by reading $KUBECONFIG (default '\~/.kube/config') and using the default context. - -.PP -To set up just a local environment, use '\-\-client\-only'. That will configure -$HELM\_HOME, but not attempt to connect to a remote cluster and install the Tiller -deployment. - -.PP -When installing Tiller, 'helm init' will attempt to install the latest released -version. You can specify an alternative image with '\-\-tiller\-image'. For those -frequently working on the latest code, the flag '\-\-canary\-image' will install -the latest pre\-release version of Tiller (e.g. the HEAD commit in the GitHub -repository on the master branch). - -.PP -To dump a manifest containing the Tiller deployment YAML, combine the -'\-\-dry\-run' and '\-\-debug' flags. - - -.SH OPTIONS -.PP -\fB\-\-canary\-image\fP[=false] - use the canary tiller image - -.PP -\fB\-c\fP, \fB\-\-client\-only\fP[=false] - if set does not install tiller - -.PP -\fB\-\-dry\-run\fP[=false] - do not install local or remote - -.PP -\fB\-\-local\-repo\-url\fP=" -\[la]http://127.0.0.1:8879/charts"\[ra] - URL for local repository - -.PP -\fB\-\-net\-host\fP[=false] - install tiller with net=host - -.PP -\fB\-\-service\-account\fP="" - name of service account - -.PP -\fB\-\-skip\-refresh\fP[=false] - do not refresh (download) the local repository cache - -.PP -\fB\-\-stable\-repo\-url\fP=" -\[la]https://kubernetes-charts.storage.googleapis.com"\[ra] - URL for stable repository - -.PP -\fB\-i\fP, \fB\-\-tiller\-image\fP="" - override tiller image - -.PP -\fB\-\-tiller\-tls\fP[=false] - install tiller with TLS enabled - -.PP -\fB\-\-tiller\-tls\-cert\fP="" - path to TLS certificate file to install with tiller - -.PP -\fB\-\-tiller\-tls\-key\fP="" - path to TLS key file to install with tiller - -.PP -\fB\-\-tiller\-tls\-verify\fP[=false] - install tiller with TLS enabled and to verify remote certificates - -.PP -\fB\-\-tls\-ca\-cert\fP="" - path to CA root certificate - -.PP -\fB\-\-upgrade\fP[=false] - upgrade if tiller is already installed - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_inspect.1 b/docs/man/man1/helm_inspect.1 deleted file mode 100644 index 0783476c7c0..00000000000 --- a/docs/man/man1/helm_inspect.1 +++ /dev/null @@ -1,84 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-inspect \- inspect a chart - - -.SH SYNOPSIS -.PP -\fBhelm inspect [CHART]\fP - - -.SH DESCRIPTION -.PP -This command inspects a chart and displays information. It takes a chart reference -('stable/drupal'), a full path to a directory or packaged chart, or a URL. - -.PP -Inspect prints the contents of the Chart.yaml file and the values.yaml file. - - -.SH OPTIONS -.PP -\fB\-\-ca\-file\fP="" - chart repository url where to locate the requested chart - -.PP -\fB\-\-cert\-file\fP="" - verify certificates of HTTPS\-enabled servers using this CA bundle - -.PP -\fB\-\-key\-file\fP="" - identify HTTPS client using this SSL key file - -.PP -\fB\-\-keyring\fP="~/.gnupg/pubring.gpg" - path to the keyring containing public verification keys - -.PP -\fB\-\-repo\fP="" - chart repository url where to locate the requested chart - -.PP -\fB\-\-verify\fP[=false] - verify the provenance data for this chart - -.PP -\fB\-\-version\fP="" - version of the chart. By default, the newest chart is shown - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP, \fBhelm\-inspect\-chart(1)\fP, \fBhelm\-inspect\-values(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_inspect_chart.1 b/docs/man/man1/helm_inspect_chart.1 deleted file mode 100644 index f728df410d2..00000000000 --- a/docs/man/man1/helm_inspect_chart.1 +++ /dev/null @@ -1,81 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-inspect\-chart \- shows inspect chart - - -.SH SYNOPSIS -.PP -\fBhelm inspect chart [CHART]\fP - - -.SH DESCRIPTION -.PP -This command inspects a chart (directory, file, or URL) and displays the contents -of the Charts.yaml file - - -.SH OPTIONS -.PP -\fB\-\-ca\-file\fP="" - chart repository url where to locate the requested chart - -.PP -\fB\-\-cert\-file\fP="" - verify certificates of HTTPS\-enabled servers using this CA bundle - -.PP -\fB\-\-key\-file\fP="" - identify HTTPS client using this SSL key file - -.PP -\fB\-\-keyring\fP="~/.gnupg/pubring.gpg" - path to the keyring containing public verification keys - -.PP -\fB\-\-repo\fP="" - chart repository url where to locate the requested chart - -.PP -\fB\-\-verify\fP[=false] - verify the provenance data for this chart - -.PP -\fB\-\-version\fP="" - version of the chart. By default, the newest chart is shown - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-inspect(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_inspect_values.1 b/docs/man/man1/helm_inspect_values.1 deleted file mode 100644 index c87dd9c6022..00000000000 --- a/docs/man/man1/helm_inspect_values.1 +++ /dev/null @@ -1,81 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-inspect\-values \- shows inspect values - - -.SH SYNOPSIS -.PP -\fBhelm inspect values [CHART]\fP - - -.SH DESCRIPTION -.PP -This command inspects a chart (directory, file, or URL) and displays the contents -of the values.yaml file - - -.SH OPTIONS -.PP -\fB\-\-ca\-file\fP="" - chart repository url where to locate the requested chart - -.PP -\fB\-\-cert\-file\fP="" - verify certificates of HTTPS\-enabled servers using this CA bundle - -.PP -\fB\-\-key\-file\fP="" - identify HTTPS client using this SSL key file - -.PP -\fB\-\-keyring\fP="~/.gnupg/pubring.gpg" - path to the keyring containing public verification keys - -.PP -\fB\-\-repo\fP="" - chart repository url where to locate the requested chart - -.PP -\fB\-\-verify\fP[=false] - verify the provenance data for this chart - -.PP -\fB\-\-version\fP="" - version of the chart. By default, the newest chart is shown - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-inspect(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_install.1 b/docs/man/man1/helm_install.1 deleted file mode 100644 index fe1856bed20..00000000000 --- a/docs/man/man1/helm_install.1 +++ /dev/null @@ -1,243 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-install \- install a chart archive - - -.SH SYNOPSIS -.PP -\fBhelm install [CHART]\fP - - -.SH DESCRIPTION -.PP -This command installs a chart archive. - -.PP -The install argument must be a chart reference, a path to a packaged chart, -a path to an unpacked chart directory or a URL. - -.PP -To override values in a chart, use either the '\-\-values' flag and pass in a file -or use the '\-\-set' flag and pass configuration from the command line. - -.PP -.RS - -.nf -$ helm install \-f myvalues.yaml ./redis - -.fi -.RE - -.PP -or - -.PP -.RS - -.nf -$ helm install \-\-set name=prod ./redis - -.fi -.RE - -.PP -You can specify the '\-\-values'/'\-f' flag multiple times. The priority will be given to the -last (right\-most) file specified. For example, if both myvalues.yaml and override.yaml -contained a key called 'Test', the value set in override.yaml would take precedence: - -.PP -.RS - -.nf -$ helm install \-f myvalues.yaml \-f override.yaml ./redis - -.fi -.RE - -.PP -You can specify the '\-\-set' flag multiple times. The priority will be given to the -last (right\-most) set specified. For example, if both 'bar' and 'newbar' values are -set for a key called 'foo', the 'newbar' value would take precedence: - -.PP -.RS - -.nf -$ helm install \-\-set foo=bar \-\-set foo=newbar ./redis - -.fi -.RE - -.PP -To check the generated manifests of a release without installing the chart, -the '\-\-debug' and '\-\-dry\-run' flags can be combined. This will still require a -round\-trip to the Tiller server. - -.PP -If \-\-verify is set, the chart MUST have a provenance file, and the provenenace -fall MUST pass all verification steps. - -.PP -There are four different ways you can express the chart you want to install: -.IP " 1." 5 -By chart reference: helm install stable/mariadb -.IP " 2." 5 -By path to a packaged chart: helm install ./nginx\-1.2.3.tgz -.IP " 3." 5 -By path to an unpacked chart directory: helm install ./nginx -.IP " 4." 5 -By absolute URL: helm install -\[la]https://example.com/charts/nginx-1.2.3.tgz\[ra] - -.PP -CHART REFERENCES - -.PP -A chart reference is a convenient way of reference a chart in a chart repository. - -.PP -When you use a chart reference ('stable/mariadb'), Helm will look in the local -configuration for a chart repository named 'stable', and will then look for a -chart in that repository whose name is 'mariadb'. It will install the latest -version of that chart unless you also supply a version number with the -'\-\-version' flag. - -.PP -To see the list of chart repositories, use 'helm repo list'. To search for -charts in a repository, use 'helm search'. - - -.SH OPTIONS -.PP -\fB\-\-ca\-file\fP="" - verify certificates of HTTPS\-enabled servers using this CA bundle - -.PP -\fB\-\-cert\-file\fP="" - identify HTTPS client using this SSL certificate file - -.PP -\fB\-\-dep\-up\fP[=false] - run helm dependency update before installing the chart. - -.PP -\fB\-\-devel\fP[=false] - use development versions, too. Equivalent to version '>0.0.0\-a'. If \-\-version is set, this is ignored. - -.PP -\fB\-\-dry\-run\fP[=false] - simulate an install - -.PP -\fB\-\-key\-file\fP="" - identify HTTPS client using this SSL key file - -.PP -\fB\-\-keyring\fP="~/.gnupg/pubring.gpg" - location of public keys used for verification - -.PP -\fB\-n\fP, \fB\-\-name\fP="" - release name. If unspecified, it will autogenerate one for you - -.PP -\fB\-\-name\-template\fP="" - specify template used to name the release - -.PP -\fB\-\-namespace\fP="" - namespace to install the release into - -.PP -\fB\-\-no\-hooks\fP[=false] - prevent hooks from running during install - -.PP -\fB\-\-replace\fP[=false] - re\-use the given name, even if that name is already used. This is unsafe in production - -.PP -\fB\-\-repo\fP="" - chart repository url where to locate the requested chart - -.PP -\fB\-\-set\fP=[] - set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - -.PP -\fB\-\-timeout\fP=300 - time in seconds to wait for any individual kubernetes operation (like Jobs for hooks) - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - -.PP -\fB\-f\fP, \fB\-\-values\fP=[] - specify values in a YAML file or a URL(can specify multiple) - -.PP -\fB\-\-verify\fP[=false] - verify the package before installing it - -.PP -\fB\-\-version\fP="" - specify the exact chart version to install. If this is not specified, the latest version is installed - -.PP -\fB\-\-wait\fP[=false] - if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as \-\-timeout - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -31\-Oct\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_lint.1 b/docs/man/man1/helm_lint.1 deleted file mode 100644 index e0d52268590..00000000000 --- a/docs/man/man1/helm_lint.1 +++ /dev/null @@ -1,62 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-lint \- examines a chart for possible issues - - -.SH SYNOPSIS -.PP -\fBhelm lint [flags] PATH\fP - - -.SH DESCRIPTION -.PP -This command takes a path to a chart and runs a series of tests to verify that -the chart is well\-formed. - -.PP -If the linter encounters things that will cause the chart to fail installation, -it will emit [ERROR] messages. If it encounters issues that break with convention -or recommendation, it will emit [WARNING] messages. - - -.SH OPTIONS -.PP -\fB\-\-strict\fP[=false] - fail on lint warnings - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_list.1 b/docs/man/man1/helm_list.1 deleted file mode 100644 index d4fccf96055..00000000000 --- a/docs/man/man1/helm_list.1 +++ /dev/null @@ -1,151 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-list \- list releases - - -.SH SYNOPSIS -.PP -\fBhelm list [flags] [FILTER]\fP - - -.SH DESCRIPTION -.PP -This command lists all of the releases. - -.PP -By default, it lists only releases that are deployed or failed. Flags like -'\-\-deleted' and '\-\-all' will alter this behavior. Such flags can be combined: -'\-\-deleted \-\-failed'. - -.PP -By default, items are sorted alphabetically. Use the '\-d' flag to sort by -release date. - -.PP -If an argument is provided, it will be treated as a filter. Filters are -regular expressions (Perl compatible) that are applied to the list of releases. -Only items that match the filter will be returned. - -.PP -.RS - -.nf -$ helm list 'ara[a\-z]+' -NAME UPDATED CHART -maudlin\-arachnid Mon May 9 16:07:08 2016 alpine\-0.1.0 - -.fi -.RE - -.PP -If no results are found, 'helm list' will exit 0, but with no output (or in -the case of no '\-q' flag, only headers). - -.PP -By default, up to 256 items may be returned. To limit this, use the '\-\-max' flag. -Setting '\-\-max' to 0 will not return all results. Rather, it will return the -server's default, which may be much higher than 256. Pairing the '\-\-max' -flag with the '\-\-offset' flag allows you to page through results. - - -.SH OPTIONS -.PP -\fB\-\-all\fP[=false] - show all releases, not just the ones marked DEPLOYED - -.PP -\fB\-d\fP, \fB\-\-date\fP[=false] - sort by release date - -.PP -\fB\-\-deleted\fP[=false] - show deleted releases - -.PP -\fB\-\-deleting\fP[=false] - show releases that are currently being deleted - -.PP -\fB\-\-deployed\fP[=false] - show deployed releases. If no other is specified, this will be automatically enabled - -.PP -\fB\-\-failed\fP[=false] - show failed releases - -.PP -\fB\-m\fP, \fB\-\-max\fP=256 - maximum number of releases to fetch - -.PP -\fB\-\-namespace\fP="" - show releases within a specific namespace - -.PP -\fB\-o\fP, \fB\-\-offset\fP="" - next release name in the list, used to offset from start value - -.PP -\fB\-r\fP, \fB\-\-reverse\fP[=false] - reverse the sort order - -.PP -\fB\-q\fP, \fB\-\-short\fP[=false] - output short (quiet) listing format - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_package.1 b/docs/man/man1/helm_package.1 deleted file mode 100644 index 07185a4c228..00000000000 --- a/docs/man/man1/helm_package.1 +++ /dev/null @@ -1,85 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-package \- package a chart directory into a chart archive - - -.SH SYNOPSIS -.PP -\fBhelm package [flags] [CHART\_PATH] [...]\fP - - -.SH DESCRIPTION -.PP -This command packages a chart into a versioned chart archive file. If a path -is given, this will look at that path for a chart (which must contain a -Chart.yaml file) and then package that directory. - -.PP -If no path is given, this will look in the present working directory for a -Chart.yaml file, and (if found) build the current directory into a chart. - -.PP -Versioned chart archives are used by Helm package repositories. - - -.SH OPTIONS -.PP -\fB\-d\fP, \fB\-\-destination\fP="." - location to write the chart. - -.PP -\fB\-\-key\fP="" - name of the key to use when signing. Used if \-\-sign is true - -.PP -\fB\-\-keyring\fP="~/.gnupg/pubring.gpg" - location of a public keyring - -.PP -\fB\-\-save\fP[=true] - save packaged chart to local chart repository - -.PP -\fB\-\-sign\fP[=false] - use a PGP private key to sign this package - -.PP -\fB\-\-version\fP="" - set the version on the chart to this semver version - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_plugin.1 b/docs/man/man1/helm_plugin.1 deleted file mode 100644 index 7af4f39fcb9..00000000000 --- a/docs/man/man1/helm_plugin.1 +++ /dev/null @@ -1,50 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-plugin \- add, list, or remove Helm plugins - - -.SH SYNOPSIS -.PP -\fBhelm plugin\fP - - -.SH DESCRIPTION -.PP -Manage client\-side Helm plugins. - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP, \fBhelm\-plugin\-install(1)\fP, \fBhelm\-plugin\-list(1)\fP, \fBhelm\-plugin\-remove(1)\fP, \fBhelm\-plugin\-update(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_plugin_install.1 b/docs/man/man1/helm_plugin_install.1 deleted file mode 100644 index d2a8d132602..00000000000 --- a/docs/man/man1/helm_plugin_install.1 +++ /dev/null @@ -1,56 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-plugin\-install \- install one or more Helm plugins - - -.SH SYNOPSIS -.PP -\fBhelm plugin install [options] \&...\fP - - -.SH DESCRIPTION -.PP -install one or more Helm plugins - - -.SH OPTIONS -.PP -\fB\-\-version\fP="" - specify a version constraint. If this is not specified, the latest version is installed - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-plugin(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_plugin_list.1 b/docs/man/man1/helm_plugin_list.1 deleted file mode 100644 index 5fcebd748b1..00000000000 --- a/docs/man/man1/helm_plugin_list.1 +++ /dev/null @@ -1,50 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-plugin\-list \- list installed Helm plugins - - -.SH SYNOPSIS -.PP -\fBhelm plugin list\fP - - -.SH DESCRIPTION -.PP -list installed Helm plugins - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-plugin(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_plugin_remove.1 b/docs/man/man1/helm_plugin_remove.1 deleted file mode 100644 index de64220ddaa..00000000000 --- a/docs/man/man1/helm_plugin_remove.1 +++ /dev/null @@ -1,50 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-plugin\-remove \- remove one or more Helm plugins - - -.SH SYNOPSIS -.PP -\fBhelm plugin remove \&...\fP - - -.SH DESCRIPTION -.PP -remove one or more Helm plugins - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-plugin(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_plugin_update.1 b/docs/man/man1/helm_plugin_update.1 deleted file mode 100644 index ad7d70a174d..00000000000 --- a/docs/man/man1/helm_plugin_update.1 +++ /dev/null @@ -1,50 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-plugin\-update \- update one or more Helm plugins - - -.SH SYNOPSIS -.PP -\fBhelm plugin update \&...\fP - - -.SH DESCRIPTION -.PP -update one or more Helm plugins - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-plugin(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_repo.1 b/docs/man/man1/helm_repo.1 deleted file mode 100644 index 19b2da8a3db..00000000000 --- a/docs/man/man1/helm_repo.1 +++ /dev/null @@ -1,55 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-repo \- add, list, remove, update, and index chart repositories - - -.SH SYNOPSIS -.PP -\fBhelm repo [FLAGS] add|remove|list|index|update [ARGS]\fP - - -.SH DESCRIPTION -.PP -This command consists of multiple subcommands to interact with chart repositories. - -.PP -It can be used to add, remove, list, and index chart repositories. -Example usage: - $ helm repo add [NAME] [REPO\_URL] - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP, \fBhelm\-repo\-add(1)\fP, \fBhelm\-repo\-index(1)\fP, \fBhelm\-repo\-list(1)\fP, \fBhelm\-repo\-remove(1)\fP, \fBhelm\-repo\-update(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_repo_add.1 b/docs/man/man1/helm_repo_add.1 deleted file mode 100644 index 3cd9f790b89..00000000000 --- a/docs/man/man1/helm_repo_add.1 +++ /dev/null @@ -1,68 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-repo\-add \- add a chart repository - - -.SH SYNOPSIS -.PP -\fBhelm repo add [flags] [NAME] [URL]\fP - - -.SH DESCRIPTION -.PP -add a chart repository - - -.SH OPTIONS -.PP -\fB\-\-ca\-file\fP="" - verify certificates of HTTPS\-enabled servers using this CA bundle - -.PP -\fB\-\-cert\-file\fP="" - identify HTTPS client using this SSL certificate file - -.PP -\fB\-\-key\-file\fP="" - identify HTTPS client using this SSL key file - -.PP -\fB\-\-no\-update\fP[=false] - raise error if repo is already registered - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-repo(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_repo_index.1 b/docs/man/man1/helm_repo_index.1 deleted file mode 100644 index edfcda6f55f..00000000000 --- a/docs/man/man1/helm_repo_index.1 +++ /dev/null @@ -1,69 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-repo\-index \- generate an index file given a directory containing packaged charts - - -.SH SYNOPSIS -.PP -\fBhelm repo index [flags] [DIR]\fP - - -.SH DESCRIPTION -.PP -Read the current directory and generate an index file based on the charts found. - -.PP -This tool is used for creating an 'index.yaml' file for a chart repository. To -set an absolute URL to the charts, use '\-\-url' flag. - -.PP -To merge the generated index with an existing index file, use the '\-\-merge' -flag. In this case, the charts found in the current directory will be merged -into the existing index, with local charts taking priority over existing charts. - - -.SH OPTIONS -.PP -\fB\-\-merge\fP="" - merge the generated index into the given index - -.PP -\fB\-\-url\fP="" - url of chart repository - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-repo(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_repo_list.1 b/docs/man/man1/helm_repo_list.1 deleted file mode 100644 index 7623f73fe61..00000000000 --- a/docs/man/man1/helm_repo_list.1 +++ /dev/null @@ -1,50 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-repo\-list \- list chart repositories - - -.SH SYNOPSIS -.PP -\fBhelm repo list [flags]\fP - - -.SH DESCRIPTION -.PP -list chart repositories - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-repo(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_repo_remove.1 b/docs/man/man1/helm_repo_remove.1 deleted file mode 100644 index cfbf217a448..00000000000 --- a/docs/man/man1/helm_repo_remove.1 +++ /dev/null @@ -1,50 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-repo\-remove \- remove a chart repository - - -.SH SYNOPSIS -.PP -\fBhelm repo remove [flags] [NAME]\fP - - -.SH DESCRIPTION -.PP -remove a chart repository - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-repo(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_repo_update.1 b/docs/man/man1/helm_repo_update.1 deleted file mode 100644 index 42bf511dd1a..00000000000 --- a/docs/man/man1/helm_repo_update.1 +++ /dev/null @@ -1,55 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-repo\-update \- update information of available charts locally from chart repositories - - -.SH SYNOPSIS -.PP -\fBhelm repo update\fP - - -.SH DESCRIPTION -.PP -Update gets the latest information about charts from the respective chart repositories. -Information is cached locally, where it is used by commands like 'helm search'. - -.PP -\&'helm update' is the deprecated form of 'helm repo update'. It will be removed in -future releases. - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm\-repo(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_reset.1 b/docs/man/man1/helm_reset.1 deleted file mode 100644 index bf735591d26..00000000000 --- a/docs/man/man1/helm_reset.1 +++ /dev/null @@ -1,82 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-reset \- uninstalls Tiller from a cluster - - -.SH SYNOPSIS -.PP -\fBhelm reset\fP - - -.SH DESCRIPTION -.PP -This command uninstalls Tiller (the helm server side component) from your -Kubernetes Cluster and optionally deletes local configuration in -$HELM\_HOME (default \~/.helm/) - - -.SH OPTIONS -.PP -\fB\-f\fP, \fB\-\-force\fP[=false] - forces Tiller uninstall even if there are releases installed - -.PP -\fB\-\-remove\-helm\-home\fP[=false] - if set deletes $HELM\_HOME - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_rollback.1 b/docs/man/man1/helm_rollback.1 deleted file mode 100644 index d91ab881d80..00000000000 --- a/docs/man/man1/helm_rollback.1 +++ /dev/null @@ -1,101 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-rollback \- roll back a release to a previous revision - - -.SH SYNOPSIS -.PP -\fBhelm rollback [flags] [RELEASE] [REVISION]\fP - - -.SH DESCRIPTION -.PP -This command rolls back a release to a previous revision. - -.PP -The first argument of the rollback command is the name of a release, and the -second is a revision (version) number. To see revision numbers, run -'helm history RELEASE'. - - -.SH OPTIONS -.PP -\fB\-\-dry\-run\fP[=false] - simulate a rollback - -.PP -\fB\-\-force\fP[=false] - force resource update through delete/recreate if needed - -.PP -\fB\-\-no\-hooks\fP[=false] - prevent hooks from running during rollback - -.PP -\fB\-\-recreate\-pods\fP[=false] - performs pods restart for the resource if applicable - -.PP -\fB\-\-timeout\fP=300 - time in seconds to wait for any individual kubernetes operation (like Jobs for hooks) - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - -.PP -\fB\-\-wait\fP[=false] - if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as \-\-timeout - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_search.1 b/docs/man/man1/helm_search.1 deleted file mode 100644 index ac2467bf25f..00000000000 --- a/docs/man/man1/helm_search.1 +++ /dev/null @@ -1,68 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-search \- search for a keyword in charts - - -.SH SYNOPSIS -.PP -\fBhelm search [keyword]\fP - - -.SH DESCRIPTION -.PP -Search reads through all of the repositories configured on the system, and -looks for matches. - -.PP -Repositories are managed with 'helm repo' commands. - - -.SH OPTIONS -.PP -\fB\-r\fP, \fB\-\-regexp\fP[=false] - use regular expressions for searching - -.PP -\fB\-v\fP, \fB\-\-version\fP="" - search using semantic versioning constraints - -.PP -\fB\-l\fP, \fB\-\-versions\fP[=false] - show the long listing, with each version of each chart on its own line - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_serve.1 b/docs/man/man1/helm_serve.1 deleted file mode 100644 index a4a9c51da79..00000000000 --- a/docs/man/man1/helm_serve.1 +++ /dev/null @@ -1,79 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-serve \- start a local http web server - - -.SH SYNOPSIS -.PP -\fBhelm serve\fP - - -.SH DESCRIPTION -.PP -This command starts a local chart repository server that serves charts from a local directory. - -.PP -The new server will provide HTTP access to a repository. By default, it will -scan all of the charts in '$HELM\_HOME/repository/local' and serve those over -the local IPv4 TCP port (default '127.0.0.1:8879'). - -.PP -This command is intended to be used for educational and testing purposes only. -It is best to rely on a dedicated web server or a cloud\-hosted solution like -Google Cloud Storage for production use. - -.PP -See -\[la]https://github.com/kubernetes/helm/blob/master/docs/chart_repository.md#hosting-chart-repositories\[ra] -for more information on hosting chart repositories in a production setting. - - -.SH OPTIONS -.PP -\fB\-\-address\fP="127.0.0.1:8879" - address to listen on - -.PP -\fB\-\-repo\-path\fP="" - local directory path from which to serve charts - -.PP -\fB\-\-url\fP="" - external URL of chart repository - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_status.1 b/docs/man/man1/helm_status.1 deleted file mode 100644 index 8f23668089e..00000000000 --- a/docs/man/man1/helm_status.1 +++ /dev/null @@ -1,83 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-status \- displays the status of the named release - - -.SH SYNOPSIS -.PP -\fBhelm status [flags] RELEASE\_NAME\fP - - -.SH DESCRIPTION -.PP -This command shows the status of a named release. -The status consists of: -\- last deployment time -\- k8s namespace in which the release lives -\- state of the release (can be: UNKNOWN, DEPLOYED, DELETED, SUPERSEDED, FAILED or DELETING) -\- list of resources that this release consists of, sorted by kind -\- details on last test suite run, if applicable -\- additional notes provided by the chart - - -.SH OPTIONS -.PP -\fB\-\-revision\fP=0 - if set, display the status of the named release with revision - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_test.1 b/docs/man/man1/helm_test.1 deleted file mode 100644 index 6da36b33b58..00000000000 --- a/docs/man/man1/helm_test.1 +++ /dev/null @@ -1,84 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-test \- test a release - - -.SH SYNOPSIS -.PP -\fBhelm test [RELEASE]\fP - - -.SH DESCRIPTION -.PP -The test command runs the tests for a release. - -.PP -The argument this command takes is the name of a deployed release. -The tests to be run are defined in the chart that was installed. - - -.SH OPTIONS -.PP -\fB\-\-cleanup\fP[=false] - delete test pods upon completion - -.PP -\fB\-\-timeout\fP=300 - time in seconds to wait for any individual kubernetes operation (like Jobs for hooks) - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_upgrade.1 b/docs/man/man1/helm_upgrade.1 deleted file mode 100644 index 24bba7c85da..00000000000 --- a/docs/man/man1/helm_upgrade.1 +++ /dev/null @@ -1,190 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-upgrade \- upgrade a release - - -.SH SYNOPSIS -.PP -\fBhelm upgrade [RELEASE] [CHART]\fP - - -.SH DESCRIPTION -.PP -This command upgrades a release to a new version of a chart. - -.PP -The upgrade arguments must be a release and chart. The chart -argument can be either: a chart reference('stable/mariadb'), a path to a chart directory, -a packaged chart, or a fully qualified URL. For chart references, the latest -version will be specified unless the '\-\-version' flag is set. - -.PP -To override values in a chart, use either the '\-\-values' flag and pass in a file -or use the '\-\-set' flag and pass configuration from the command line. - -.PP -You can specify the '\-\-values'/'\-f' flag multiple times. The priority will be given to the -last (right\-most) file specified. For example, if both myvalues.yaml and override.yaml -contained a key called 'Test', the value set in override.yaml would take precedence: - -.PP -.RS - -.nf -$ helm upgrade \-f myvalues.yaml \-f override.yaml redis ./redis - -.fi -.RE - -.PP -You can specify the '\-\-set' flag multiple times. The priority will be given to the -last (right\-most) set specified. For example, if both 'bar' and 'newbar' values are -set for a key called 'foo', the 'newbar' value would take precedence: - -.PP -.RS - -.nf -$ helm upgrade \-\-set foo=bar \-\-set foo=newbar redis ./redis - -.fi -.RE - - -.SH OPTIONS -.PP -\fB\-\-ca\-file\fP="" - verify certificates of HTTPS\-enabled servers using this CA bundle - -.PP -\fB\-\-cert\-file\fP="" - identify HTTPS client using this SSL certificate file - -.PP -\fB\-\-devel\fP[=false] - use development versions, too. Equivalent to version '>0.0.0\-a'. If \-\-version is set, this is ignored. - -.PP -\fB\-\-dry\-run\fP[=false] - simulate an upgrade - -.PP -\fB\-\-force\fP[=false] - force resource update through delete/recreate if needed - -.PP -\fB\-i\fP, \fB\-\-install\fP[=false] - if a release by this name doesn't already exist, run an install - -.PP -\fB\-\-key\-file\fP="" - identify HTTPS client using this SSL key file - -.PP -\fB\-\-keyring\fP="~/.gnupg/pubring.gpg" - path to the keyring that contains public signing keys - -.PP -\fB\-\-namespace\fP="default" - namespace to install the release into (only used if \-\-install is set) - -.PP -\fB\-\-no\-hooks\fP[=false] - disable pre/post upgrade hooks - -.PP -\fB\-\-recreate\-pods\fP[=false] - performs pods restart for the resource if applicable - -.PP -\fB\-\-repo\fP="" - chart repository url where to locate the requested chart - -.PP -\fB\-\-reset\-values\fP[=false] - when upgrading, reset the values to the ones built into the chart - -.PP -\fB\-\-reuse\-values\fP[=false] - when upgrading, reuse the last release's values, and merge in any new values. If '\-\-reset\-values' is specified, this is ignored. - -.PP -\fB\-\-set\fP=[] - set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - -.PP -\fB\-\-timeout\fP=300 - time in seconds to wait for any individual kubernetes operation (like Jobs for hooks) - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - -.PP -\fB\-f\fP, \fB\-\-values\fP=[] - specify values in a YAML file or a URL(can specify multiple) - -.PP -\fB\-\-verify\fP[=false] - verify the provenance of the chart before upgrading - -.PP -\fB\-\-version\fP="" - specify the exact chart version to use. If this is not specified, the latest version is used - -.PP -\fB\-\-wait\fP[=false] - if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as \-\-timeout - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_verify.1 b/docs/man/man1/helm_verify.1 deleted file mode 100644 index 5297924ae86..00000000000 --- a/docs/man/man1/helm_verify.1 +++ /dev/null @@ -1,65 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-verify \- verify that a chart at the given path has been signed and is valid - - -.SH SYNOPSIS -.PP -\fBhelm verify [flags] PATH\fP - - -.SH DESCRIPTION -.PP -Verify that the given chart has a valid provenance file. - -.PP -Provenance files provide crytographic verification that a chart has not been -tampered with, and was packaged by a trusted provider. - -.PP -This command can be used to verify a local chart. Several other commands provide -'\-\-verify' flags that run the same validation. To generate a signed package, use -the 'helm package \-\-sign' command. - - -.SH OPTIONS -.PP -\fB\-\-keyring\fP="~/.gnupg/pubring.gpg" - keyring containing public keys - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/man/man1/helm_version.1 b/docs/man/man1/helm_version.1 deleted file mode 100644 index 1f1bf600d09..00000000000 --- a/docs/man/man1/helm_version.1 +++ /dev/null @@ -1,103 +0,0 @@ -.TH "HELM" "1" "May 2017" "Auto generated by spf13/cobra" "" -.nh -.ad l - - -.SH NAME -.PP -helm\-version \- print the client/server version information - - -.SH SYNOPSIS -.PP -\fBhelm version\fP - - -.SH DESCRIPTION -.PP -Show the client and server versions for Helm and tiller. - -.PP -This will print a representation of the client and server versions of Helm and -Tiller. The output will look something like this: - -.PP -Client: \&version.Version{SemVer:"v2.0.0", GitCommit:"ff52399e51bb880526e9cd0ed8386f6433b74da1", GitTreeState:"clean"} -Server: \&version.Version{SemVer:"v2.0.0", GitCommit:"b0c113dfb9f612a9add796549da66c0d294508a3", GitTreeState:"clean"} -.IP \(bu 2 -SemVer is the semantic version of the release. -.IP \(bu 2 -GitCommit is the SHA for the commit that this version was built from. -.IP \(bu 2 -GitTreeState is "clean" if there are no local code changes when this binary was -built, and "dirty" if the binary was built from locally modified code. - -.PP -To print just the client version, use '\-\-client'. To print just the server version, -use '\-\-server'. - - -.SH OPTIONS -.PP -\fB\-c\fP, \fB\-\-client\fP[=false] - client version only - -.PP -\fB\-s\fP, \fB\-\-server\fP[=false] - server version only - -.PP -\fB\-\-short\fP[=false] - print the version number - -.PP -\fB\-\-tls\fP[=false] - enable TLS for request - -.PP -\fB\-\-tls\-ca\-cert\fP="$HELM\_HOME/ca.pem" - path to TLS CA certificate file - -.PP -\fB\-\-tls\-cert\fP="$HELM\_HOME/cert.pem" - path to TLS certificate file - -.PP -\fB\-\-tls\-key\fP="$HELM\_HOME/key.pem" - path to TLS key file - -.PP -\fB\-\-tls\-verify\fP[=false] - enable TLS for request and verify remote - - -.SH OPTIONS INHERITED FROM PARENT COMMANDS -.PP -\fB\-\-debug\fP[=false] - enable verbose output - -.PP -\fB\-\-home\fP="~/.helm" - location of your Helm config. Overrides $HELM\_HOME - -.PP -\fB\-\-host\fP="localhost:44134" - address of tiller. Overrides $HELM\_HOST - -.PP -\fB\-\-kube\-context\fP="" - name of the kubeconfig context to use - -.PP -\fB\-\-tiller\-namespace\fP="kube\-system" - namespace of tiller - - -.SH SEE ALSO -.PP -\fBhelm(1)\fP - - -.SH HISTORY -.PP -19\-May\-2017 Auto generated by spf13/cobra diff --git a/docs/plugins.md b/docs/plugins.md deleted file mode 100644 index 82bcfe33bab..00000000000 --- a/docs/plugins.md +++ /dev/null @@ -1,201 +0,0 @@ -# The Helm Plugins Guide - -Helm 2.1.0 introduced the concept of a client-side Helm _plugin_. A plugin is a -tool that can be accessed through the `helm` CLI, but which is not part of the -built-in Helm codebase. - -Existing plugins can be found on [related](related.md#helm-plugins) section or by searching [Github](https://github.com/search?q=topic%3Ahelm-plugin&type=Repositories). - -This guide explains how to use and create plugins. - -## An Overview - -Helm plugins are add-on tools that integrate seamlessly with Helm. They provide -a way to extend the core feature set of Helm, but without requiring every new -feature to be written in Go and added to the core tool. - -Helm plugins have the following features: - -- They can be added and removed from a Helm installation without impacting the - core Helm tool. -- They can be written in any programming language. -- They integrate with Helm, and will show up in `helm help` and other places. - -Helm plugins live in `$(helm home)/plugins`. - -The Helm plugin model is partially modeled on Git's plugin model. To that end, -you may sometimes hear `helm` referred to as the _porcelain_ layer, with -plugins being the _plumbing_. This is a shorthand way of suggesting that -Helm provides the user experience and top level processing logic, while the -plugins do the "detail work" of performing a desired action. - -## Installing a Plugin - -Plugins are installed using the `$ helm plugin install ` command. You can pass in a path to a plugin on your local file system or a url of a remote VCS repo. The `helm plugin install` command clones or copies the plugin at the path/url given into `$ (helm home)/plugins` - -```console -$ helm plugin install https://github.com/technosophos/helm-template -``` - -If you have a plugin tar distribution, simply untar the plugin into the -`$(helm home)/plugins` directory. - -You can also install tarball plugins directly from url by issuing `helm plugin install http://domain/path/to/plugin.tar.gz` - -## Building Plugins - -In many ways, a plugin is similar to a chart. Each plugin has a top-level -directory, and then a `plugin.yaml` file. - -``` -$(helm home)/plugins/ - |- keybase/ - | - |- plugin.yaml - |- keybase.sh - -``` - -In the example above, the `keybase` plugin is contained inside of a directory -named `keybase`. It has two files: `plugin.yaml` (required) and an executable -script, `keybase.sh` (optional). - -The core of a plugin is a simple YAML file named `plugin.yaml`. -Here is a plugin YAML for a plugin that adds support for Keybase operations: - -``` -name: "keybase" -version: "0.1.0" -usage: "Integrate Keybase.io tools with Helm" -description: |- - This plugin provides Keybase services to Helm. -ignoreFlags: false -useTunnel: false -command: "$HELM_PLUGIN_DIR/keybase.sh" -``` - -The `name` is the name of the plugin. When Helm executes it plugin, this is the -name it will use (e.g. `helm NAME` will invoke this plugin). - -_`name` should match the directory name._ In our example above, that means the -plugin with `name: keybase` should be contained in a directory named `keybase`. - -Restrictions on `name`: - -- `name` cannot duplicate one of the existing `helm` top-level commands. -- `name` must be restricted to the characters ASCII a-z, A-Z, 0-9, `_` and `-`. - -`version` is the SemVer 2 version of the plugin. -`usage` and `description` are both used to generate the help text of a command. - -The `ignoreFlags` switch tells Helm to _not_ pass flags to the plugin. So if a -plugin is called with `helm myplugin --foo` and `ignoreFlags: true`, then `--foo` -is silently discarded. - -The `useTunnel` switch indicates that the plugin needs a tunnel to Tiller. This -should be set to `true` _anytime a plugin talks to Tiller_. It will cause Helm -to open a tunnel, and then set `$TILLER_HOST` to the right local address for that -tunnel. But don't worry: if Helm detects that a tunnel is not necessary because -Tiller is running locally, it will not create the tunnel. - -Finally, and most importantly, `command` is the command that this plugin will -execute when it is called. Environment variables are interpolated before the plugin -is executed. The pattern above illustrates the preferred way to indicate where -the plugin program lives. - -There are some strategies for working with plugin commands: - -- If a plugin includes an executable, the executable for a `command:` should be - packaged in the plugin directory. -- The `command:` line will have any environment variables expanded before - execution. `$HELM_PLUGIN_DIR` will point to the plugin directory. -- The command itself is not executed in a shell. So you can't oneline a shell script. -- Helm injects lots of configuration into environment variables. Take a look at - the environment to see what information is available. -- Helm makes no assumptions about the language of the plugin. You can write it - in whatever you prefer. -- Commands are responsible for implementing specific help text for `-h` and `--help`. - Helm will use `usage` and `description` for `helm help` and `helm help myplugin`, - but will not handle `helm myplugin --help`. - -## Downloader Plugins -By default, Helm is able to fetch Charts using HTTP/S. As of Helm 2.4.0, plugins -can have a special capability to download Charts from arbitrary sources. - -Plugins shall declare this special capability in the `plugin.yaml` file (top level): - -``` -downloaders: -- command: "bin/mydownloader" - protocols: - - "myprotocol" - - "myprotocols" -``` - -If such plugin is installed, Helm can interact with the repository using the specified -protocol scheme by invoking the `command`. The special repository shall be added -similarly to the regular ones: `helm repo add favorite myprotocol://example.com/` -The rules for the special repos are the same to the regular ones: Helm must be able -to download the `index.yaml` file in order to discover and cache the list of -available Charts. - -The defined command will be invoked with the following scheme: -`command certFile keyFile caFile full-URL`. The SSL credentials are coming from the -repo definition, stored in `$HELM_HOME/repository/repositories.yaml`. Downloader -plugin is expected to dump the raw content to stdout and report errors on stderr. - -## Environment Variables - -When Helm executes a plugin, it passes the outer environment to the plugin, and -also injects some additional environment variables. - -Variables like `KUBECONFIG` are set for the plugin if they are set in the -outer environment. - -The following variables are guaranteed to be set: - -- `HELM_PLUGIN`: The path to the plugins directory -- `HELM_PLUGIN_NAME`: The name of the plugin, as invoked by `helm`. So - `helm myplug` will have the short name `myplug`. -- `HELM_PLUGIN_DIR`: The directory that contains the plugin. -- `HELM_BIN`: The path to the `helm` command (as executed by the user). -- `HELM_HOME`: The path to the Helm home. -- `HELM_PATH_*`: Paths to important Helm files and directories are stored in - environment variables prefixed by `HELM_PATH`. -- `TILLER_HOST`: The `domain:port` to Tiller. If a tunnel is created, this - will point to the local endpoint for the tunnel. Otherwise, it will point - to `$HELM_HOST`, `--host`, or the default host (according to Helm's rules of - precedence). - -While `HELM_HOST` _may_ be set, there is no guarantee that it will point to the -correct Tiller instance. This is done to allow plugin developer to access -`HELM_HOST` in its raw state when the plugin itself needs to manually configure -a connection. - -## A Note on `useTunnel` - -If a plugin specifies `useTunnel: true`, Helm will do the following (in order): - -1. Parse global flags and the environment -2. Create the tunnel -3. Set `TILLER_HOST` -4. Execute the plugin -5. Close the tunnel - -The tunnel is removed as soon as the `command` returns. So, for example, a -command cannot background a process and assume that that process will be able -to use the tunnel. - -## A Note on Flag Parsing - -When executing a plugin, Helm will parse global flags for its own use. Some of -these flags are _not_ passed on to the plugin. - -- `--debug`: If this is specified, `$HELM_DEBUG` is set to `1` -- `--home`: This is converted to `$HELM_HOME` -- `--host`: This is converted to `$HELM_HOST` -- `--kube-context`: This is simply dropped. If your plugin uses `useTunnel`, this - is used to set up the tunnel for you. - -Plugins _should_ display help text and then exit for `-h` and `--help`. In all -other cases, plugins may use flags as appropriate. diff --git a/docs/provenance.md b/docs/provenance.md deleted file mode 100644 index 331074e8ca2..00000000000 --- a/docs/provenance.md +++ /dev/null @@ -1,277 +0,0 @@ -# Helm Provenance and Integrity - -Helm has provenance tools which help chart users verify the integrity and origin -of a package. Using industry-standard tools based on PKI, GnuPG, and well-respected -package managers, Helm can generate and verify signature files. - -## Overview - -Integrity is established by comparing a chart to a provenance record. Provenance -records are stored in _provenance files_, which are stored alongside a packaged -chart. For example, if a chart is named `myapp-1.2.3.tgz`, its provenance file -will be `myapp-1.2.3.tgz.prov`. - -Provenance files are generated at packaging time (`helm package --sign ...`), and -can be checked by multiple commands, notable `helm install --verify`. - -## The Workflow - -This section describes a potential workflow for using provenance data effectively. - -Prerequisites: - -- A valid PGP keypair in a binary (not ASCII-armored) format -- The `helm` command line tool -- GnuPG command line tools (optional) -- Keybase command line tools (optional) - -**NOTE:** If your PGP private key has a passphrase, you will be prompted to enter -that passphrase for any commands that support the `--sign` option. - -Creating a new chart is the same as before: - -``` -$ helm create mychart -Creating mychart -``` - -Once ready to package, add the `--sign` flag to `helm package`. Also, specify -the name under which the signing key is known and the keyring containing the corresponding private key: - -``` -$ helm package --sign --key 'helm signing key' --keyring path/to/keyring.secret mychart -``` - -**TIP:** for GnuPG users, your secret keyring is in `~/.gnupg/secring.gpg`. You can -use `gpg --list-secret-keys` to list the keys you have. - -**Warning:** the GnuPG v2 store your secret keyring using a new format 'kbx' on the default location '~/.gnupg/pubring.kbx'. Please use the following command to convert your keyring to the legacy gpg format: - -``` -$ gpg --export-secret-keys >~/.gnupg/secring.gpg -``` - -At this point, you should see both `mychart-0.1.0.tgz` and `mychart-0.1.0.tgz.prov`. -Both files should eventually be uploaded to your desired chart repository. - -You can verify a chart using `helm verify`: - -``` -$ helm verify mychart-0.1.0.tgz -``` - -A failed verification looks like this: - -``` -$ helm verify topchart-0.1.0.tgz -Error: sha256 sum does not match for topchart-0.1.0.tgz: "sha256:1939fbf7c1023d2f6b865d137bbb600e0c42061c3235528b1e8c82f4450c12a7" != "sha256:5a391a90de56778dd3274e47d789a2c84e0e106e1a37ef8cfa51fd60ac9e623a" -``` - -To verify during an install, use the `--verify` flag. - -``` -$ helm install --verify mychart-0.1.0.tgz -``` - -If the keyring (containing the public key associated with the signed chart) is not in the default location, you may need to point to the -keyring with `--keyring PATH` as in the `helm package` example. - -If verification fails, the install will be aborted before the chart is even pushed -up to Tiller. - -### Using Keybase.io credentials - -The [Keybase.io](https://keybase.io) service makes it easy to establish a chain of -trust for a cryptographic identity. Keybase credentials can be used to sign charts. - -Prerequisites: - -- A configured Keybase.io account -- GnuPG installed locally -- The `keybase` CLI installed locally - -#### Signing packages - -The first step is to import your keybase keys into your local GnuPG keyring: - -``` -$ keybase pgp export -s | gpg --import -``` - -This will convert your Keybase key into the OpenPGP format, and then import it -locally into your `~/.gnupg/secring.gpg` file. - -You can double check by running `gpg --list-secret-keys`. - -``` -$ gpg --list-secret-keys 1 â†ĩ -/Users/mattbutcher/.gnupg/secring.gpg -------------------------------------- -sec 2048R/1FC18762 2016-07-25 -uid technosophos (keybase.io/technosophos) -ssb 2048R/D125E546 2016-07-25 -``` - -Note that your secret key will have an identifier string: - -``` -technosophos (keybase.io/technosophos) -``` - -That is the full name of your key. - -Next, you can package and sign a chart with `helm package`. Make sure you use at -least part of that name string in `--key`. - -``` -$ helm package --sign --key technosophos --keyring ~/.gnupg/secring.gpg mychart -``` - -As a result, the `package` command should produce both a `.tgz` file and a `.tgz.prov` -file. - -#### Verifying packages - -You can also use a similar technique to verify a chart signed by someone else's -Keybase key. Say you want to verify a package signed by `keybase.io/technosophos`. -To do this, use the `keybase` tool: - -``` -$ keybase follow technosophos -$ keybase pgp pull -``` - -The first command above tracks the user `technosophos`. Next `keybase pgp pull` -downloads the OpenPGP keys of all of the accounts you follow, placing them in -your GnuPG keyring (`~/.gnupg/pubring.gpg`). - -At this point, you can now use `helm verify` or any of the commands with a `--verify` -flag: - -``` -$ helm verify somechart-1.2.3.tgz -``` - -### Reasons a chart may not verify - -These are common reasons for failure. - -- The prov file is missing or corrupt. This indicates that something is misconfigured - or that the original maintainer did not create a provenance file. -- The key used to sign the file is not in your keyring. This indicate that the - entity who signed the chart is not someone you've already signaled that you trust. -- The verification of the prov file failed. This indicates that something is wrong - with either the chart or the provenance data. -- The file hashes in the provenance file do not match the hash of the archive file. This - indicates that the archive has been tampered with. - -If a verification fails, there is reason to distrust the package. - -## The Provenance File -The provenance file contains a chart’s YAML file plus several pieces of -verification information. Provenance files are designed to be automatically -generated. - - -The following pieces of provenance data are added: - - -* The chart file (Chart.yaml) is included to give both humans and tools an easy - view into the contents of the chart. -* The signature (SHA256, just like Docker) of the chart package (the .tgz file) - is included, and may be used to verify the integrity of the chart package. -* The entire body is signed using the algorithm used by PGP (see - [http://keybase.io] for an emerging way of making crypto signing and - verification easy). - -The combination of this gives users the following assurances: - -* The package itself has not been tampered with (checksum package tgz). -* The entity who released this package is known (via the GnuPG/PGP signature). - -The format of the file looks something like this: - -``` ------BEGIN PGP SIGNED MESSAGE----- -name: nginx -description: The nginx web server as a replication controller and service pair. -version: 0.5.1 -keywords: - - https - - http - - web server - - proxy -source: -- https://github.com/foo/bar -home: http://nginx.com - -... -files: - nginx-0.5.1.tgz: “sha256:9f5270f50fc842cfcb717f817e95178f” ------BEGIN PGP SIGNATURE----- -Version: GnuPG v1.4.9 (GNU/Linux) - -iEYEARECAAYFAkjilUEACgQkB01zfu119ZnHuQCdGCcg2YxF3XFscJLS4lzHlvte -WkQAmQGHuuoLEJuKhRNo+Wy7mhE7u1YG -=eifq ------END PGP SIGNATURE----- -``` - -Note that the YAML section contains two documents (separated by `...\n`). The -first is the Chart.yaml. The second is the checksums, a map of filenames to -SHA-256 digests (value shown is fake/truncated) - -The signature block is a standard PGP signature, which provides [tamper -resistance](http://www.rossde.com/PGP/pgp_signatures.html). - -## Chart Repositories - -Chart repositories serve as a centralized collection of Helm charts. - -Chart repositories must make it possible to serve provenance files over HTTP via -a specific request, and must make them available at the same URI path as the chart. - -For example, if the base URL for a package is `https://example.com/charts/mychart-1.2.3.tgz`, -the provenance file, if it exists, MUST be accessible at `https://example.com/charts/mychart-1.2.3.tgz.prov`. - -From the end user's perspective, `helm install --verify myrepo/mychart-1.2.3` -should result in the download of both the chart and the provenance file with no -additional user configuration or action. - -## Establishing Authority and Authenticity - -When dealing with chain-of-trust systems, it is important to be able to -establish the authority of a signer. Or, to put this plainly, the system -above hinges on the fact that you trust the person who signed the chart. -That, in turn, means you need to trust the public key of the signer. - -One of the design decisions with Kubernetes Helm has been that the Helm -project would not insert itself into the chain of trust as a necessary -party. We don't want to be "the certificate authority" for all chart -signers. Instead, we strongly favor a decentralized model, which is part -of the reason we chose OpenPGP as our foundational technology. -So when it comes to establishing authority, we have left this -step more-or-less undefined in Helm 2.0.0. - -However, we have some pointers and recommendations for those interested -in using the provenance system: - -- The [Keybase](https://keybase.io) platform provides a public - centralized repository for trust information. - - You can use Keybase to store your keys or to get the public keys of others. - - Keybase also has fabulous documentation available - - While we haven't tested it, Keybase's "secure website" feature could - be used to serve Helm charts. -- The [official Kubernetes Charts project](https://github.com/kubernetes/charts) - is trying to solve this problem for the official chart repository. - - There is a long issue there [detailing the current thoughts](https://github.com/kubernetes/charts/issues/23). - - The basic idea is that an official "chart reviewer" signs charts with - her or his key, and the resulting provenance file is then uploaded - to the chart repository. - - There has been some work on the idea that a list of valid signing - keys may be included in the `index.yaml` file of a repository. - -Finally, chain-of-trust is an evolving feature of Helm, and some -community members have proposed adapting part of the OSI model for -signatures. This is an open line of inquiry in the Helm team. If you're -interested, jump on in. diff --git a/docs/quickstart.md b/docs/quickstart.md deleted file mode 100644 index 52a7c800f9e..00000000000 --- a/docs/quickstart.md +++ /dev/null @@ -1,137 +0,0 @@ -# Quickstart Guide - -This guide covers how you can quickly get started using Helm. - -## Prerequisites - -The following prerequisites are required for a successful and properly secured use of Helm. - -1. A Kubernetes cluster -2. Deciding what security configurations to apply to your installation, if any -3. Installing and configuring Helm and Tiller, the cluster-side service. - - -### Install Kubernetes or have access to a cluster -- You must have Kubernetes installed. For the latest release of Helm, we recommend the latest stable release of Kubernetes, which in most cases is the second-latest minor release. -- You should also have a local configured copy of `kubectl`. - -NOTE: Kubernetes versions prior to 1.6 have limited or no support for role-based access controls (RBAC). - -Helm will figure out where to install Tiller by reading your Kubernetes -configuration file (usually `$HOME/.kube/config`). This is the same file -that `kubectl` uses. - -To find out which cluster Tiller would install to, you can run -`kubectl config current-context` or `kubectl cluster-info`. - -```console -$ kubectl config current-context -my-cluster -``` - -### Understand your Security Context - -As with all powerful tools, ensure you are installing it correctly for your scenario. - -If you're using Helm on a cluster that you completely control, like minikube or a cluster on a private network in which sharing is not a concern, the default installation -- which applies no security configuration -- is fine, and it's definitely the easiest. To install Helm without additional security steps, [install Helm](#Install-Helm) and then [initialize Helm](#initialize-helm-and-install-tiller). - -However, if your cluster is exposed to a larger network or if you share your cluster with others -- production clusters fall into this category -- you must take extra steps to secure your installation to prevent careless or malicious actors from damaging the cluster or its data. To apply configurations that secure Helm for use in production environments and other multi-tenant scenarios, see [Securing a Helm installation](securing_installation.md) - -If your cluster has Role-Based Access Control (RBAC) enabled, you may want -to [configure a service account and rules](rbac.md) before proceeding. - -## Install Helm - -Download a binary release of the Helm client. You can use tools like -`homebrew`, or look at [the official releases page](https://github.com/kubernetes/helm/releases). - -For more details, or for other options, see [the installation -guide](install.md). - -## Initialize Helm and Install Tiller - -Once you have Helm ready, you can initialize the local CLI and also -install Tiller into your Kubernetes cluster in one step: - -```console -$ helm init -``` - -This will install Tiller into the Kubernetes cluster you saw with -`kubectl config current-context`. - -**TIP:** Want to install into a different cluster? Use the -`--kube-context` flag. - -**TIP:** When you want to upgrade Tiller, just run `helm init --upgrade`. - -By default, when Tiller is installed,it does not have authentication enabled. -To learn more about configuring strong TLS authentication for Tiller, consult -[the Tiller TLS guide](tiller_ssl.md). - -## Install an Example Chart - -To install a chart, you can run the `helm install` command. Helm has -several ways to find and install a chart, but the easiest is to use one -of the official `stable` charts. - -```console -$ helm repo update # Make sure we get the latest list of charts -$ helm install stable/mysql -Released smiling-penguin -``` - -In the example above, the `stable/mysql` chart was released, and the name of -our new release is `smiling-penguin`. You get a simple idea of the -features of this MySQL chart by running `helm inspect stable/mysql`. - -Whenever you install a chart, a new release is created. So one chart can -be installed multiple times into the same cluster. And each can be -independently managed and upgraded. - -The `helm install` command is a very powerful command with many -capabilities. To learn more about it, check out the [Using Helm -Guide](using_helm.md) - -## Learn About Releases - -It's easy to see what has been released using Helm: - -```console -$ helm ls -NAME VERSION UPDATED                   STATUS   CHART -smiling-penguin 1 Wed Sep 28 12:59:46 2016 DEPLOYED mysql-0.1.0 -``` - -The `helm list` function will show you a list of all deployed releases. - -## Uninstall a Release - -To uninstall a release, use the `helm delete` command: - -```console -$ helm delete smiling-penguin -Removed smiling-penguin -``` - -This will uninstall `smiling-penguin` from Kubernetes, but you will -still be able to request information about that release: - -```console -$ helm status smiling-penguin -Status: DELETED -... -``` - -Because Helm tracks your releases even after you've deleted them, you -can audit a cluster's history, and even undelete a release (with `helm -rollback`). - -## Reading the Help Text - -To learn more about the available Helm commands, use `helm help` or type -a command followed by the `-h` flag: - -```console -$ helm get -h -``` diff --git a/docs/rbac.md b/docs/rbac.md deleted file mode 100644 index 2a3dfe7a0ba..00000000000 --- a/docs/rbac.md +++ /dev/null @@ -1,281 +0,0 @@ -# Role-based Access Control - -In Kubernetes, granting a role to an application-specific service account is a best practice to ensure that your application is operating in the scope that you have specified. Read more about service account permissions [in the official Kubernetes docs](https://kubernetes.io/docs/admin/authorization/rbac/#service-account-permissions). - -Bitnami also has a fantastic guide for [configuring RBAC in your cluster](https://docs.bitnami.com/kubernetes/how-to/configure-rbac-in-your-kubernetes-cluster/) that takes you through RBAC basics. - -This guide is for users who want to restrict Tiller's capabilities to install resources to certain namespaces, or to grant a Helm client running access to a Tiller instance. - -## Tiller and Role-based Access Control - -You can add a service account to Tiller using the `--service-account ` flag while you're configuring Helm. As a prerequisite, you'll have to create a role binding which specifies a [role](https://kubernetes.io/docs/admin/authorization/rbac/#role-and-clusterrole) and a [service account](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) name that have been set up in advance. - -Once you have satisfied the pre-requisite and have a service account with the correct permissions, you'll run a command like this: `helm init --service-account ` - -### Example: Service account with cluster-admin role - -```console -$ kubectl create serviceaccount tiller --namespace kube-system -serviceaccount "tiller" created -``` - -In `rbac-config.yaml`: - -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: tiller - namespace: kube-system ---- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRoleBinding -metadata: - name: tiller -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: cluster-admin -subjects: - - kind: ServiceAccount - name: tiller - namespace: kube-system -``` - -_Note: The cluster-admin role is created by default in a Kubernetes cluster, so you don't have to define it explicitly._ - -```console -$ kubectl create -f rbac-config.yaml -serviceaccount "tiller" created -clusterrolebinding "tiller" created -$ helm init --service-account tiller -``` - -### Example: Deploy Tiller in a namespace, restricted to deploying resources only in that namespace - -In the example above, we gave Tiller admin access to the entire cluster. You are not at all required to give Tiller cluster-admin access for it to work. Instead of specifying a ClusterRole or a ClusterRoleBinding, you can specify a Role and RoleBinding to limit Tiller's scope to a particular namespace. - -```console -$ kubectl create namespace tiller-world -namespace "tiller-world" created -$ kubectl create serviceaccount tiller --namespace tiller-world -serviceaccount "tiller" created -``` - -Define a Role that allows Tiller to manage all resources in `tiller-world` like in `role-tiller.yaml`: - -```yaml -kind: Role -apiVersion: rbac.authorization.k8s.io/v1beta1 -metadata: - name: tiller-manager - namespace: tiller-world -rules: -- apiGroups: ["", "extensions", "apps"] - resources: ["*"] - verbs: ["*"] -``` - -```console -$ kubectl create -f role-tiller.yaml -role "tiller-manager" created -``` - -In `rolebinding-tiller.yaml`, - -```yaml -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1beta1 -metadata: - name: tiller-binding - namespace: tiller-world -subjects: -- kind: ServiceAccount - name: tiller - namespace: tiller-world -roleRef: - kind: Role - name: tiller-manager - apiGroup: rbac.authorization.k8s.io -``` - -```console -$ kubectl create -f rolebinding-tiller.yaml -rolebinding "tiller-binding" created -``` - -Afterwards you can run `helm init` to install Tiller in the `tiller-world` namespace. - -```console -$ helm init --service-account tiller --tiller-namespace tiller-world -$HELM_HOME has been configured at /Users/awesome-user/.helm. - -Tiller (the Helm server side component) has been installed into your Kubernetes Cluster. -Happy Helming! - -$ helm install nginx --tiller-namespace tiller-world --namespace tiller-world -NAME: wayfaring-yak -LAST DEPLOYED: Mon Aug 7 16:00:16 2017 -NAMESPACE: tiller-world -STATUS: DEPLOYED - -RESOURCES: -==> v1/Pod -NAME READY STATUS RESTARTS AGE -wayfaring-yak-alpine 0/1 ContainerCreating 0 0s -``` - -### Example: Deploy Tiller in a namespace, restricted to deploying resources in another namespace - -In the example above, we gave Tiller admin access to the namespace it was deployed inside. Now, let's limit Tiller's scope to deploy resources in a different namespace! - -For example, let's install Tiller in the namespace `myorg-system` and allow Tiller to deploy resources in the namespace `myorg-users`. - -```console -$ kubectl create namespace myorg-system -namespace "myorg-system" created -$ kubectl create serviceaccount tiller --namespace myorg-system -serviceaccount "tiller" created -``` - -Define a Role that allows Tiller to manage all resources in `myorg-users` like in `role-tiller.yaml`: - -```yaml -kind: Role -apiVersion: rbac.authorization.k8s.io/v1beta1 -metadata: - name: tiller-manager - namespace: myorg-users -rules: -- apiGroups: ["", "extensions", "apps"] - resources: ["*"] - verbs: ["*"] -``` - -```console -$ kubectl create -f role-tiller.yaml -role "tiller-manager" created -``` - -Bind the service account to that role. In `rolebinding-tiller.yaml`, - -```yaml -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1beta1 -metadata: - name: tiller-binding - namespace: myorg-users -subjects: -- kind: ServiceAccount - name: tiller - namespace: myorg-system -roleRef: - kind: Role - name: tiller-manager - apiGroup: rbac.authorization.k8s.io -``` - -```console -$ kubectl create -f rolebinding-tiller.yaml -rolebinding "tiller-binding" created -``` - -We'll also need to grant Tiller access to read configmaps in myorg-system so it can store release information. In `role-tiller-myorg-system.yaml`: - -```yaml -kind: Role -apiVersion: rbac.authorization.k8s.io/v1beta1 -metadata: - namespace: myorg-system - name: tiller-manager -rules: -- apiGroups: ["", "extensions", "apps"] - resources: ["configmaps"] - verbs: ["*"] -``` - -```console -$ kubectl create -f role-tiller-myorg-system.yaml -role "tiller-manager" created -``` - -And the respective role binding. In `rolebinding-tiller-myorg-system.yaml`: - -```yaml -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1beta1 -metadata: - name: tiller-binding - namespace: myorg-system -subjects: -- kind: ServiceAccount - name: tiller - namespace: myorg-system -roleRef: - kind: Role - name: tiller-manager - apiGroup: rbac.authorization.k8s.io -``` - -```console -$ kubectl create -f rolebinding-tiller-myorg-system.yaml -rolebinding "tiller-binding" created -``` - -## Helm and Role-based Access Control - -When running a Helm client in a pod, in order for the Helm client to talk to a Tiller instance, it will need certain privileges to be granted. Specifically, the Helm client will need to be able to create pods, forward ports and be able to list pods in the namespace where Tiller is running (so it can find Tiller). - -### Example: Deploy Helm in a namespace, talking to Tiller in another namespace - -In this example, we will assume Tiller is running in a namespace called `tiller-world` and that the Helm client is running in a namespace called `helm-world`. By default, Tiller is running in the `kube-system` namespace. - -In `helm-user.yaml`: - -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: helm - namespace: helm-world ---- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: Role -metadata: - name: tiller-user - namespace: tiller-world -rules: -- apiGroups: - - "" - resources: - - pods/portforward - verbs: - - create -- apiGroups: - - "" - resources: - - pods - verbs: - - list ---- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: RoleBinding -metadata: - name: tiller-user-binding - namespace: tiller-world -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: tiller-user -subjects: -- kind: ServiceAccount - name: helm - namespace: helm-world -``` - -```console -$ kubectl create -f helm-user.yaml -serviceaccount "helm" created -role "tiller-user" created -rolebinding "tiller-user-binding" created -``` diff --git a/docs/related.md b/docs/related.md deleted file mode 100644 index 997e3f01d13..00000000000 --- a/docs/related.md +++ /dev/null @@ -1,87 +0,0 @@ -# Related Projects and Documentation - -The Helm community has produced many extra tools, plugins, and documentation about -Helm. We love to hear about these projects. If you have anything you'd like to -add to this list, please open an [issue](https://github.com/kubernetes/helm/issues) -or [pull request](https://github.com/kubernetes/helm/pulls). - -## Article, Blogs, How-Tos, and Extra Documentation -- [Using Helm to Deploy to Kubernetes](https://daemonza.github.io/2017/02/20/using-helm-to-deploy-to-kubernetes/) -- [Honestbee's Helm Chart Conventions](https://gist.github.com/so0k/f927a4b60003cedd101a0911757c605a) -- [Deploying Kubernetes Applications with Helm](http://cloudacademy.com/blog/deploying-kubernetes-applications-with-helm/) -- [Releasing backward-incompatible changes: Kubernetes, Jenkins, Prometheus Operator, Helm and Traefik](https://medium.com/@enxebre/releasing-backward-incompatible-changes-kubernetes-jenkins-plugin-prometheus-operator-helm-self-6263ca61a1b1#.e0c7elxhq) -- [CI/CD with Kubernetes, Helm & Wercker ](http://www.slideshare.net/Diacode/cicd-with-kubernetes-helm-wercker-madscalability) -- [The missing CI/CD Kubernetes component: Helm package manager](https://hackernoon.com/the-missing-ci-cd-kubernetes-component-helm-package-manager-1fe002aac680#.691sk2zhu) -- [The Workflow "Umbrella" Helm Chart](https://deis.com/blog/2017/workflow-chart-assembly) -- [GitLab, Consumer Driven Contracts, Helm and Kubernetes](https://medium.com/@enxebre/gitlab-consumer-driven-contracts-helm-and-kubernetes-b7235a60a1cb#.xwp1y4tgi) -- [Writing a Helm Chart](https://www.influxdata.com/packaged-kubernetes-deployments-writing-helm-chart/) -- [Creating a Helm Plugin in 3 Steps](http://technosophos.com/2017/03/21/creating-a-helm-plugin.html) - -## Video, Audio, and Podcast - -- [CI/CD with Jenkins, Kubernetes, and Helm](https://www.youtube.com/watch?v=NVoln4HdZOY): AKA "The Infamous Croc Hunter Video". -- [KubeCon2016: Delivering Kubernetes-Native Applications by Michelle Noorali](https://www.youtube.com/watch?v=zBc1goRfk3k&index=49&list=PLj6h78yzYM2PqgIGU1Qmi8nY7dqn9PCr4) -- [Helm with Michelle Noorali and Matthew Butcher](https://gcppodcast.com/post/episode-50-helm-with-michelle-noorali-and-matthew-butcher/): The official Google CloudPlatform Podcast interviews Michelle and Matt about Helm. - -## Helm Plugins - -- [helm-tiller](https://github.com/adamreese/helm-tiller) - Additional commands to work with Tiller -- [Technosophos's Helm Plugins](https://github.com/technosophos/helm-plugins) - Plugins for GitHub, Keybase, and GPG -- [helm-template](https://github.com/technosophos/helm-template) - Debug/render templates client-side -- [Helm Value Store](https://github.com/skuid/helm-value-store) - Plugin for working with Helm deployment values -- [Helm Diff](https://github.com/databus23/helm-diff) - Preview `helm upgrade` as a coloured diff -- [helm-env](https://github.com/adamreese/helm-env) - Plugin to show current environment -- [helm-last](https://github.com/adamreese/helm-last) - Plugin to show the latest release -- [helm-nuke](https://github.com/adamreese/helm-nuke) - Plugin to destroy all releases -- [helm-local](https://github.com/adamreese/helm-local) - Plugin to run Tiller as a local daemon -- [App Registry](https://github.com/app-registry/helm-plugin) - Plugin to manage charts via the [App Registry specification](https://github.com/app-registry/spec) -- [helm-secrets](https://github.com/futuresimple/helm-secrets) - Plugin to manage and store secrets safely -- [helm-edit](https://github.com/mstrzele/helm-edit) - Plugin for editing release's values -- [helm-gcs](https://github.com/nouney/helm-gcs) - Plugin to manage repositories on Google Cloud Storage -- [helm-github](https://github.com/sagansystems/helm-github) - Plugin to install Helm Charts from Github repositories -- [helm-monitor](https://github.com/ContainerSolutions/helm-monitor) - Plugin to monitor a release and rollback based on Prometheus/ElasticSearch query -- [helm-k8comp](https://github.com/cststack/k8comp) - Plugin to create Helm Charts from hiera using k8comp -- [helm-hashtag](https://github.com/balboah/helm-hashtag) - Plugin for tracking docker tag hash digests as values -- [helm-unittest](https://github.com/lrills/helm-unittest) - Plugin for unit testing chart locally with YAML - -We also encourage GitHub authors to use the [helm-plugin](https://github.com/search?q=topic%3Ahelm-plugin&type=Repositories) -tag on their plugin repositories. - -## Additional Tools - -Tools layered on top of Helm or Tiller. - -- [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 -- [Landscaper](https://github.com/Eneco/landscaper/) - "Landscaper takes a set of Helm Chart references with values (a desired state), and realizes this in a Kubernetes cluster." -- [Rudder](https://github.com/AcalephStorage/rudder) - RESTful (JSON) proxy for Tiller's API -- [Helmfile](https://github.com/roboll/helmfile) - Helmfile is a declarative spec for deploying helm charts -- [Autohelm](https://github.com/reactiveops/autohelm) - Autohelm is _another_ simple declarative spec for deploying helm charts. Written in python and supports git urls as a source for helm charts. -- [Helmsman](https://github.com/Praqma/helmsman) - Helmsman is a helm-charts-as-code tool which enables installing/upgrading/protecting/moving/deleting releases from version controlled desired state files (described in a simple TOML format). -- [Schelm](https://github.com/databus23/schelm) - Render a Helm manifest to a directory -- [Drone.io Helm Plugin](http://plugins.drone.io/ipedrazas/drone-helm/) - Run Helm inside of the Drone CI/CD system -- [Cog](https://github.com/ohaiwalt/cog-helm) - Helm chart to deploy Cog on Kubernetes -- [Monocular](https://github.com/helm/monocular) - Web UI for Helm Chart repositories -- [Helm Chart Publisher](https://github.com/luizbafilho/helm-chart-publisher) - HTTP API for publishing Helm Charts in an easy way -- [Armada](https://github.com/att-comdev/armada) - Manage prefixed releases throughout various Kubernetes namespaces, and removes completed jobs for complex deployments. Used by the [Openstack-Helm](https://github.com/openstack/openstack-helm) team. -- [ChartMuseum](https://github.com/chartmuseum/chartmuseum) - Helm Chart Repository with support for Amazon S3 and Google Cloud Storage -- [Helm.NET](https://github.com/qmfrederik/helm) - A .NET client for Tiller's API -- [Codefresh](https://codefresh.io) - Kubernetes native CI/CD and management platform with UI dashboards for managing Helm charts and releases - -## Helm Included - -Platforms, distributions, and services that include Helm support. - -- [Kubernetic](https://kubernetic.com/) - Kubernetes Desktop Client -- [Cabin](http://www.skippbox.com/cabin/) - Mobile App for Managing Kubernetes -- [Qstack](https://qstack.com) -- [Fabric8](https://fabric8.io) - Integrated development platform for Kubernetes -- [Jenkins X](http://jenkins-x.io/) - open source automated CI/CD for Kubernetes which uses Helm for [promoting](http://jenkins-x.io/about/features/#promotion) applications through [environments via GitOps](http://jenkins-x.io/about/features/#environments) - -## Misc - -Grab bag of useful things for Chart authors and Helm users - -- [Await](https://github.com/saltside/await) - Docker image to "await" different conditions--especially useful for init containers. [More Info](http://blog.slashdeploy.com/2017/02/16/introducing-await/) diff --git a/docs/release_checklist.md b/docs/release_checklist.md deleted file mode 100644 index 26506985ca5..00000000000 --- a/docs/release_checklist.md +++ /dev/null @@ -1,267 +0,0 @@ -# Release Checklist - -**IMPORTANT**: If your experience deviates from this document, please document the changes to keep it up-to-date. - -## A Maintainer's Guide to Releasing Helm - -So you're in charge of a new release for helm? Cool. Here's what to do... - -![TODO: Nothing](images/nothing.png) - -Just kidding! :trollface: - -All releases will be of the form vX.Y.Z where X is the major version number, Y is the minor version number and Z is the patch release number. This project strictly follows [semantic versioning](http://semver.org/) so following this step is critical. - -It is important to note that this document assumes that the git remote in your repository that corresponds to "https://github.com/kubernetes/helm" is named "upstream". If yours is not (for example, if you've chosen to name it "origin" or something similar instead), be sure to adjust the listed snippets for your local environment accordingly. If you are not sure what your upstream remote is named, use a command like `git remote -v` to find out. - -If you don't have an upstream remote, you can add one easily using something like: - -```shell -git remote add upstream git@github.com:kubernetes/helm.git -``` - -In this doc, we are going to reference a few environment variables as well, which you may want to set for convenience. For major/minor releases, use the following: - -```shell -export RELEASE_NAME=vX.Y.0 -export RELEASE_BRANCH_NAME="release-$RELEASE_NAME" -export RELEASE_CANDIDATE_NAME="$RELEASE_NAME-rc1" -``` - -If you are creating a patch release, you may want to use the following instead: - -```shell -export PREVIOUS_PATCH_RELEASE=vX.Y.Z -export RELEASE_NAME=vX.Y.Z+1 -export RELEASE_BRANCH_NAME="release-X.Y" -export RELEASE_CANDIDATE_NAME="$RELEASE_NAME-rc1" -``` - -## 1. Create the Release Branch - -### Major/Minor Releases - -Major releases are for new feature additions and behavioral changes *that break backwards compatibility*. Minor releases are for new feature additions that do not break backwards compatibility. To create a major or minor release, start by creating a `release-vX.Y.0` branch from master. - -```shell -git fetch upstream -git checkout upstream/master -git checkout -b $RELEASE_BRANCH_NAME -``` - -This new branch is going to be the base for the release, which we are going to iterate upon later. - -### Patch releases - -Patch releases are a few critical cherry-picked fixes to existing releases. Start by creating a `release-vX.Y.Z` branch from the latest patch release. - -```shell -git fetch upstream --tags -git checkout $PREVIOUS_PATCH_RELEASE -git checkout -b $RELEASE_BRANCH_NAME -``` - -From here, we can cherry-pick the commits we want to bring into the patch release: - -```shell -# get the commits ids we want to cherry-pick -git log --oneline -# cherry-pick the commits starting from the oldest one, without including merge commits -git cherry-pick -x -git cherry-pick -x -``` - -This new branch is going to be the base for the release, which we are going to iterate upon later. - -## 2. Change the Version Number in Git - -When doing a minor release, make sure to update pkg/version/version.go with the new release version. - -```shell -$ git diff pkg/version/version.go -diff --git a/pkg/version/version.go b/pkg/version/version.go -index 2109a0a..6f5a1a4 100644 ---- a/pkg/version/version.go -+++ b/pkg/version/version.go -@@ -26,7 +26,7 @@ var ( - // Increment major number for new feature additions and behavioral changes. - // Increment minor number for bug fixes and performance enhancements. - // Increment patch number for critical fixes to existing releases. -- Version = "v2.6" -+ Version = "v2.7" - - // BuildMetadata is extra build time data - BuildMetadata = "unreleased" -``` - -The README stores links to the latest release for helm. We want to change the version to the first release candidate which we are releasing (more on that in step 5). - -```shell -$ git diff README.md -diff --git a/README.md b/README.md -index 022afd79..547839e2 100644 ---- a/README.md -+++ b/README.md -@@ -34,10 +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.7.0-darwin-amd64.tar.gz) --- [Linux](https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz) --- [Linux 32-bit](https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-386.tar.gz) --- [Windows](https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-windows-amd64.tar.gz) -+- [OSX](https://kubernetes-helm.storage.googleapis.com/helm-v2.8.0-darwin-amd64.tar.gz) -+- [Linux](https://kubernetes-helm.storage.googleapis.com/helm-v2.8.0-linux-amd64.tar.gz) -+- [Linux 32-bit](https://kubernetes-helm.storage.googleapis.com/helm-v2.8.0-linux-386.tar.gz) -+- [Windows](https://kubernetes-helm.storage.googleapis.com/helm-v2.8.0-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`. -``` - -For patch releases, the old version number will be the latest patch release, so just bump the patch number, incrementing Z by one. - -```shell -git add . -git commit -m "bump version to $RELEASE_CANDIDATE_NAME" -``` - -## 3. Commit and Push the Release Branch - -In order for others to start testing, we can now push the release branch upstream and start the test process. - -```shell -git push upstream $RELEASE_BRANCH_NAME -``` - -Make sure to check [helm on CircleCI](https://circleci.com/gh/kubernetes/helm) and make sure the release passed CI before proceeding. - -If anyone is available, let others peer-review the branch before continuing to ensure that all the proper changes have been made and all of the commits for the release are there. - -## 4. Create a Release Candidate - -Now that the release branch is out and ready, it is time to start creating and iterating on release candidates. - -```shell -git tag --sign --annotate "${RELEASE_CANDIDATE_NAME}" --message "Helm release ${RELEASE_CANDIDATE_NAME}" -git push upstream $RELEASE_CANDIDATE_NAME -``` - -CircleCI will automatically create a tagged release image and client binary to test with. - -For testers, the process to start testing after CircleCI finishes building the artifacts involves the following steps to grab the client from Google Cloud Storage: - -linux/amd64, using /bin/bash: - -```shell -wget https://kubernetes-helm.storage.googleapis.com/helm-$RELEASE_CANDIDATE_NAME-linux-amd64.tar.gz -``` - -darwin/amd64, using Terminal.app: - -```shell -wget https://kubernetes-helm.storage.googleapis.com/helm-$RELEASE_CANDIDATE_NAME-darwin-amd64.tar.gz -``` - -windows/amd64, using PowerShell: - -```shell -PS C:\> Invoke-WebRequest -Uri "https://kubernetes-helm.storage.googleapis.com/helm-$RELEASE_CANDIDATE_NAME-windows-amd64.tar.gz" -OutFile "helm-$ReleaseCandidateName-windows-amd64.tar.gz" -``` - -Then, unpack and move the binary to somewhere on your $PATH, or move it somewhere and add it to your $PATH (e.g. /usr/local/bin/helm for linux/macOS, C:\Program Files\helm\helm.exe for Windows). - -## 5. Iterate on Successive Release Candidates - -Spend several days explicitly investing time and resources to try and break helm in every possible way, documenting any findings pertinent to the release. This time should be spent testing and finding ways in which the release might have caused various features or upgrade environments to have issues, not coding. During this time, the release is in code freeze, and any additional code changes will be pushed out to the next release. - -During this phase, the $RELEASE_BRANCH_NAME branch will keep evolving as you will produce new release candidates. The frequency of new candidates is up to the release manager: use your best judgement taking into account the severity of reported issues, testers' availability, and the release deadline date. Generally speaking, it is better to let a release roll over the deadline than to ship a broken release. - -Each time you'll want to produce a new release candidate, you will start by adding commits to the branch by cherry-picking from master: - -```shell -git cherry-pick -x -``` - -You will also want to update the release version number and the CHANGELOG as we did in steps 2 and 3 as separate commits. - -After that, tag it and notify users of the new release candidate: - -```shell -export RELEASE_CANDIDATE_NAME="$RELEASE_NAME-rc2" -git tag --sign --annotate "${RELEASE_CANDIDATE_NAME}" --message "Helm release ${RELEASE_CANDIDATE_NAME}" -git push upstream $RELEASE_CANDIDATE_NAME -``` - -From here on just repeat this process, continuously testing until you're happy with the release candidate. - -## 6. Finalize the Release - -When you're finally happy with the quality of a release candidate, you can move on and create the real thing. Double-check one last time to make sure everything is in order, then finally push the release tag. - -```shell -git checkout $RELEASE_BRANCH_NAME -git tag --sign --annotate "${RELEASE_NAME}" --message "Helm release ${RELEASE_NAME}" -git push upstream $RELEASE_NAME -``` - -## 7. Write the Release Notes - -We will auto-generate a changelog based on the commits that occurred during a release cycle, but it is usually more beneficial to the end-user if the release notes are hand-written by a human being/marketing team/dog. - -If you're releasing a major/minor release, listing notable user-facing features is usually sufficient. For patch releases, do the same, but make note of the symptoms and who is affected. - -An example release note for a minor release would look like this: - -```markdown -## vX.Y.Z - -Helm vX.Y.Z is a feature release. This release, we focused on . Users are encouraged to upgrade for the best experience. - -The community keeps growing, and we'd love to see you there. - -- Join the discussion in [Kubernetes Slack](https://slack.k8s.io/): - - `#helm-users` for questions and just to hang out - - `#helm-dev` for discussing PRs, code, and bugs -- Hang out at the Public Developer Call: Thursday, 9:30 Pacific via [Zoom](https://zoom.us/j/4526666954) -- Test, debug, and contribute charts: [GitHub/kubernetes/charts](https://github.com/kubernetes/charts) - -## Installation and Upgrading - -Download Helm X.Y. The common platform binaries are here: - -- [OSX](https://storage.googleapis.com/kubernetes-helm/helm-vX.Y.Z-darwin-amd64.tar.gz) -- [Linux](https://storage.googleapis.com/kubernetes-helm/helm-vX.Y.Z-linux-amd64.tar.gz) -- [Windows](https://storage.googleapis.com/kubernetes-helm/helm-vX.Y.Z-windows-amd64.tar.gz) - -Once you have the client installed, upgrade Tiller with `helm init --upgrade`. - -The [Quickstart Guide](https://docs.helm.sh/using_helm/#quickstart-guide) will get you going from there. For **upgrade instructions** or detailed installation notes, check the [install guide](https://docs.helm.sh/using_helm/#installing-helm). You can also use a [script to install](https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get) on any system with `bash`. - -## What's Next - -- vX.Y.Z+1 will contain only bug fixes. -- vX.Y+1.Z is the next feature release. This release will focus on ... - -## Changelog - -- chore(*): bump version to v2.7.0 08c1144f5eb3e3b636d9775617287cc26e53dba4 (Adam Reese) -- fix circle not building tags f4f932fabd197f7e6d608c8672b33a483b4b76fa (Matthew Fisher) -``` - -The changelog at the bottom of the release notes can be generated with this command: - -```shell -PREVIOUS_RELEASE=vX.Y.Z -git log --no-merges --pretty=format:'- %s %H (%aN)' $RELEASE_NAME $PREVIOUS_RELEASE -``` - -Once finished, go into GitHub and edit the release notes for the tagged release with the notes written here. - -## 9. Evangelize - -Congratulations! You're done. Go grab yourself a $DRINK_OF_CHOICE. You've earned it. - -After enjoying a nice $DRINK_OF_CHOICE, go forth and announce the glad tidings of the new release in Slack and on Twitter. You should also notify any key partners in the helm community such as the homebrew formula maintainers, the owners of incubator projects (e.g. ChartMuseum) and any other interested parties. - -Optionally, write a blog post about the new release and showcase some of the new features on there! diff --git a/docs/securing_installation.md b/docs/securing_installation.md deleted file mode 100644 index 5c420242efd..00000000000 --- a/docs/securing_installation.md +++ /dev/null @@ -1,111 +0,0 @@ -# Securing your Helm Installation - -Helm is a powerful and flexible package-management and operations tool for Kubernetes. Installing it using the default installation command -- `helm init` -- quickly and easily installs **Tiller**, the server-side component with which Helm corresponds. - -This default installation applies **_no security configurations_**, however. It's completely appropriate to use this type of installation when you are working against a cluster with no or very few security concerns, such as local development with Minikube or with a cluster that is well-secured in a private network with no data-sharing or no other users or teams. If this is the case, then the default installation is fine, but remember: With great power comes great responsibility. Always use due diligence when deciding to use the default installation. - -## Who Needs Security Configurations? - -For the following types of clusters we strongly recommend that you apply the proper security configurations to Helm and Tiller to ensure the safety of the cluster, the data in it, and the network to which it is connected. - -- Clusters that are exposed to uncontrolled network environments: either untrusted network actors can access the cluster, or untrusted applications that can access the network environment. -- Clusters that are for many people to use -- _multitenant_ clusters -- as a shared environment -- Clusters that have access to or use high-value data or networks of any type - -Often, environments like these are referred to as _production grade_ or _production quality_ because the damage done to any company by misuse of the cluster can be profound for either customers, the company itself, or both. Once the risk of damage becomes high enough, you need to ensure the integrity of your cluster no matter what the actual risk. - -To configure your installation properly for your environment, you must: - -- Understand the security context of your cluster -- Choose the Best Practices you should apply to your helm installation - -The following assumes you have a Kubernetes configuration file (a _kubeconfig_ file) or one was given to you to access a cluster. - -## Understanding the Security Context of your Cluster - -`helm init` installs Tiller into the cluster in the `kube-system` namespace and without any RBAC rules applied. This is appropriate for local development and other private scenarios because it enables you to be productive immediately. It also enables you to continue running Helm with existing Kubernetes clusters that do not have role-based access control (RBAC) support until you can move your workloads to a more recent Kubernetes version. - -There are four main areas to consider when securing a tiller installation: - -1. Role-based access control, or RBAC -2. Tiller's gRPC endpoint and its usage by Helm -3. Tiller release information -4. Helm charts - -### RBAC - -Recent versions of Kubernetes employ a [role-based access control (or RBAC)](https://en.wikipedia.org/wiki/Role-based_access_control) system (as do modern operating systems) to help mitigate the damage that can done if credentials are misused or bugs exist. Even where an identity is hijacked, the identity has only so many permissions to a controlled space. This effectively adds a layer of security to limit the scope of any attack with that identity. - -Helm and Tiller are designed to install, remove, and modify logical applications that can contain many services interacting together. As a result, often its usefulness involves cluster-wide operations, which in a multitenant cluster means that great care must be taken with access to a cluster-wide Tiller installation to prevent improper activity. - -Specific users and teams -- developers, operators, system and network administrators -- will need their own portion of the cluster in which they can use Helm and Tiller without risking other portions of the cluster. This means using a Kubernetes cluster with RBAC enabled and Tiller configured to enforce them. For more information about using RBAC in Kubernetes, see [Using RBAC Authorization](rbac.md). - -#### Tiller and User Permissions - -Tiller in its current form does not provide a way to map user credentials to specific permissions within Kubernetes. When Tiller is running inside of the cluster, it operates with the permissions of its service account. If no service account name is supplied to Tiller, it runs with the default service account for that namespace. This means that all Tiller operations on that server are executed using the Tiller pod's credentials and permissions. - -To properly limit what Tiller itself can do, the standard Kubernetes RBAC mechanisms must be attached to Tiller, including Roles and RoleBindings that place explicit limits on what things a Tiller instance can install, and where. - -This situation may change in the future. While the community has several methods that might address this, at the moment performing actions using the rights of the client, instead of the rights of Tiller, is contingent upon the outcome of the Pod Identity Working Group, which has taken on the task of solving the problem in a general way. - - -### The Tiller gRPC Endpoint and TLS - -In the default installation the gRPC endpoint that Tiller offers is available inside the cluster (not external to the cluster) without authentication configuration applied. Without applying authentication, any process in the cluster can use the gRPC endpoint to perform operations inside the cluster. In a local or secured private cluster, this enables rapid usage and is normal. (When running outside the cluster, Helm authenticates through the Kubernetes API server to reach Tiller, leveraging existing Kubernetes authentication support.) - -Shared and production clusters -- for the most part -- should use Helm 2.7.2 at a minimum and configure TLS for each Tiller gRPC endpoint to ensure that within the cluster usage of gRPC endpoints is only for the properly authenticated identity for that endpoint. Doing so enables any number of Tiller instances to be deployed in any number of namespaces and yet no unauthenticated usage of any gRPC endpoint is possible. Finally, usa Helm `init` with the `--tiller-tls-verify` option to install Tiller with TLS enabled and to verify remote certificates, and all other Helm commands should use the `--tls` option. - -For more information about the proper steps to configure Tiller and use Helm properly with TLS configured, see [Using SSL between Helm and Tiller](tiller_ssl.md). - -When Helm clients are connecting from outside of the cluster, the security between the Helm client and the API server is managed by Kubernetes itself. You may want to ensure that this link is secure. Note that if you are using the TLS configuration recommended above, not even the Kubernetes API server has access to the unencrypted messages between the client and Tiller. - -### Tiller's Release Information - -For historical reasons, Tiller stores its release information in ConfigMaps. We suggest changing the default to Secrets. - -Secrets are the Kubernetes accepted mechanism for saving configuration data that is considered sensitive. While secrets don't themselves offer many protections, Kubernetes cluster management software often treats them differently than other objects. Thus, we suggest using secrets to store releases. - -Enabling this feature currently requires setting the `--storage=secret` flag in the tiller-deploy deployment. This entails directly modifying the deployment or using `helm init --override=...`, as no helm init flag is currently available to do this for you. For more information, see [Using --override](install.md#using---override). - -### Thinking about Charts - -Because of the relative longevity of Helm, the Helm chart ecosystem evolved without the immediate concern for cluster-wide control, and especially in the developer space this makes complete sense. However, charts are a kind of package that not only installs containers you may or may not have validated yourself, but it may also install into more than one namespace. - -As with all shared software, in a controlled or shared environment you must validate all software you install yourself _before_ you install it. If you have secured Tiller with TLS and have installed it with permissions to only one or a subset of namespaces, some charts may fail to install -- but in these environments, that is exactly what you want. If you need to use the chart, you may have to work with the creator or modify it yourself in order to use it securely in a multitenant cluster with proper RBAC rules applied. The `helm template` command renders the chart locally and displays the output. - -Once vetted, you can use Helm's provenance tools to [ensure the provenance and integrity of charts](provenance.md) that you use. - -### gRPC Tools and Secured Tiller Configurations - -Many very useful tools use the gRPC interface directly, and having been built against the default installation -- which provides cluster-wide access -- may fail once security configurations have been applied. RBAC policies are controlled by you or by the cluster operator, and either can be adjusted for the tool, or the tool can be configured to work properly within the constraints of specific RBAC policies applied to Tiller. The same may need to be done if the gRPC endpoint is secured: the tools need their own secure TLS configuration in order to use a specific Tiller instance. The combination of RBAC policies and a secured gRPC endpoint configured in conjunction with gRPC tools enables you to control your cluster environment as you should. - -## Best Practices for Securing Helm and Tiller - -The following guidelines reiterate the Best Practices for securing Helm and Tiller and using them correctly. - -1. Create a cluster with RBAC enabled -2. Configure each Tiller gRPC endpoint to use a separate TLS certificate -3. Release information should be a Kubernetes Secret -4. Install one Tiller per user, team, or other organizational entity with the `--service-account` flag, Roles, and RoleBindings -5. Use the `--tiller-tls-verify` option with `helm init` and the `--tls` flag with other Helm commands to enforce verification - -If these steps are followed, an example `helm init` command might look something like this: - -```bash -$ helm init \ ---tiller-tls \ ---tiller-tls-verify \ ---tiller-tls-ca-cert=ca.pem \ ---tiller-tls-cert=cert.pem \ ---tiller-tls-key=key.pem \ ---service-account=accountname -``` - -This command will start Tiller with both strong authentication over gRPC, and a service account to which RBAC policies have been applied. - - - - - - - diff --git a/docs/tiller_ssl.md b/docs/tiller_ssl.md deleted file mode 100644 index d7f0166c4f4..00000000000 --- a/docs/tiller_ssl.md +++ /dev/null @@ -1,291 +0,0 @@ -# Using SSL Between Helm and Tiller - -This document explains how to create strong SSL/TLS connections between Helm and -Tiller. The emphasis here is on creating an internal CA, and using both the -cryptographic and identity functions of SSL. - -> Support for TLS-based auth was introduced in Helm 2.3.0 - -Configuring SSL is considered an advanced topic, and knowledge of Helm and Tiller -is assumed. - -## Overview - -The Tiller authentication model uses client-side SSL certificates. Tiller itself -verifies these certificates using a certificate authority. Likewise, the client -also verifies Tiller's identity by certificate authority. - -There are numerous possible configurations for setting up certificates and authorities, -but the method we cover here will work for most situations. - -> As of Helm 2.7.2, Tiller _requires_ that the client certificate be validated -> by its CA. In prior versions, Tiller used a weaker validation strategy that -> allowed self-signed certificates. - -In this guide, we will show how to: - -- Create a private CA that is used to issue certificates for Tiller clients and - servers. -- Create a certificate for Tiller -- Create a certificate for the Helm client -- Create a Tiller instance that uses the certificate -- Configure the Helm client to use the CA and client-side certificate - -By the end of this guide, you should have a Tiller instance running that will -only accept connections from clients who can be authenticated by SSL certificate. - -## Generating Certificate Authorities and Certificates - -One way to generate SSL CAs is via the `openssl` command line tool. There are many -guides and best practices documents available online. This explanation is focused -on getting ready within a small amount of time. For production configurations, -we urge readers to read [the official documentation](https://www.openssl.org) and -consult other resources. - -### Generate a Certificate Authority - -The simplest way to generate a certificate authority is to run two commands: - -```console -$ openssl genrsa -out ./ca.key.pem 4096 -$ openssl req -key ca.key.pem -new -x509 -days 7300 -sha256 -out ca.cert.pem -extensions v3_ca -Enter pass phrase for ca.key.pem: -You are about to be asked to enter information that will be incorporated -into your certificate request. -What you are about to enter is what is called a Distinguished Name or a DN. -There are quite a few fields but you can leave some blank -For some fields there will be a default value, -If you enter '.', the field will be left blank. ------ -Country Name (2 letter code) [AU]:US -State or Province Name (full name) [Some-State]:CO -Locality Name (eg, city) []:Boulder -Organization Name (eg, company) [Internet Widgits Pty Ltd]:tiller -Organizational Unit Name (eg, section) []: -Common Name (e.g. server FQDN or YOUR name) []:tiller -Email Address []:tiller@example.com -``` - -Note that the data input above is _sample data_. You should customize to your own -specifications. - -The above will generate both a secret key and a CA. Note that these two files are -very important. The key in particular should be handled with particular care. - -Often, you will want to generate an intermediate signing key. For the sake of brevity, -we will be signing keys with our root CA. - -### Generating Certificates - -We will be generating two certificates, each representing a type of certificate: - -- One certificate is for Tiller. You will want one of these _per tiller host_ that - you run. -- One certificate is for the user. You will want one of these _per helm user_. - -Since the commands to generate these are the same, we'll be creating both at the -same time. The names will indicate their target. - -First, the Tiller key: - -```console -$ openssl genrsa -out ./tiller.key.pem 4096 -Generating RSA private key, 4096 bit long modulus -..........................................................................................................................................................................................................................................................................................................................++ -............................................................................++ -e is 65537 (0x10001) -Enter pass phrase for ./tiller.key.pem: -Verifying - Enter pass phrase for ./tiller.key.pem: -``` - -Next, generate the Helm client's key: - -```console -$ openssl genrsa -out ./helm.key.pem 4096 -Generating RSA private key, 4096 bit long modulus -.....++ -......................................................................................................................................................................................++ -e is 65537 (0x10001) -Enter pass phrase for ./helm.key.pem: -Verifying - Enter pass phrase for ./helm.key.pem: -``` - -Again, for production use you will generate one client certificate for each user. - -Next we need to create certificates from these keys. For each certificate, this is -a two-step process of creating a CSR, and then creating the certificate. - -```console -$ openssl req -key tiller.key.pem -new -sha256 -out tiller.csr.pem -Enter pass phrase for tiller.key.pem: -You are about to be asked to enter information that will be incorporated -into your certificate request. -What you are about to enter is what is called a Distinguished Name or a DN. -There are quite a few fields but you can leave some blank -For some fields there will be a default value, -If you enter '.', the field will be left blank. ------ -Country Name (2 letter code) [AU]:US -State or Province Name (full name) [Some-State]:CO -Locality Name (eg, city) []:Boulder -Organization Name (eg, company) [Internet Widgits Pty Ltd]:Tiller Server -Organizational Unit Name (eg, section) []: -Common Name (e.g. server FQDN or YOUR name) []:tiller-server -Email Address []: - -Please enter the following 'extra' attributes -to be sent with your certificate request -A challenge password []: -An optional company name []: -``` - -And we repeat this step for the Helm client certificate: - -```console -$ openssl req -key helm.key.pem -new -sha256 -out helm.csr.pem -# Answer the questions with your client user's info -``` - -(In rare cases, we've had to add the `-nodes` flag when generating the request.) - -Now we sign each of these CSRs with the CA certificate we created: - -```console -$ openssl x509 -req -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial -in tiller.csr.pem -out tiller.cert.pem -Signature ok -subject=/C=US/ST=CO/L=Boulder/O=Tiller Server/CN=tiller-server -Getting CA Private Key -Enter pass phrase for ca.key.pem: -``` - -And again for the client certificate: - -```console -$ openssl x509 -req -CA ca.cert.pem -CAkey ca.key.pem -CAcreateserial -in helm.csr.pem -out helm.cert.pem -``` - -At this point, the important files for us are these: - -``` -# The CA. Make sure the key is kept secret. -ca.cert.pem -ca.key.pem -# The Helm client files -helm.cert.pem -helm.key.pem -# The Tiller server files. -tiller.cert.pem -tiller.key.pem -``` - -Now we're ready to move on to the next steps. - -## Creating a Custom Tiller Installation - -Helm includes full support for creating a deployment configured for SSL. By specifying -a few flags, the `helm init` command can create a new Tiller installation complete -with all of our SSL configuration. - -To take a look at what this will generate, run this command: - -```console -$ helm init --dry-run --debug --tiller-tls --tiller-tls-cert ./tiller.cert.pem --tiller-tls-key ./tiller.key.pem --tiller-tls-verify --tls-ca-cert ca.cert.pem -``` - -The output will show you a Deployment, a Secret, and a Service. Your SSL information -will be preloaded into the Secret, which the Deployment will mount to pods as they -start up. - -If you want to customize the manifest, you can save that output to a file and then -use `kubectl create` to load it into your cluster. - -> We strongly recommend enabling RBAC on your cluster and adding [service accounts](rbac.md) -> with RBAC. - -Otherwise, you can remove the `--dry-run` and `--debug` flags. We also recommend -putting Tiller in a non-system namespace (`--tiller-namespace=something`) and enable -a service account (`--service-account=somename`). But for this example we will stay -with the basics: - -```console -$ helm init --tiller-tls --tiller-tls-cert ./tiller.cert.pem --tiller-tls-key ./tiller.key.pem --tiller-tls-verify --tls-ca-cert ca.cert.pem -``` - -In a minute or two it should be ready. We can check Tiller like this: - -```console -$ kubectl -n kube-system get deployment -NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE -... other stuff -tiller-deploy 1 1 1 1 2m -``` - -If there is a problem, you may want to use `kubectl get pods -n kube-system` to -find out what went wrong. With the SSL/TLS support, the most common problems all -have to do with improperly generated TLS certificates or accidentally swapping the -cert and the key. - -At this point, you should get a _failure_ when you run basic Helm commands: - -```console -$ helm ls -Error: transport is closing -``` - -This is because your Helm client does not have the correct certificate to authenticate -to Tiller. - -## Configuring the Helm Client - -The Tiller server is now running with TLS protection. It's time to configure the -Helm client to also perform TLS operations. - -For a quick test, we can specify our configuration manually. We'll run a normal -Helm command (`helm ls`), but with SSL/TLS enabled. - -```console -helm ls --tls --tls-ca-cert ca.cert.pem --tls-cert helm.cert.pem --tls-key helm.key.pem -``` - -This configuration sends our client-side certificate to establish identity, uses -the client key for encryption, and uses the CA certificate to validate the remote -Tiller's identity. - -Typing a line that is cumbersome, though. The shortcut is to move the key, -cert, and CA into `$HELM_HOME`: - -```console -$ cp ca.cert.pem $(helm home)/ca.pem -$ cp helm.cert.pem $(helm home)/cert.pem -$ cp helm.key.pem $(helm home)/key.pem -``` - -With this, you can simply run `helm ls --tls` to enable TLS. - -### Troubleshooting - -*Running a command, I get `Error: transport is closing`* - -This is almost always due to a configuration error in which the client is missing -a certificate (`--tls-cert`) or the certificate is bad. - -*I'm using a certificate, but get `Error: remote error: tls: bad certificate`* - -This means that Tiller's CA cannot verify your certificate. In the examples above, -we used a single CA to generate both the client and server certificates. In these -examples, the CA has _signed_ the client's certificate. We then load that CA -up to Tiller. So when the client certificate is sent to the server, Tiller -checks the client certificate against the CA. - -*If I use `--tls-verify` on the client, I get `Error: x509: certificate is valid for tiller-server, not localhost`* - -If you plan to use `--tls-verify` on the client, you will need to make sure that -the host name that Helm connects to matches the host name on the certificate. In -some cases this is awkward, since Helm will connect over localhost, or the FQDN is -not available for public resolution. - -## References - -https://github.com/denji/golang-tls -https://www.openssl.org/docs/ -https://jamielinux.com/docs/openssl-certificate-authority/sign-server-and-client-certificates.html diff --git a/docs/using_helm.md b/docs/using_helm.md deleted file mode 100755 index 6bf7bc40a4d..00000000000 --- a/docs/using_helm.md +++ /dev/null @@ -1,515 +0,0 @@ -# Using Helm - -This guide explains the basics of using Helm (and Tiller) to manage -packages on your Kubernetes cluster. It assumes that you have already -[installed](install.md) the Helm client and the Tiller server (typically by `helm -init`). - -If you are simply interested in running a few quick commands, you may -wish to begin with the [Quickstart Guide](quickstart.md). This chapter -covers the particulars of Helm commands, and explains how to use Helm. - -## Three Big Concepts - -A *Chart* is a Helm package. It contains all of the resource definitions -necessary to run an application, tool, or service inside of a Kubernetes -cluster. Think of it like the Kubernetes equivalent of a Homebrew formula, -an Apt dpkg, or a Yum RPM file. - -A *Repository* is the place where charts can be collected and shared. -It's like Perl's [CPAN archive](http://www.cpan.org) or the -[Fedora Package Database](https://admin.fedoraproject.org/pkgdb/), but for -Kubernetes packages. - -A *Release* is an instance of a chart running in a Kubernetes cluster. -One chart can often be installed many times into the same cluster. And -each time it is installed, a new _release_ is created. Consider a MySQL -chart. If you want two databases running in your cluster, you can -install that chart twice. Each one will have its own _release_, which -will in turn have its own _release name_. - -With these concepts in mind, we can now explain Helm like this: - -Helm installs _charts_ into Kubernetes, creating a new _release_ for -each installation. And to find new charts, you can search Helm chart -_repositories_. - -## 'helm search': Finding Charts - -When you first install Helm, it is preconfigured to talk to the official -Kubernetes charts repository. This repository contains a number of -carefully curated and maintained charts. This chart repository is named -`stable` by default. - -You can see which charts are available by running `helm search`: - -``` -$ helm search -NAME VERSION DESCRIPTION -stable/drupal 0.3.2 One of the most versatile open source content m... -stable/jenkins 0.1.0 A Jenkins Helm chart for Kubernetes. -stable/mariadb 0.5.1 Chart for MariaDB -stable/mysql 0.1.0 Chart for MySQL -... -``` - -With no filter, `helm search` shows you all of the available charts. You -can narrow down your results by searching with a filter: - -``` -$ helm search mysql -NAME VERSION DESCRIPTION -stable/mysql 0.1.0 Chart for MySQL -stable/mariadb 0.5.1 Chart for MariaDB -``` - -Now you will only see the results that match your filter. - -Why is -`mariadb` in the list? Because its package description relates it to -MySQL. We can use `helm inspect chart` to see this: - -``` -$ helm inspect stable/mariadb -Fetched stable/mariadb to mariadb-0.5.1.tgz -description: Chart for MariaDB -engine: gotpl -home: https://mariadb.org -keywords: -- mariadb -- mysql -- database -- sql -... -``` - -Search is a good way to find available packages. Once you have found a -package you want to install, you can use `helm install` to install it. - -## 'helm install': Installing a Package - -To install a new package, use the `helm install` command. At its -simplest, it takes only one argument: The name of the chart. - -``` -$ helm install stable/mariadb -Fetched stable/mariadb-0.3.0 to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz -happy-panda -Last Deployed: Wed Sep 28 12:32:28 2016 -Namespace: default -Status: DEPLOYED - -Resources: -==> extensions/Deployment -NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE -happy-panda-mariadb 1 0 0 0 1s - -==> v1/Secret -NAME TYPE DATA AGE -happy-panda-mariadb Opaque 2 1s - -==> v1/Service -NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE -happy-panda-mariadb 10.0.0.70 3306/TCP 1s - - -Notes: -MariaDB can be accessed via port 3306 on the following DNS name from within your cluster: -happy-panda-mariadb.default.svc.cluster.local - -To connect to your database run the following command: - - kubectl run happy-panda-mariadb-client --rm --tty -i --image bitnami/mariadb --command -- mysql -h happy-panda-mariadb -``` - -Now the `mariadb` chart is installed. Note that installing a chart -creates a new _release_ object. The release above is named -`happy-panda`. (If you want to use your own release name, simply use the -`--name` flag on `helm install`.) - -During installation, the `helm` client will print useful information -about which resources were created, what the state of the release is, -and also whether there are additional configuration steps you can or -should take. - -Helm does not wait until all of the resources are running before it -exits. Many charts require Docker images that are over 600M in size, and -may take a long time to install into the cluster. - -To keep track of a release's state, or to re-read configuration -information, you can use `helm status`: - -``` -$ helm status happy-panda -Last Deployed: Wed Sep 28 12:32:28 2016 -Namespace: default -Status: DEPLOYED - -Resources: -==> v1/Service -NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE -happy-panda-mariadb 10.0.0.70 3306/TCP 4m - -==> extensions/Deployment -NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE -happy-panda-mariadb 1 1 1 1 4m - -==> v1/Secret -NAME TYPE DATA AGE -happy-panda-mariadb Opaque 2 4m - - -Notes: -MariaDB can be accessed via port 3306 on the following DNS name from within your cluster: -happy-panda-mariadb.default.svc.cluster.local - -To connect to your database run the following command: - - kubectl run happy-panda-mariadb-client --rm --tty -i --image bitnami/mariadb --command -- mysql -h happy-panda-mariadb -``` - -The above shows the current state of your release. - -### Customizing the Chart Before Installing - -Installing the way we have here will only use the default configuration -options for this chart. Many times, you will want to customize the chart -to use your preferred configuration. - -To see what options are configurable on a chart, use `helm inspect -values`: - -```console -helm inspect values stable/mariadb -Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz -## Bitnami MariaDB image version -## ref: https://hub.docker.com/r/bitnami/mariadb/tags/ -## -## Default: none -imageTag: 10.1.14-r3 - -## Specify a imagePullPolicy -## Default to 'Always' if imageTag is 'latest', else set to 'IfNotPresent' -## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images -## -# imagePullPolicy: - -## Specify password for root user -## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#setting-the-root-password-on-first-run -## -# mariadbRootPassword: - -## Create a database user -## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-user-on-first-run -## -# mariadbUser: -# mariadbPassword: - -## Create a database -## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-on-first-run -## -# mariadbDatabase: -``` - -You can then override any of these settings in a YAML formatted file, -and then pass that file during installation. - -```console -$ echo '{mariadbUser: user0, mariadbDatabase: user0db}' > config.yaml -$ helm install -f config.yaml stable/mariadb -``` - -The above will create a default MariaDB user with the name `user0`, and -grant this user access to a newly created `user0db` database, but will -accept all the rest of the defaults for that chart. - -There are two ways to pass configuration data during install: - -- `--values` (or `-f`): Specify a YAML file with overrides. This can be specified multiple times - and the rightmost file will take precedence -- `--set`: Specify overrides on the command line. - -If both are used, `--set` values are merged into `--values` with higher precedence. -Overrides specified with `--set` are persisted in a configmap. Values that have been -`--set` can be viewed for a given release with `helm get values `. -Values that have been `--set` can be cleared by running `helm upgrade` with `--reset-values` -specified. - -#### The Format and Limitations of `--set` - -The `--set` option takes zero or more name/value pairs. At its simplest, it is -used like this: `--set name=value`. The YAML equivalent of that is: - -```yaml -name: value -``` - -Multiple values are separated by `,` characters. So `--set a=b,c=d` becomes: - -```yaml -a: b -c: d -``` - -More complex expressions are supported. For example, `--set outer.inner=value` is -translated into this: -```yaml -outer: - inner: value -``` - -Lists can be expressed by enclosing values in `{` and `}`. For example, -`--set name={a, b, c}` translates to: - -```yaml -name: - - a - - b - - c -``` - -As of Helm 2.5.0, it is possible to access list items using an array index syntax. -For example, `--set servers[0].port=80` becomes: - -```yaml -servers: - - port: 80 -``` - -Multiple values can be set this way. The line `--set servers[0].port=80,servers[0].host=example` becomes: - -```yaml -servers: - - port: 80 - host: example -``` - -Sometimes you need to use special characters in your `--set` lines. You can use -a backslash to escape the characters; `--set name=value1\,value2` will become: - -```yaml -name: "value1,value2" -``` - -Similarly, you can escape dot sequences as well, which may come in handy when charts use the -`toYaml` function to parse annotations, labels and node selectors. The syntax for -`--set nodeSelector."kubernetes\.io/role"=master` becomes: - -```yaml -nodeSelector: - kubernetes.io/role: master -``` - -Deeply nested data structures can be difficult to express using `--set`. Chart -designers are encouraged to consider the `--set` usage when designing the format -of a `values.yaml` file. - -### More Installation Methods - -The `helm install` command can install from several sources: - -- A chart repository (as we've seen above) -- A local chart archive (`helm install foo-0.1.1.tgz`) -- An unpacked chart directory (`helm install path/to/foo`) -- A full URL (`helm install https://example.com/charts/foo-1.2.3.tgz`) - -## 'helm upgrade' and 'helm rollback': Upgrading a Release, and Recovering on Failure - -When a new version of a chart is released, or when you want to change -the configuration of your release, you can use the `helm upgrade` -command. - -An upgrade takes an existing release and upgrades it according to the -information you provide. Because Kubernetes charts can be large and -complex, Helm tries to perform the least invasive upgrade. It will only -update things that have changed since the last release. - -```console -$ helm upgrade -f panda.yaml happy-panda stable/mariadb -Fetched stable/mariadb-0.3.0.tgz to /Users/mattbutcher/Code/Go/src/k8s.io/helm/mariadb-0.3.0.tgz -happy-panda has been upgraded. Happy Helming! -Last Deployed: Wed Sep 28 12:47:54 2016 -Namespace: default -Status: DEPLOYED -... -``` - -In the above case, the `happy-panda` release is upgraded with the same -chart, but with a new YAML file: - -```yaml -mariadbUser: user1 -``` - -We can use `helm get values` to see whether that new setting took -effect. - -```console -$ helm get values happy-panda -mariadbUser: user1 -``` - -The `helm get` command is a useful tool for looking at a release in the -cluster. And as we can see above, it shows that our new values from -`panda.yaml` were deployed to the cluster. - -Now, if something does not go as planned during a release, it is easy to -roll back to a previous release using `helm rollback [RELEASE] [REVISION]`. - -```console -$ helm rollback happy-panda 1 -``` - -The above rolls back our happy-panda to its very first release version. -A release version is an incremental revision. Every time an install, -upgrade, or rollback happens, the revision number is incremented by 1. -The first revision number is always 1. And we can use `helm history [RELEASE]` -to see revision numbers for a certain release. - -## Helpful Options for Install/Upgrade/Rollback -There are several other helpful options you can specify for customizing the -behavior of Helm during an install/upgrade/rollback. Please note that this -is not a full list of cli flags. To see a description of all flags, just run -`helm --help`. - -- `--timeout`: A value in seconds to wait for Kubernetes commands to complete - This defaults to 300 (5 minutes) -- `--wait`: Waits until all Pods are in a ready state, PVCs are bound, Deployments - have minimum (`Desired` minus `maxUnavailable`) Pods in ready state and - Services have an IP address (and Ingress if a `LoadBalancer`) before - marking the release as successful. It will wait for as long as the - `--timeout` value. If timeout is reached, the release will be marked as - `FAILED`. Note: In scenario where Deployment has `replicas` set to 1 and - `maxUnavailable` is not set to 0 as part of rolling update strategy, - `--wait` will return as ready as it has satisfied the minimum Pod in ready condition. -- `--no-hooks`: This skips running hooks for the command -- `--recreate-pods` (only available for `upgrade` and `rollback`): This flag - will cause all pods to be recreated (with the exception of pods belonging to - deployments) - -## 'helm delete': Deleting a Release - -When it is time to uninstall or delete a release from the cluster, use -the `helm delete` command: - -``` -$ helm delete happy-panda -``` - -This will remove the release from the cluster. You can see all of your -currently deployed releases with the `helm list` command: - -``` -$ helm list -NAME VERSION UPDATED STATUS CHART -inky-cat 1 Wed Sep 28 12:59:46 2016 DEPLOYED alpine-0.1.0 -``` - -From the output above, we can see that the `happy-panda` release was -deleted. - -However, Helm always keeps records of what releases happened. Need to -see the deleted releases? `helm list --deleted` shows those, and `helm -list --all` shows all of the releases (deleted and currently deployed, -as well as releases that failed): - -```console -⇒ helm list --all -NAME VERSION UPDATED STATUS CHART -happy-panda 2 Wed Sep 28 12:47:54 2016 DELETED mariadb-0.3.0 -inky-cat 1 Wed Sep 28 12:59:46 2016 DEPLOYED alpine-0.1.0 -kindred-angelf 2 Tue Sep 27 16:16:10 2016 DELETED alpine-0.1.0 -``` - -Because Helm keeps records of deleted releases, a release name cannot be -re-used. (If you _really_ need to re-use a release name, you can use the -`--replace` flag, but it will simply re-use the existing release and -replace its resources.) - -Note that because releases are preserved in this way, you can rollback a -deleted resource, and have it re-activate. - -## 'helm repo': Working with Repositories - -So far, we've been installing charts only from the `stable` repository. -But you can configure `helm` to use other repositories. Helm provides -several repository tools under the `helm repo` command. - -You can see which repositories are configured using `helm repo list`: - -```console -$ helm repo list -NAME URL -stable https://kubernetes-charts.storage.googleapis.com -local http://localhost:8879/charts -mumoshu https://mumoshu.github.io/charts -``` - -And new repositories can be added with `helm repo add`: - -```console -$ helm repo add dev https://example.com/dev-charts -``` - -Because chart repositories change frequently, at any point you can make -sure your Helm client is up to date by running `helm repo update`. - -## Creating Your Own Charts - -The [Chart Development Guide](charts.md) explains how to develop your own -charts. But you can get started quickly by using the `helm create` -command: - -```console -$ helm create deis-workflow -Creating deis-workflow -``` - -Now there is a chart in `./deis-workflow`. You can edit it and create -your own templates. - -As you edit your chart, you can validate that it is well-formatted by -running `helm lint`. - -When it's time to package the chart up for distribution, you can run the -`helm package` command: - -```console -$ helm package deis-workflow -deis-workflow-0.1.0.tgz -``` - -And that chart can now easily be installed by `helm install`: - -```console -$ helm install ./deis-workflow-0.1.0.tgz -... -``` - -Charts that are archived can be loaded into chart repositories. See the -documentation for your chart repository server to learn how to upload. - -Note: The `stable` repository is managed on the [Kubernetes Charts -GitHub repository](https://github.com/kubernetes/charts). That project -accepts chart source code, and (after audit) packages those for you. - -## Tiller, Namespaces and RBAC -In some cases you may wish to scope Tiller or deploy multiple Tillers to a single cluster. Here are some best practices when operating in those circumstances. - -1. Tiller can be [installed](install.md) into any namespace. By default, it is installed into kube-system. You can run multiple Tillers provided they each run in their own namespace. -2. Limiting Tiller to only be able to install into specific namespaces and/or resource types is controlled by Kubernetes [RBAC](https://kubernetes.io/docs/admin/authorization/rbac/) roles and rolebindings. You can add a service account to Tiller when configuring Helm via `helm init --service-account `. You can find more information about that [here](rbac.md). -3. Release names are unique PER TILLER INSTANCE. -4. Charts should only contain resources that exist in a single namespace. -5. It is not recommended to have multiple Tillers configured to manage resources in the same namespace. - -## Conclusion - -This chapter has covered the basic usage patterns of the `helm` client, -including searching, installation, upgrading, and deleting. It has also -covered useful utility commands like `helm status`, `helm get`, and -`helm repo`. - -For more information on these commands, take a look at Helm's built-in -help: `helm help`. - -In the next chapter, we look at the process of developing charts. diff --git a/glide.lock b/glide.lock deleted file mode 100644 index 6c54c927c7a..00000000000 --- a/glide.lock +++ /dev/null @@ -1,853 +0,0 @@ -hash: 6837936360d447b64aa7a09d3c89c18ac5540b009a57fc4d3227af299bf40268 -updated: 2018-04-03T08:17:14.801847688-07:00 -imports: -- name: cloud.google.com/go - version: 3b1ae45394a234c385be014e9a488f2bb6eef821 - subpackages: - - compute/metadata - - internal -- name: github.com/aokoli/goutils - version: 9c37978a95bd5c709a15883b6242714ea6709e64 -- name: github.com/asaskevich/govalidator - version: 7664702784775e51966f0885f5cd27435916517b -- name: github.com/Azure/go-ansiterm - version: 19f72df4d05d31cbe1c56bfc8045c96babff6c7e - subpackages: - - winterm -- name: github.com/Azure/go-autorest - version: d4e6b95c12a08b4de2d48b45d5b4d594e5d32fab - subpackages: - - autorest - - autorest/adal - - autorest/azure - - autorest/date -- name: github.com/beorn7/perks - version: 3ac7bf7a47d159a033b107610db8a1b6575507a4 - subpackages: - - quantile -- name: github.com/BurntSushi/toml - version: b26d9c308763d68093482582cea63d69be07a0f0 -- name: github.com/cpuguy83/go-md2man - version: 71acacd42f85e5e82f70a55327789582a5200a90 - subpackages: - - md2man -- name: github.com/davecgh/go-spew - version: 782f4967f2dc4564575ca782fe2d04090b5faca8 - subpackages: - - spew -- name: github.com/dgrijalva/jwt-go - version: 01aeca54ebda6e0fbfafd0a524d234159c05ec20 -- name: github.com/docker/distribution - version: edc3ab29cdff8694dd6feb85cfeb4b5f1b38ed9c - subpackages: - - digestset - - reference -- name: github.com/docker/docker - version: 4f3616fb1c112e206b88cb7a9922bf49067a7756 - subpackages: - - api - - api/types - - api/types/blkiodev - - api/types/container - - api/types/events - - api/types/filters - - api/types/image - - api/types/mount - - api/types/network - - api/types/registry - - api/types/strslice - - api/types/swarm - - api/types/swarm/runtime - - api/types/time - - api/types/versions - - api/types/volume - - client - - pkg/ioutils - - pkg/jsonlog - - pkg/jsonmessage - - pkg/longpath - - pkg/mount - - pkg/parsers - - pkg/stdcopy - - pkg/sysinfo - - pkg/system - - pkg/term - - pkg/term/windows - - pkg/tlsconfig -- name: github.com/docker/go-connections - version: 3ede32e2033de7505e6500d6c868c2b9ed9f169d - subpackages: - - nat - - sockets - - tlsconfig -- name: github.com/docker/go-units - version: 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1 -- name: github.com/docker/spdystream - version: 449fdfce4d962303d702fec724ef0ad181c92528 - subpackages: - - spdy -- name: github.com/evanphx/json-patch - version: 944e07253867aacae43c04b2e6a239005443f33a -- name: github.com/exponent-io/jsonpath - version: d6023ce2651d8eafb5c75bb0c7167536102ec9f5 -- name: github.com/fatih/camelcase - version: f6a740d52f961c60348ebb109adde9f4635d7540 -- name: github.com/ghodss/yaml - version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee -- name: github.com/go-openapi/jsonpointer - version: 46af16f9f7b149af66e5d1bd010e3574dc06de98 -- name: github.com/go-openapi/jsonreference - version: 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272 -- name: github.com/go-openapi/spec - version: 1de3e0542de65ad8d75452a595886fdd0befb363 -- name: github.com/go-openapi/swag - version: f3f9494671f93fcff853e3c6e9e948b3eb71e590 -- name: github.com/gobwas/glob - version: 5ccd90ef52e1e632236f7326478d4faa74f99438 - subpackages: - - compiler - - match - - syntax - - syntax/ast - - syntax/lexer - - util/runes - - util/strings -- name: github.com/gogo/protobuf - version: c0656edd0d9eab7c66d1eb0c568f9039345796f7 - subpackages: - - proto - - sortkeys -- name: github.com/golang/glog - version: 44145f04b68cf362d9c4df2182967c2275eaefed -- name: github.com/golang/groupcache - version: 02826c3e79038b59d737d3b1c0a1d937f71a4433 - subpackages: - - lru -- name: github.com/golang/protobuf - version: 1643683e1b54a9e88ad26d98f81400c8c9d9f4f9 - subpackages: - - proto - - ptypes - - ptypes/any - - ptypes/duration - - ptypes/timestamp -- name: github.com/google/btree - version: 7d79101e329e5a3adf994758c578dab82b90c017 -- name: github.com/google/gofuzz - version: 44d81051d367757e1c7c6a5a86423ece9afcf63c -- name: github.com/google/uuid - version: 064e2069ce9c359c118179501254f67d7d37ba24 -- name: github.com/googleapis/gnostic - version: 0c5108395e2debce0d731cf0287ddf7242066aba - subpackages: - - OpenAPIv2 - - compiler - - extensions -- name: github.com/gophercloud/gophercloud - version: 6da026c32e2d622cc242d32984259c77237aefe1 - subpackages: - - openstack - - openstack/identity/v2/tenants - - openstack/identity/v2/tokens - - openstack/identity/v3/tokens - - openstack/utils - - pagination -- name: github.com/gosuri/uitable - version: 36ee7e946282a3fb1cfecd476ddc9b35d8847e42 - subpackages: - - util/strutil - - util/wordwrap -- name: github.com/gregjones/httpcache - version: 787624de3eb7bd915c329cba748687a3b22666a6 - subpackages: - - diskcache -- name: github.com/grpc-ecosystem/go-grpc-prometheus - version: 0c1b191dbfe51efdabe3c14b9f6f3b96429e0722 -- name: github.com/hashicorp/golang-lru - version: a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4 - subpackages: - - simplelru -- name: github.com/howeyc/gopass - version: bf9dde6d0d2c004a008c27aaee91170c786f6db8 -- name: github.com/huandu/xstrings - version: 3959339b333561bf62a38b424fd41517c2c90f40 -- name: github.com/imdario/mergo - version: 6633656539c1639d9d78127b7d47c622b5d7b6dc -- name: github.com/inconshreveable/mousetrap - version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 -- name: github.com/json-iterator/go - version: 13f86432b882000a51c6e610c620974462691a97 -- name: github.com/mailru/easyjson - version: 2f5df55504ebc322e4d52d34df6a1f5b503bf26d - subpackages: - - buffer - - jlexer - - jwriter -- name: github.com/MakeNowJust/heredoc - version: bb23615498cded5e105af4ce27de75b089cbe851 -- name: github.com/Masterminds/semver - version: 517734cc7d6470c0d07130e40fd40bdeb9bcd3fd -- name: github.com/Masterminds/sprig - version: 6b2a58267f6a8b1dc8e2eb5519b984008fa85e8c -- name: github.com/Masterminds/vcs - version: 3084677c2c188840777bff30054f2b553729d329 -- name: github.com/mattn/go-runewidth - version: d6bea18f789704b5f83375793155289da36a3c7f -- name: github.com/matttproud/golang_protobuf_extensions - version: fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a - subpackages: - - pbutil -- name: github.com/mitchellh/go-wordwrap - version: ad45545899c7b13c020ea92b2072220eefad42b8 -- name: github.com/opencontainers/go-digest - version: a6d0ee40d4207ea02364bd3b9e8e77b9159ba1eb -- name: github.com/opencontainers/image-spec - version: 372ad780f63454fbbbbcc7cf80e5b90245c13e13 - subpackages: - - specs-go - - specs-go/v1 -- name: github.com/pborman/uuid - version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 -- name: github.com/peterbourgon/diskv - version: 5f041e8faa004a95c88a202771f4cc3e991971e6 -- name: github.com/prometheus/client_golang - version: c5b7fccd204277076155f10851dad72b76a49317 - subpackages: - - prometheus - - prometheus/promhttp -- name: github.com/prometheus/client_model - version: fa8ad6fec33561be4280a8f0514318c79d7f6cb6 - subpackages: - - go -- name: github.com/prometheus/common - version: 13ba4ddd0caa9c28ca7b7bffe1dfa9ed8d5ef207 - subpackages: - - expfmt - - internal/bitbucket.org/ww/goautoneg - - model -- name: github.com/prometheus/procfs - version: 65c1f6f8f0fc1e2185eb9863a3bc751496404259 - subpackages: - - xfs -- name: github.com/PuerkitoBio/purell - version: 8a290539e2e8629dbc4e6bad948158f790ec31f4 -- name: github.com/PuerkitoBio/urlesc - version: 5bd2802263f21d8788851d5305584c82a5c75d7e -- name: github.com/russross/blackfriday - version: 300106c228d52c8941d4b3de6054a6062a86dda3 -- name: github.com/shurcooL/sanitized_anchor_name - version: 10ef21a441db47d8b13ebcc5fd2310f636973c77 -- name: github.com/sirupsen/logrus - version: 89742aefa4b206dcf400792f3bd35b542998eb3b -- name: github.com/spf13/cobra - version: f62e98d28ab7ad31d707ba837a966378465c7b57 - subpackages: - - doc -- name: github.com/spf13/pflag - version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7 -- name: github.com/technosophos/moniker - version: ab470f5e105a44d0c87ea21bacd6a335c4816d83 -- name: golang.org/x/crypto - version: 81e90905daefcd6fd217b62423c0908922eadb30 - subpackages: - - cast5 - - ed25519 - - ed25519/internal/edwards25519 - - openpgp - - openpgp/armor - - openpgp/clearsign - - openpgp/elgamal - - openpgp/errors - - openpgp/packet - - openpgp/s2k - - pbkdf2 - - scrypt - - ssh/terminal -- name: golang.org/x/net - version: 1c05540f6879653db88113bc4a2b70aec4bd491f - subpackages: - - context - - context/ctxhttp - - http2 - - http2/hpack - - idna - - internal/timeseries - - lex/httplex - - trace -- name: golang.org/x/oauth2 - version: a6bd8cefa1811bd24b86f8902872e4e8225f74c4 - subpackages: - - google - - internal - - jws - - jwt -- name: golang.org/x/sys - version: 43eea11bc92608addb41b8a406b0407495c106f6 - subpackages: - - unix - - windows -- name: golang.org/x/text - version: b19bf474d317b857955b12035d2c5acb57ce8b01 - subpackages: - - cases - - encoding - - encoding/internal - - encoding/internal/identifier - - encoding/unicode - - internal - - internal/tag - - internal/utf8internal - - language - - runes - - secure/bidirule - - secure/precis - - transform - - unicode/bidi - - unicode/norm - - width -- name: golang.org/x/time - version: f51c12702a4d776e4c1fa9b0fabab841babae631 - subpackages: - - rate -- name: google.golang.org/appengine - version: 12d5545dc1cfa6047a286d5e853841b6471f4c19 - subpackages: - - internal - - internal/app_identity - - internal/base - - internal/datastore - - internal/log - - internal/modules - - internal/remote_api - - internal/urlfetch - - urlfetch -- name: google.golang.org/genproto - version: 09f6ed296fc66555a25fe4ce95173148778dfa85 - subpackages: - - googleapis/rpc/status -- name: google.golang.org/grpc - version: 5ffe3083946d5603a0578721101dc8165b1d5b5f - subpackages: - - balancer - - codes - - connectivity - - credentials - - grpclb/grpc_lb_v1/messages - - grpclog - - health - - health/grpc_health_v1 - - internal - - keepalive - - metadata - - naming - - peer - - resolver - - stats - - status - - tap - - transport -- name: gopkg.in/inf.v0 - version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 -- name: gopkg.in/square/go-jose.v2 - version: f8f38de21b4dcd69d0413faf231983f5fd6634b1 - subpackages: - - cipher - - json - - jwt -- name: gopkg.in/yaml.v2 - version: 53feefa2559fb8dfa8d81baad31be332c97d6c77 -- name: k8s.io/api - version: c699ec51538f0cfd4afa8bfcfe1e0779cafbe666 - subpackages: - - admission/v1beta1 - - admissionregistration/v1alpha1 - - admissionregistration/v1beta1 - - apps/v1 - - apps/v1beta1 - - apps/v1beta2 - - authentication/v1 - - authentication/v1beta1 - - authorization/v1 - - authorization/v1beta1 - - autoscaling/v1 - - autoscaling/v2beta1 - - batch/v1 - - batch/v1beta1 - - batch/v2alpha1 - - certificates/v1beta1 - - core/v1 - - events/v1beta1 - - extensions/v1beta1 - - imagepolicy/v1alpha1 - - networking/v1 - - policy/v1beta1 - - rbac/v1 - - rbac/v1alpha1 - - rbac/v1beta1 - - scheduling/v1alpha1 - - settings/v1alpha1 - - storage/v1 - - storage/v1alpha1 - - storage/v1beta1 -- name: k8s.io/apiextensions-apiserver - version: 898b0eda132e1aeac43a459785144ee4bf9b0a2e - subpackages: - - pkg/features -- name: k8s.io/apimachinery - version: 54101a56dda9a0962bc48751c058eb4c546dcbb9 - subpackages: - - pkg/api/equality - - pkg/api/errors - - pkg/api/meta - - pkg/api/resource - - pkg/api/validation - - pkg/apimachinery - - pkg/apimachinery/announced - - pkg/apimachinery/registered - - pkg/apis/meta/internalversion - - pkg/apis/meta/v1 - - pkg/apis/meta/v1/unstructured - - pkg/apis/meta/v1/validation - - pkg/apis/meta/v1beta1 - - pkg/conversion - - pkg/conversion/queryparams - - pkg/fields - - pkg/labels - - pkg/runtime - - pkg/runtime/schema - - pkg/runtime/serializer - - pkg/runtime/serializer/json - - pkg/runtime/serializer/protobuf - - pkg/runtime/serializer/recognizer - - pkg/runtime/serializer/streaming - - pkg/runtime/serializer/versioning - - pkg/selection - - pkg/types - - pkg/util/cache - - pkg/util/clock - - pkg/util/diff - - pkg/util/duration - - pkg/util/errors - - pkg/util/framer - - pkg/util/httpstream - - pkg/util/httpstream/spdy - - pkg/util/intstr - - pkg/util/json - - pkg/util/mergepatch - - pkg/util/net - - pkg/util/rand - - pkg/util/remotecommand - - pkg/util/runtime - - pkg/util/sets - - pkg/util/strategicpatch - - pkg/util/uuid - - pkg/util/validation - - pkg/util/validation/field - - pkg/util/wait - - pkg/util/yaml - - pkg/version - - pkg/watch - - third_party/forked/golang/json - - third_party/forked/golang/netutil - - third_party/forked/golang/reflect -- name: k8s.io/apiserver - version: ea53f8588c655568158b4ff53f5ec6fa4ebfc332 - subpackages: - - pkg/apis/audit - - pkg/authentication/authenticator - - pkg/authentication/serviceaccount - - pkg/authentication/user - - pkg/endpoints/request - - pkg/features - - pkg/util/feature - - pkg/util/flag -- name: k8s.io/client-go - version: 23781f4d6632d88e869066eaebb743857aa1ef9b - subpackages: - - discovery - - discovery/fake - - dynamic - - informers - - informers/admissionregistration - - informers/admissionregistration/v1alpha1 - - informers/admissionregistration/v1beta1 - - informers/apps - - informers/apps/v1 - - informers/apps/v1beta1 - - informers/apps/v1beta2 - - informers/autoscaling - - informers/autoscaling/v1 - - informers/autoscaling/v2beta1 - - informers/batch - - informers/batch/v1 - - informers/batch/v1beta1 - - informers/batch/v2alpha1 - - informers/certificates - - informers/certificates/v1beta1 - - informers/core - - informers/core/v1 - - informers/events - - informers/events/v1beta1 - - informers/extensions - - informers/extensions/v1beta1 - - informers/internalinterfaces - - informers/networking - - informers/networking/v1 - - informers/policy - - informers/policy/v1beta1 - - informers/rbac - - informers/rbac/v1 - - informers/rbac/v1alpha1 - - informers/rbac/v1beta1 - - informers/scheduling - - informers/scheduling/v1alpha1 - - informers/settings - - informers/settings/v1alpha1 - - informers/storage - - informers/storage/v1 - - informers/storage/v1alpha1 - - informers/storage/v1beta1 - - kubernetes - - kubernetes/fake - - kubernetes/scheme - - kubernetes/typed/admissionregistration/v1alpha1 - - kubernetes/typed/admissionregistration/v1alpha1/fake - - kubernetes/typed/admissionregistration/v1beta1 - - kubernetes/typed/admissionregistration/v1beta1/fake - - kubernetes/typed/apps/v1 - - kubernetes/typed/apps/v1/fake - - kubernetes/typed/apps/v1beta1 - - kubernetes/typed/apps/v1beta1/fake - - kubernetes/typed/apps/v1beta2 - - kubernetes/typed/apps/v1beta2/fake - - kubernetes/typed/authentication/v1 - - kubernetes/typed/authentication/v1/fake - - kubernetes/typed/authentication/v1beta1 - - kubernetes/typed/authentication/v1beta1/fake - - kubernetes/typed/authorization/v1 - - kubernetes/typed/authorization/v1/fake - - kubernetes/typed/authorization/v1beta1 - - kubernetes/typed/authorization/v1beta1/fake - - kubernetes/typed/autoscaling/v1 - - kubernetes/typed/autoscaling/v1/fake - - kubernetes/typed/autoscaling/v2beta1 - - kubernetes/typed/autoscaling/v2beta1/fake - - kubernetes/typed/batch/v1 - - kubernetes/typed/batch/v1/fake - - kubernetes/typed/batch/v1beta1 - - kubernetes/typed/batch/v1beta1/fake - - kubernetes/typed/batch/v2alpha1 - - kubernetes/typed/batch/v2alpha1/fake - - kubernetes/typed/certificates/v1beta1 - - kubernetes/typed/certificates/v1beta1/fake - - kubernetes/typed/core/v1 - - kubernetes/typed/core/v1/fake - - kubernetes/typed/events/v1beta1 - - kubernetes/typed/events/v1beta1/fake - - kubernetes/typed/extensions/v1beta1 - - kubernetes/typed/extensions/v1beta1/fake - - kubernetes/typed/networking/v1 - - kubernetes/typed/networking/v1/fake - - kubernetes/typed/policy/v1beta1 - - kubernetes/typed/policy/v1beta1/fake - - kubernetes/typed/rbac/v1 - - kubernetes/typed/rbac/v1/fake - - kubernetes/typed/rbac/v1alpha1 - - kubernetes/typed/rbac/v1alpha1/fake - - kubernetes/typed/rbac/v1beta1 - - kubernetes/typed/rbac/v1beta1/fake - - kubernetes/typed/scheduling/v1alpha1 - - kubernetes/typed/scheduling/v1alpha1/fake - - kubernetes/typed/settings/v1alpha1 - - kubernetes/typed/settings/v1alpha1/fake - - kubernetes/typed/storage/v1 - - kubernetes/typed/storage/v1/fake - - kubernetes/typed/storage/v1alpha1 - - kubernetes/typed/storage/v1alpha1/fake - - kubernetes/typed/storage/v1beta1 - - kubernetes/typed/storage/v1beta1/fake - - listers/admissionregistration/v1alpha1 - - listers/admissionregistration/v1beta1 - - listers/apps/v1 - - listers/apps/v1beta1 - - listers/apps/v1beta2 - - listers/autoscaling/v1 - - listers/autoscaling/v2beta1 - - listers/batch/v1 - - listers/batch/v1beta1 - - listers/batch/v2alpha1 - - listers/certificates/v1beta1 - - listers/core/v1 - - listers/events/v1beta1 - - listers/extensions/v1beta1 - - listers/networking/v1 - - listers/policy/v1beta1 - - listers/rbac/v1 - - listers/rbac/v1alpha1 - - listers/rbac/v1beta1 - - listers/scheduling/v1alpha1 - - listers/settings/v1alpha1 - - listers/storage/v1 - - listers/storage/v1alpha1 - - listers/storage/v1beta1 - - pkg/apis/clientauthentication - - pkg/apis/clientauthentication/v1alpha1 - - pkg/version - - plugin/pkg/client/auth - - plugin/pkg/client/auth/azure - - plugin/pkg/client/auth/exec - - plugin/pkg/client/auth/gcp - - plugin/pkg/client/auth/oidc - - plugin/pkg/client/auth/openstack - - rest - - rest/fake - - rest/watch - - scale - - scale/scheme - - scale/scheme/appsint - - scale/scheme/appsv1beta1 - - scale/scheme/appsv1beta2 - - scale/scheme/autoscalingv1 - - scale/scheme/extensionsint - - scale/scheme/extensionsv1beta1 - - testing - - third_party/forked/golang/template - - tools/auth - - tools/cache - - tools/clientcmd - - tools/clientcmd/api - - tools/clientcmd/api/latest - - tools/clientcmd/api/v1 - - tools/metrics - - tools/pager - - tools/portforward - - tools/record - - tools/reference - - tools/remotecommand - - transport - - transport/spdy - - util/buffer - - util/cert - - util/exec - - util/flowcontrol - - util/homedir - - util/integer - - util/jsonpath - - util/retry - - util/workqueue -- name: k8s.io/kube-openapi - version: 50ae88d24ede7b8bad68e23c805b5d3da5c8abaf - subpackages: - - pkg/util/proto - - pkg/util/proto/validation -- name: k8s.io/kubernetes - version: a22f9fd34871d9dc9e5db2c02c713821d18ab2cd - subpackages: - - pkg/api/events - - pkg/api/legacyscheme - - pkg/api/pod - - pkg/api/ref - - pkg/api/resource - - pkg/api/service - - pkg/api/testapi - - pkg/api/v1/pod - - pkg/apis/admission - - pkg/apis/admission/install - - pkg/apis/admission/v1beta1 - - pkg/apis/admissionregistration - - pkg/apis/admissionregistration/install - - pkg/apis/admissionregistration/v1alpha1 - - pkg/apis/admissionregistration/v1beta1 - - pkg/apis/apps - - pkg/apis/apps/install - - pkg/apis/apps/v1 - - pkg/apis/apps/v1beta1 - - pkg/apis/apps/v1beta2 - - pkg/apis/authentication - - pkg/apis/authentication/install - - pkg/apis/authentication/v1 - - pkg/apis/authentication/v1beta1 - - pkg/apis/authorization - - pkg/apis/authorization/install - - pkg/apis/authorization/v1 - - pkg/apis/authorization/v1beta1 - - pkg/apis/autoscaling - - pkg/apis/autoscaling/install - - pkg/apis/autoscaling/v1 - - pkg/apis/autoscaling/v2beta1 - - pkg/apis/batch - - pkg/apis/batch/install - - pkg/apis/batch/v1 - - pkg/apis/batch/v1beta1 - - pkg/apis/batch/v2alpha1 - - pkg/apis/certificates - - pkg/apis/certificates/install - - pkg/apis/certificates/v1beta1 - - pkg/apis/componentconfig - - pkg/apis/componentconfig/install - - pkg/apis/componentconfig/v1alpha1 - - pkg/apis/core - - pkg/apis/core/helper - - pkg/apis/core/helper/qos - - pkg/apis/core/install - - pkg/apis/core/pods - - pkg/apis/core/v1 - - pkg/apis/core/v1/helper - - pkg/apis/core/v1/helper/qos - - pkg/apis/core/validation - - pkg/apis/events - - pkg/apis/events/install - - pkg/apis/events/v1beta1 - - pkg/apis/extensions - - pkg/apis/extensions/install - - pkg/apis/extensions/v1beta1 - - pkg/apis/imagepolicy - - pkg/apis/imagepolicy/install - - pkg/apis/imagepolicy/v1alpha1 - - pkg/apis/networking - - pkg/apis/networking/install - - pkg/apis/networking/v1 - - pkg/apis/policy - - pkg/apis/policy/install - - pkg/apis/policy/v1beta1 - - pkg/apis/rbac - - pkg/apis/rbac/install - - pkg/apis/rbac/v1 - - pkg/apis/rbac/v1alpha1 - - pkg/apis/rbac/v1beta1 - - pkg/apis/scheduling - - pkg/apis/scheduling/install - - pkg/apis/scheduling/v1alpha1 - - pkg/apis/settings - - pkg/apis/settings/install - - pkg/apis/settings/v1alpha1 - - pkg/apis/storage - - pkg/apis/storage/install - - pkg/apis/storage/util - - pkg/apis/storage/v1 - - pkg/apis/storage/v1alpha1 - - pkg/apis/storage/v1beta1 - - pkg/capabilities - - pkg/client/clientset_generated/internalclientset - - pkg/client/clientset_generated/internalclientset/fake - - pkg/client/clientset_generated/internalclientset/scheme - - pkg/client/clientset_generated/internalclientset/typed/admissionregistration/internalversion - - pkg/client/clientset_generated/internalclientset/typed/admissionregistration/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/apps/internalversion - - pkg/client/clientset_generated/internalclientset/typed/apps/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/authentication/internalversion - - pkg/client/clientset_generated/internalclientset/typed/authentication/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/authorization/internalversion - - pkg/client/clientset_generated/internalclientset/typed/authorization/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/autoscaling/internalversion - - pkg/client/clientset_generated/internalclientset/typed/autoscaling/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/batch/internalversion - - pkg/client/clientset_generated/internalclientset/typed/batch/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/certificates/internalversion - - pkg/client/clientset_generated/internalclientset/typed/certificates/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/core/internalversion - - pkg/client/clientset_generated/internalclientset/typed/core/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/events/internalversion - - pkg/client/clientset_generated/internalclientset/typed/events/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/extensions/internalversion - - pkg/client/clientset_generated/internalclientset/typed/extensions/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/networking/internalversion - - pkg/client/clientset_generated/internalclientset/typed/networking/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/policy/internalversion - - pkg/client/clientset_generated/internalclientset/typed/policy/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion - - pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/scheduling/internalversion - - pkg/client/clientset_generated/internalclientset/typed/scheduling/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/settings/internalversion - - pkg/client/clientset_generated/internalclientset/typed/settings/internalversion/fake - - pkg/client/clientset_generated/internalclientset/typed/storage/internalversion - - pkg/client/clientset_generated/internalclientset/typed/storage/internalversion/fake - - pkg/client/conditions - - pkg/cloudprovider - - pkg/controller - - pkg/controller/daemon - - pkg/controller/daemon/util - - pkg/controller/deployment/util - - pkg/controller/history - - pkg/controller/statefulset - - pkg/controller/volume/events - - pkg/controller/volume/persistentvolume - - pkg/controller/volume/persistentvolume/metrics - - pkg/credentialprovider - - pkg/features - - pkg/fieldpath - - pkg/kubectl - - pkg/kubectl/apps - - pkg/kubectl/categories - - pkg/kubectl/cmd/templates - - pkg/kubectl/cmd/testing - - pkg/kubectl/cmd/util - - pkg/kubectl/cmd/util/openapi - - pkg/kubectl/cmd/util/openapi/testing - - pkg/kubectl/cmd/util/openapi/validation - - pkg/kubectl/plugins - - pkg/kubectl/resource - - pkg/kubectl/scheme - - pkg/kubectl/util - - pkg/kubectl/util/hash - - pkg/kubectl/util/slice - - pkg/kubectl/util/term - - pkg/kubectl/util/transport - - pkg/kubectl/validation - - pkg/kubelet/apis - - pkg/kubelet/types - - pkg/master/ports - - pkg/printers - - pkg/printers/internalversion - - pkg/registry/rbac/validation - - pkg/scheduler/algorithm - - pkg/scheduler/algorithm/predicates - - pkg/scheduler/algorithm/priorities/util - - pkg/scheduler/api - - pkg/scheduler/schedulercache - - pkg/scheduler/util - - pkg/scheduler/volumebinder - - pkg/security/apparmor - - pkg/serviceaccount - - pkg/util/file - - pkg/util/goroutinemap - - pkg/util/goroutinemap/exponentialbackoff - - pkg/util/hash - - pkg/util/interrupt - - pkg/util/io - - pkg/util/labels - - pkg/util/metrics - - pkg/util/mount - - pkg/util/net/sets - - pkg/util/node - - pkg/util/nsenter - - pkg/util/parsers - - pkg/util/pointer - - pkg/util/slice - - pkg/util/taints - - pkg/version - - pkg/volume - - pkg/volume/util - - pkg/volume/util/fs - - pkg/volume/util/recyclerclient - - pkg/volume/util/types -- name: k8s.io/utils - version: aedf551cdb8b0119df3a19c65fde413a13b34997 - subpackages: - - clock - - exec - - exec/testing -- name: vbom.ml/util - version: db5cfe13f5cc80a4990d98e2e1b0707a4d1a5394 - subpackages: - - sortorder -testImports: -- name: github.com/pmezard/go-difflib - version: d8ed2627bdf02c080bf22230dbb337003b7aba2d - subpackages: - - difflib -- name: github.com/stretchr/testify - version: e3a8ff8ce36581f87a15341206f205b1da467059 - subpackages: - - assert diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index 7fcb16d0b86..00000000000 --- a/glide.yaml +++ /dev/null @@ -1,65 +0,0 @@ -package: k8s.io/helm -import: -- package: golang.org/x/net - subpackages: - - context -- package: github.com/spf13/cobra - version: f62e98d28ab7ad31d707ba837a966378465c7b57 -- package: github.com/spf13/pflag - version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7 -- package: github.com/Masterminds/vcs - version: ~1.11.0 - # Pin version of mergo that is compatible with both sprig and Kubernetes -- package: github.com/imdario/mergo - version: 6633656539c1639d9d78127b7d47c622b5d7b6dc -- package: github.com/Masterminds/sprig - version: ^2.14.1 -- package: github.com/ghodss/yaml -- package: github.com/Masterminds/semver - version: ~1.3.1 -- package: github.com/technosophos/moniker -- package: github.com/golang/protobuf - version: 1643683e1b54a9e88ad26d98f81400c8c9d9f4f9 - subpackages: - - proto - - ptypes/any - - ptypes/timestamp -- package: google.golang.org/grpc - version: 1.7.2 -- package: github.com/gosuri/uitable -- package: github.com/asaskevich/govalidator - version: ^4.0.0 -- package: golang.org/x/crypto - subpackages: - - openpgp -# pin version of golang.org/x/sys that is compatible with golang.org/x/crypto -- package: golang.org/x/sys - version: 43eea11 - subpackages: - - unix - - windows -- package: github.com/gobwas/glob - version: ^0.2.1 -- package: github.com/evanphx/json-patch -- package: github.com/BurntSushi/toml - version: ~0.3.0 -- package: github.com/prometheus/client_golang - version: 0.8.0 -- package: github.com/grpc-ecosystem/go-grpc-prometheus - -- package: k8s.io/kubernetes - version: release-1.10 -- package: k8s.io/client-go - version: kubernetes-1.10.0 -- package: k8s.io/api - version: release-1.10 -- package: k8s.io/apimachinery - version: release-1.10 -- package: k8s.io/apiserver - version: release-1.10 - -testImports: -- package: github.com/stretchr/testify - version: ^1.1.4 - subpackages: - - assert diff --git a/go.mod b/go.mod new file mode 100644 index 00000000000..0df729c4ea8 --- /dev/null +++ b/go.mod @@ -0,0 +1,54 @@ +module helm.sh/helm/v3 + +go 1.16 + +require ( + github.com/BurntSushi/toml v0.3.1 + github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/Masterminds/goutils v1.1.1 + github.com/Masterminds/semver/v3 v3.1.1 + github.com/Masterminds/sprig/v3 v3.2.2 + github.com/Masterminds/squirrel v1.5.0 + github.com/Masterminds/vcs v1.13.1 + github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 + github.com/containerd/containerd v1.4.4 + github.com/cyphar/filepath-securejoin v0.2.2 + github.com/deislabs/oras v0.10.0 + github.com/docker/distribution v2.7.1+incompatible + github.com/docker/docker v1.4.2-0.20200203170920-46ec8731fbce + github.com/docker/go-units v0.4.0 + github.com/evanphx/json-patch v4.9.0+incompatible + github.com/gobwas/glob v0.2.3 + github.com/gofrs/flock v0.8.0 + github.com/gosuri/uitable v0.0.4 + github.com/jmoiron/sqlx v1.3.1 + github.com/lib/pq v1.10.0 + github.com/mattn/go-shellwords v1.0.11 + github.com/mitchellh/copystructure v1.1.1 + github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.0.1 + github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 + github.com/pkg/errors v0.9.1 + github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 + github.com/sirupsen/logrus v1.8.1 + github.com/spf13/cobra v1.1.3 + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.7.0 + github.com/xeipuuv/gojsonschema v1.2.0 + golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad + golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 + k8s.io/api v0.20.4 + k8s.io/apiextensions-apiserver v0.20.4 + k8s.io/apimachinery v0.20.4 + k8s.io/apiserver v0.20.4 + k8s.io/cli-runtime v0.20.4 + k8s.io/client-go v0.20.4 + k8s.io/klog/v2 v2.8.0 + k8s.io/kubectl v0.20.4 + sigs.k8s.io/yaml v1.2.0 +) + +replace ( + github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d + github.com/docker/docker => github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000000..a715f3c92db --- /dev/null +++ b/go.sum @@ -0,0 +1,1066 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1 h1:eVvIXUKiTgv++6YnWb42DUA1YL7qDugnKP0HljexdnQ= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5 h1:Y3bBUV4rTuxenJJs41HU3qmqsb+auo+a3Lz+PlJPpL0= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8= +github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Masterminds/vcs v1.13.1 h1:NL3G1X7/7xduQtA2sJLpVpfHTNBALVNSjob6KEjPXNQ= +github.com/Masterminds/vcs v1.13.1/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/hcsshim v0.8.14 h1:lbPVK25c1cu5xTLITwpUcxoA9vKrKErASPYygvouJns= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= +github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1 h1:pgAtgj+A31JBVtEHu2uHuEx0n+2ukqUJnS2vVe5pQNA= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 h1:qWj4qVYZ95vLWwqyNJCQg7rDsG5wPdze0UaPolH7DUk= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.4 h1:rtRG4N6Ct7GNssATwgpvMGfnjnwfjnu/Zs9W3Ikzq+M= +github.com/containerd/containerd v1.4.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7 h1:6ejg6Lkk8dskcM7wQ28gONkukbQkM4qpj4RnYbpFzrI= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= +github.com/deislabs/oras v0.10.0 h1:Eufbi8zVaULb7vYj5HKM9qv9qw6fJ7P75JSjn//gR0E= +github.com/deislabs/oras v0.10.0/go.mod h1:N1UzE7rBa9qLyN4l8IlBTxc2PkrRcKgWQ3HTJvRnJRE= +github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/cli v20.10.3+incompatible h1:WVEgoV/GpsTK5hruhHdYi79blQ+nmcm+7Ru/ZuiF+7E= +github.com/docker/cli v20.10.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v0.0.0-20191216044856-a8371794149d h1:jC8tT/S0OGx2cswpeUTn4gOIea8P08lD3VFQT0cOZ50= +github.com/docker/distribution v0.0.0-20191216044856-a8371794149d/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916 h1:yWHOI+vFjEsAakUTSrtqc/SAHrhSkmn48pqjidZX3QA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8= +github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg= +github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o= +github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg= +github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= +github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= +github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8= +github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33 h1:893HsJqtxp9z1SF76gg6hY70hRY1wVlTSnC/h1yUDCo= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmoiron/sqlx v1.3.1 h1:aLN7YINNZ7cYOPK3QC83dbM6KT0NMqVMw961TqrejlE= +github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-shellwords v1.0.11 h1:vCoR9VPpsk/TZFW2JwK5I9S0xdrtUq2bph6/YjEPnaw= +github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4= +github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f h1:2+myh5ml7lgEU/51gbeLHfKGNfgEQQIWrlbdaOsidbQ= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= +github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:NT0cwArZg/wGdvY8pzej4tPr+9WGmDdkF8Suj+mkz2g= +github.com/moby/moby v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.4.0 h1:LUa41nrWTQNGhzdsZ5lTnkwbNjj6rXTdazA1cSdjkOY= +github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 h1:HXr/qUllAWv9riaI4zh2eXWKmCSDqVS/XH1MRHLKRwk= +github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= +github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= +gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.20.4 h1:xZjKidCirayzX6tHONRQyTNDVIR55TYVqgATqo6ZULY= +k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= +k8s.io/apiextensions-apiserver v0.20.4 h1:VO/Y5PwBdznMIctX/vvgSNhxffikEmcLC/V1bpbhHhU= +k8s.io/apiextensions-apiserver v0.20.4/go.mod h1:Hzebis/9c6Io5yzHp24Vg4XOkTp1ViMwKP/6gmpsfA4= +k8s.io/apimachinery v0.20.4 h1:vhxQ0PPUUU2Ns1b9r4/UFp13UPs8cw2iOoTjnY9faa0= +k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apiserver v0.20.4 h1:zMMKIgIUDIFiwK3LyY7qOV4Z4wKsHVYExL6vXY9fPX4= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/cli-runtime v0.20.4 h1:jVU13lBeebHLtarHeHkoIi3uRONFzccmP7hHLzEoQ4w= +k8s.io/cli-runtime v0.20.4/go.mod h1:dz38e1CM4uuIhy8PMFUZv7qsvIdoE3ByZYlmbHNCkt4= +k8s.io/client-go v0.20.4 h1:85crgh1IotNkLpKYKZHVNI1JT86nr/iDCvq2iWKsql4= +k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= +k8s.io/code-generator v0.20.4/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= +k8s.io/component-base v0.20.4 h1:gdvPs4G11e99meQnW4zN+oYOjH8qkLz1sURrAzvKWqc= +k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= +k8s.io/component-helpers v0.20.4/go.mod h1:S7jGg8zQp3kwvSzfuGtNaQAMVmvzomXDioTm5vABn9g= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= +k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kubectl v0.20.4 h1:Y1gUiigiZM+ulcrnWeqSHlTd0/7xWcQIXjuMnjtHyoo= +k8s.io/kubectl v0.20.4/go.mod h1:yCC5lUQyXRmmtwyxfaakryh9ezzp/bT0O14LeoFLbGo= +k8s.io/metrics v0.20.4/go.mod h1:DDXS+Ls+2NAxRcVhXKghRPa3csljyJRjDRjPe6EOg/g= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= +sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/internal/experimental/registry/authorizer.go b/internal/experimental/registry/authorizer.go new file mode 100644 index 00000000000..918a999bad1 --- /dev/null +++ b/internal/experimental/registry/authorizer.go @@ -0,0 +1,28 @@ +/* +Copyright The Helm Authors. + +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 registry // import "helm.sh/helm/v3/internal/experimental/registry" + +import ( + "github.com/deislabs/oras/pkg/auth" +) + +type ( + // Authorizer handles registry auth operations + Authorizer struct { + auth.Client + } +) diff --git a/internal/experimental/registry/cache.go b/internal/experimental/registry/cache.go new file mode 100644 index 00000000000..5aca6366803 --- /dev/null +++ b/internal/experimental/registry/cache.go @@ -0,0 +1,368 @@ +/* +Copyright The Helm Authors. + +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 registry // import "helm.sh/helm/v3/internal/experimental/registry" + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "time" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/errdefs" + orascontent "github.com/deislabs/oras/pkg/content" + digest "github.com/opencontainers/go-digest" + specs "github.com/opencontainers/image-spec/specs-go" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" +) + +const ( + // CacheRootDir is the root directory for a cache + CacheRootDir = "cache" +) + +type ( + // Cache handles local/in-memory storage of Helm charts, compliant with OCI Layout + Cache struct { + debug bool + out io.Writer + rootDir string + ociStore *orascontent.OCIStore + memoryStore *orascontent.Memorystore + } + + // CacheRefSummary contains as much info as available describing a chart reference in cache + // Note: fields here are sorted by the order in which they are set in FetchReference method + CacheRefSummary struct { + Name string + Repo string + Tag string + Exists bool + Manifest *ocispec.Descriptor + Config *ocispec.Descriptor + ContentLayer *ocispec.Descriptor + Size int64 + Digest digest.Digest + CreatedAt time.Time + Chart *chart.Chart + } +) + +// NewCache returns a new OCI Layout-compliant cache with config +func NewCache(opts ...CacheOption) (*Cache, error) { + cache := &Cache{ + out: ioutil.Discard, + } + for _, opt := range opts { + opt(cache) + } + // validate + if cache.rootDir == "" { + return nil, errors.New("must set cache root dir on initialization") + } + return cache, nil +} + +// FetchReference retrieves a chart ref from cache +func (cache *Cache) FetchReference(ref *Reference) (*CacheRefSummary, error) { + if err := cache.init(); err != nil { + return nil, err + } + r := CacheRefSummary{ + Name: ref.FullName(), + Repo: ref.Repo, + Tag: ref.Tag, + } + for _, desc := range cache.ociStore.ListReferences() { + if desc.Annotations[ocispec.AnnotationRefName] == r.Name { + r.Exists = true + manifestBytes, err := cache.fetchBlob(&desc) + if err != nil { + return &r, err + } + var manifest ocispec.Manifest + err = json.Unmarshal(manifestBytes, &manifest) + if err != nil { + return &r, err + } + r.Manifest = &desc + r.Config = &manifest.Config + numLayers := len(manifest.Layers) + if numLayers != 1 { + return &r, errors.New( + fmt.Sprintf("manifest does not contain exactly 1 layer (total: %d)", numLayers)) + } + var contentLayer *ocispec.Descriptor + for _, layer := range manifest.Layers { + switch layer.MediaType { + case HelmChartContentLayerMediaType: + contentLayer = &layer + } + } + if contentLayer == nil { + return &r, errors.New( + fmt.Sprintf("manifest does not contain a layer with mediatype %s", HelmChartContentLayerMediaType)) + } + if contentLayer.Size == 0 { + return &r, errors.New( + fmt.Sprintf("manifest layer with mediatype %s is of size 0", HelmChartContentLayerMediaType)) + } + r.ContentLayer = contentLayer + info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest) + if err != nil { + return &r, err + } + r.Size = info.Size + r.Digest = info.Digest + r.CreatedAt = info.CreatedAt + contentBytes, err := cache.fetchBlob(contentLayer) + if err != nil { + return &r, err + } + ch, err := loader.LoadArchive(bytes.NewBuffer(contentBytes)) + if err != nil { + return &r, err + } + r.Chart = ch + } + } + return &r, nil +} + +// StoreReference stores a chart ref in cache +func (cache *Cache) StoreReference(ref *Reference, ch *chart.Chart) (*CacheRefSummary, error) { + if err := cache.init(); err != nil { + return nil, err + } + r := CacheRefSummary{ + Name: ref.FullName(), + Repo: ref.Repo, + Tag: ref.Tag, + Chart: ch, + } + existing, _ := cache.FetchReference(ref) + r.Exists = existing.Exists + config, _, err := cache.saveChartConfig(ch) + if err != nil { + return &r, err + } + r.Config = config + contentLayer, _, err := cache.saveChartContentLayer(ch) + if err != nil { + return &r, err + } + r.ContentLayer = contentLayer + info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest) + if err != nil { + return &r, err + } + r.Size = info.Size + r.Digest = info.Digest + r.CreatedAt = info.CreatedAt + manifest, _, err := cache.saveChartManifest(config, contentLayer) + if err != nil { + return &r, err + } + r.Manifest = manifest + return &r, nil +} + +// DeleteReference deletes a chart ref from cache +// TODO: garbage collection, only manifest removed +func (cache *Cache) DeleteReference(ref *Reference) (*CacheRefSummary, error) { + if err := cache.init(); err != nil { + return nil, err + } + r, err := cache.FetchReference(ref) + if err != nil || !r.Exists { + return r, err + } + cache.ociStore.DeleteReference(r.Name) + err = cache.ociStore.SaveIndex() + return r, err +} + +// ListReferences lists all chart refs in a cache +func (cache *Cache) ListReferences() ([]*CacheRefSummary, error) { + if err := cache.init(); err != nil { + return nil, err + } + var rr []*CacheRefSummary + for _, desc := range cache.ociStore.ListReferences() { + name := desc.Annotations[ocispec.AnnotationRefName] + if name == "" { + if cache.debug { + fmt.Fprintf(cache.out, "warning: found manifest without name: %s", desc.Digest.Hex()) + } + continue + } + ref, err := ParseReference(name) + if err != nil { + return rr, err + } + r, err := cache.FetchReference(ref) + if err != nil { + return rr, err + } + rr = append(rr, r) + } + return rr, nil +} + +// AddManifest provides a manifest to the cache index.json +func (cache *Cache) AddManifest(ref *Reference, manifest *ocispec.Descriptor) error { + if err := cache.init(); err != nil { + return err + } + cache.ociStore.AddReference(ref.FullName(), *manifest) + err := cache.ociStore.SaveIndex() + return err +} + +// Provider provides a valid containerd Provider +func (cache *Cache) Provider() content.Provider { + return content.Provider(cache.ociStore) +} + +// Ingester provides a valid containerd Ingester +func (cache *Cache) Ingester() content.Ingester { + return content.Ingester(cache.ociStore) +} + +// ProvideIngester provides a valid oras ProvideIngester +func (cache *Cache) ProvideIngester() orascontent.ProvideIngester { + return orascontent.ProvideIngester(cache.ociStore) +} + +// init creates files needed necessary for OCI layout store +func (cache *Cache) init() error { + if cache.ociStore == nil { + ociStore, err := orascontent.NewOCIStore(cache.rootDir) + if err != nil { + return err + } + cache.ociStore = ociStore + cache.memoryStore = orascontent.NewMemoryStore() + } + return nil +} + +// saveChartConfig stores the Chart.yaml as json blob and returns a descriptor +func (cache *Cache) saveChartConfig(ch *chart.Chart) (*ocispec.Descriptor, bool, error) { + configBytes, err := json.Marshal(ch.Metadata) + if err != nil { + return nil, false, err + } + configExists, err := cache.storeBlob(configBytes) + if err != nil { + return nil, configExists, err + } + descriptor := cache.memoryStore.Add("", HelmChartConfigMediaType, configBytes) + return &descriptor, configExists, nil +} + +// saveChartContentLayer stores the chart as tarball blob and returns a descriptor +func (cache *Cache) saveChartContentLayer(ch *chart.Chart) (*ocispec.Descriptor, bool, error) { + destDir := filepath.Join(cache.rootDir, ".build") + os.MkdirAll(destDir, 0755) + tmpFile, err := chartutil.Save(ch, destDir) + defer os.Remove(tmpFile) + if err != nil { + return nil, false, errors.Wrap(err, "failed to save") + } + contentBytes, err := ioutil.ReadFile(tmpFile) + if err != nil { + return nil, false, err + } + contentExists, err := cache.storeBlob(contentBytes) + if err != nil { + return nil, contentExists, err + } + descriptor := cache.memoryStore.Add("", HelmChartContentLayerMediaType, contentBytes) + return &descriptor, contentExists, nil +} + +// saveChartManifest stores the chart manifest as json blob and returns a descriptor +func (cache *Cache) saveChartManifest(config *ocispec.Descriptor, contentLayer *ocispec.Descriptor) (*ocispec.Descriptor, bool, error) { + manifest := ocispec.Manifest{ + Versioned: specs.Versioned{SchemaVersion: 2}, + Config: *config, + Layers: []ocispec.Descriptor{*contentLayer}, + } + manifestBytes, err := json.Marshal(manifest) + if err != nil { + return nil, false, err + } + manifestExists, err := cache.storeBlob(manifestBytes) + if err != nil { + return nil, manifestExists, err + } + descriptor := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromBytes(manifestBytes), + Size: int64(len(manifestBytes)), + } + return &descriptor, manifestExists, nil +} + +// storeBlob stores a blob on filesystem +func (cache *Cache) storeBlob(blobBytes []byte) (bool, error) { + var exists bool + writer, err := cache.ociStore.Store.Writer(ctx(cache.out, cache.debug), + content.WithRef(digest.FromBytes(blobBytes).Hex())) + if err != nil { + return exists, err + } + _, err = writer.Write(blobBytes) + if err != nil { + return exists, err + } + err = writer.Commit(ctx(cache.out, cache.debug), 0, writer.Digest()) + if err != nil { + if !errdefs.IsAlreadyExists(err) { + return exists, err + } + exists = true + } + err = writer.Close() + return exists, err +} + +// fetchBlob retrieves a blob from filesystem +func (cache *Cache) fetchBlob(desc *ocispec.Descriptor) ([]byte, error) { + reader, err := cache.ociStore.ReaderAt(ctx(cache.out, cache.debug), *desc) + if err != nil { + return nil, err + } + defer reader.Close() + + bytes := make([]byte, desc.Size) + _, err = reader.ReadAt(bytes, 0) + if err != nil { + return nil, err + } + return bytes, nil +} diff --git a/internal/experimental/registry/cache_opts.go b/internal/experimental/registry/cache_opts.go new file mode 100644 index 00000000000..6851ae80754 --- /dev/null +++ b/internal/experimental/registry/cache_opts.go @@ -0,0 +1,48 @@ +/* +Copyright The Helm Authors. + +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 registry // import "helm.sh/helm/v3/internal/experimental/registry" + +import ( + "io" +) + +type ( + // CacheOption allows specifying various settings configurable by the user for overriding the defaults + // used when creating a new default cache + CacheOption func(*Cache) +) + +// CacheOptDebug returns a function that sets the debug setting on cache options set +func CacheOptDebug(debug bool) CacheOption { + return func(cache *Cache) { + cache.debug = debug + } +} + +// CacheOptWriter returns a function that sets the writer setting on cache options set +func CacheOptWriter(out io.Writer) CacheOption { + return func(cache *Cache) { + cache.out = out + } +} + +// CacheOptRoot returns a function that sets the root directory setting on cache options set +func CacheOptRoot(rootDir string) CacheOption { + return func(cache *Cache) { + cache.rootDir = rootDir + } +} diff --git a/internal/experimental/registry/client.go b/internal/experimental/registry/client.go new file mode 100644 index 00000000000..c889ee91325 --- /dev/null +++ b/internal/experimental/registry/client.go @@ -0,0 +1,336 @@ +/* +Copyright The Helm Authors. + +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 registry // import "helm.sh/helm/v3/internal/experimental/registry" + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "net/http" + "sort" + + auth "github.com/deislabs/oras/pkg/auth/docker" + "github.com/deislabs/oras/pkg/content" + "github.com/deislabs/oras/pkg/oras" + "github.com/gosuri/uitable" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/helmpath" +) + +const ( + // CredentialsFileBasename is the filename for auth credentials file + CredentialsFileBasename = "config.json" +) + +type ( + // Client works with OCI-compliant registries and local Helm chart cache + Client struct { + debug bool + // path to repository config file e.g. ~/.docker/config.json + credentialsFile string + out io.Writer + authorizer *Authorizer + resolver *Resolver + cache *Cache + } +) + +// NewClient returns a new registry client with config +func NewClient(opts ...ClientOption) (*Client, error) { + client := &Client{ + out: ioutil.Discard, + } + for _, opt := range opts { + opt(client) + } + // set defaults if fields are missing + if client.credentialsFile == "" { + client.credentialsFile = helmpath.CachePath("registry", CredentialsFileBasename) + } + if client.authorizer == nil { + authClient, err := auth.NewClient(client.credentialsFile) + if err != nil { + return nil, err + } + client.authorizer = &Authorizer{ + Client: authClient, + } + } + if client.resolver == nil { + resolver, err := client.authorizer.Resolver(context.Background(), http.DefaultClient, false) + if err != nil { + return nil, err + } + client.resolver = &Resolver{ + Resolver: resolver, + } + } + if client.cache == nil { + cache, err := NewCache( + CacheOptDebug(client.debug), + CacheOptWriter(client.out), + CacheOptRoot(helmpath.CachePath("registry", CacheRootDir)), + ) + if err != nil { + return nil, err + } + client.cache = cache + } + return client, nil +} + +// Login logs into a registry +func (c *Client) Login(hostname string, username string, password string, insecure bool) error { + err := c.authorizer.Login(ctx(c.out, c.debug), hostname, username, password, insecure) + if err != nil { + return err + } + fmt.Fprintf(c.out, "Login succeeded\n") + return nil +} + +// Logout logs out of a registry +func (c *Client) Logout(hostname string) error { + err := c.authorizer.Logout(ctx(c.out, c.debug), hostname) + if err != nil { + return err + } + fmt.Fprintln(c.out, "Logout succeeded") + return nil +} + +// PushChart uploads a chart to a registry +func (c *Client) PushChart(ref *Reference) error { + r, err := c.cache.FetchReference(ref) + if err != nil { + return err + } + if !r.Exists { + return errors.New(fmt.Sprintf("Chart not found: %s", r.Name)) + } + fmt.Fprintf(c.out, "The push refers to repository [%s]\n", r.Repo) + c.printCacheRefSummary(r) + layers := []ocispec.Descriptor{*r.ContentLayer} + _, err = oras.Push(ctx(c.out, c.debug), c.resolver, r.Name, c.cache.Provider(), layers, + oras.WithConfig(*r.Config), oras.WithNameValidation(nil)) + if err != nil { + return err + } + s := "" + numLayers := len(layers) + if 1 < numLayers { + s = "s" + } + fmt.Fprintf(c.out, + "%s: pushed to remote (%d layer%s, %s total)\n", r.Tag, numLayers, s, byteCountBinary(r.Size)) + return nil +} + +// PullChart downloads a chart from a registry +func (c *Client) PullChart(ref *Reference) (*bytes.Buffer, error) { + buf := bytes.NewBuffer(nil) + + if ref.Tag == "" { + return buf, errors.New("tag explicitly required") + } + + fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo) + + store := content.NewMemoryStore() + fullname := ref.FullName() + _ = fullname + _, layerDescriptors, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), store, + oras.WithPullEmptyNameAllowed(), + oras.WithAllowedMediaTypes(KnownMediaTypes())) + if err != nil { + return buf, err + } + + numLayers := len(layerDescriptors) + if numLayers < 1 { + return buf, errors.New( + fmt.Sprintf("manifest does not contain at least 1 layer (total: %d)", numLayers)) + } + + var contentLayer *ocispec.Descriptor + for _, layer := range layerDescriptors { + layer := layer + switch layer.MediaType { + case HelmChartContentLayerMediaType: + contentLayer = &layer + + } + } + + if contentLayer == nil { + return buf, errors.New( + fmt.Sprintf("manifest does not contain a layer with mediatype %s", + HelmChartContentLayerMediaType)) + } + + _, b, ok := store.Get(*contentLayer) + if !ok { + return buf, errors.Errorf("Unable to retrieve blob with digest %s", contentLayer.Digest) + } + + buf = bytes.NewBuffer(b) + return buf, nil +} + +// PullChartToCache pulls a chart from an OCI Registry to the Registry Cache. +// This function is needed for `helm chart pull`, which is experimental and will be deprecated soon. +// Likewise, the Registry cache will soon be deprecated as will this function. +func (c *Client) PullChartToCache(ref *Reference) error { + if ref.Tag == "" { + return errors.New("tag explicitly required") + } + existing, err := c.cache.FetchReference(ref) + if err != nil { + return err + } + fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo) + manifest, _, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), c.cache.Ingester(), + oras.WithPullEmptyNameAllowed(), + oras.WithAllowedMediaTypes(KnownMediaTypes()), + oras.WithContentProvideIngester(c.cache.ProvideIngester())) + if err != nil { + return err + } + err = c.cache.AddManifest(ref, &manifest) + if err != nil { + return err + } + r, err := c.cache.FetchReference(ref) + if err != nil { + return err + } + if !r.Exists { + return errors.New(fmt.Sprintf("Chart not found: %s", r.Name)) + } + c.printCacheRefSummary(r) + if !existing.Exists { + fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s\n", ref.FullName()) + } else { + fmt.Fprintf(c.out, "Status: Chart is up to date for %s\n", ref.FullName()) + } + return err +} + +// SaveChart stores a copy of chart in local cache +func (c *Client) SaveChart(ch *chart.Chart, ref *Reference) error { + r, err := c.cache.StoreReference(ref, ch) + if err != nil { + return err + } + c.printCacheRefSummary(r) + err = c.cache.AddManifest(ref, r.Manifest) + if err != nil { + return err + } + fmt.Fprintf(c.out, "%s: saved\n", r.Tag) + return nil +} + +// LoadChart retrieves a chart object by reference +func (c *Client) LoadChart(ref *Reference) (*chart.Chart, error) { + r, err := c.cache.FetchReference(ref) + if err != nil { + return nil, err + } + if !r.Exists { + return nil, errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName())) + } + c.printCacheRefSummary(r) + return r.Chart, nil +} + +// RemoveChart deletes a locally saved chart +func (c *Client) RemoveChart(ref *Reference) error { + r, err := c.cache.DeleteReference(ref) + if err != nil { + return err + } + if !r.Exists { + return errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName())) + } + fmt.Fprintf(c.out, "%s: removed\n", r.Tag) + return nil +} + +// PrintChartTable prints a list of locally stored charts +func (c *Client) PrintChartTable() error { + table := uitable.New() + table.MaxColWidth = 60 + table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED") + rows, err := c.getChartTableRows() + if err != nil { + return err + } + for _, row := range rows { + table.AddRow(row...) + } + fmt.Fprintln(c.out, table.String()) + return nil +} + +// printCacheRefSummary prints out chart ref summary +func (c *Client) printCacheRefSummary(r *CacheRefSummary) { + fmt.Fprintf(c.out, "ref: %s\n", r.Name) + fmt.Fprintf(c.out, "digest: %s\n", r.Manifest.Digest.Hex()) + fmt.Fprintf(c.out, "size: %s\n", byteCountBinary(r.Size)) + fmt.Fprintf(c.out, "name: %s\n", r.Chart.Metadata.Name) + fmt.Fprintf(c.out, "version: %s\n", r.Chart.Metadata.Version) +} + +// getChartTableRows returns rows in uitable-friendly format +func (c *Client) getChartTableRows() ([][]interface{}, error) { + rr, err := c.cache.ListReferences() + if err != nil { + return nil, err + } + refsMap := map[string]map[string]string{} + for _, r := range rr { + refsMap[r.Name] = map[string]string{ + "name": r.Chart.Metadata.Name, + "version": r.Chart.Metadata.Version, + "digest": shortDigest(r.Manifest.Digest.Hex()), + "size": byteCountBinary(r.Size), + "created": timeAgo(r.CreatedAt), + } + } + // Sort and convert to format expected by uitable + rows := make([][]interface{}, len(refsMap)) + keys := make([]string, 0, len(refsMap)) + for key := range refsMap { + keys = append(keys, key) + } + sort.Strings(keys) + for i, key := range keys { + rows[i] = make([]interface{}, 6) + rows[i][0] = key + ref := refsMap[key] + for j, k := range []string{"name", "version", "digest", "size", "created"} { + rows[i][j+1] = ref[k] + } + } + return rows, nil +} diff --git a/internal/experimental/registry/client_opts.go b/internal/experimental/registry/client_opts.go new file mode 100644 index 00000000000..e2f742aec5a --- /dev/null +++ b/internal/experimental/registry/client_opts.go @@ -0,0 +1,69 @@ +/* +Copyright The Helm Authors. + +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 registry // import "helm.sh/helm/v3/internal/experimental/registry" + +import ( + "io" +) + +type ( + // ClientOption allows specifying various settings configurable by the user for overriding the defaults + // used when creating a new default client + ClientOption func(*Client) +) + +// ClientOptDebug returns a function that sets the debug setting on client options set +func ClientOptDebug(debug bool) ClientOption { + return func(client *Client) { + client.debug = debug + } +} + +// ClientOptWriter returns a function that sets the writer setting on client options set +func ClientOptWriter(out io.Writer) ClientOption { + return func(client *Client) { + client.out = out + } +} + +// ClientOptResolver returns a function that sets the resolver setting on client options set +func ClientOptResolver(resolver *Resolver) ClientOption { + return func(client *Client) { + client.resolver = resolver + } +} + +// ClientOptAuthorizer returns a function that sets the authorizer setting on client options set +func ClientOptAuthorizer(authorizer *Authorizer) ClientOption { + return func(client *Client) { + client.authorizer = authorizer + } +} + +// ClientOptCache returns a function that sets the cache setting on a client options set +func ClientOptCache(cache *Cache) ClientOption { + return func(client *Client) { + client.cache = cache + } +} + +// ClientOptCredentialsFile returns a function that sets the cache setting on a client options set +func ClientOptCredentialsFile(credentialsFile string) ClientOption { + return func(client *Client) { + client.credentialsFile = credentialsFile + } +} diff --git a/internal/experimental/registry/client_test.go b/internal/experimental/registry/client_test.go new file mode 100644 index 00000000000..a9936ba1389 --- /dev/null +++ b/internal/experimental/registry/client_test.go @@ -0,0 +1,294 @@ +/* +Copyright The Helm Authors. + +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 registry + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/containerd/containerd/errdefs" + auth "github.com/deislabs/oras/pkg/auth/docker" + "github.com/docker/distribution/configuration" + "github.com/docker/distribution/registry" + _ "github.com/docker/distribution/registry/auth/htpasswd" + _ "github.com/docker/distribution/registry/storage/driver/inmemory" + "github.com/phayes/freeport" + "github.com/stretchr/testify/suite" + "golang.org/x/crypto/bcrypt" + + "helm.sh/helm/v3/pkg/chart" +) + +var ( + testCacheRootDir = "helm-registry-test" + testHtpasswdFileBasename = "authtest.htpasswd" + testUsername = "myuser" + testPassword = "mypass" +) + +type RegistryClientTestSuite struct { + suite.Suite + Out io.Writer + DockerRegistryHost string + CompromisedRegistryHost string + CacheRootDir string + RegistryClient *Client +} + +func (suite *RegistryClientTestSuite) SetupSuite() { + suite.CacheRootDir = testCacheRootDir + os.RemoveAll(suite.CacheRootDir) + os.Mkdir(suite.CacheRootDir, 0700) + + var out bytes.Buffer + suite.Out = &out + credentialsFile := filepath.Join(suite.CacheRootDir, CredentialsFileBasename) + + client, err := auth.NewClient(credentialsFile) + suite.Nil(err, "no error creating auth client") + + resolver, err := client.Resolver(context.Background(), http.DefaultClient, false) + suite.Nil(err, "no error creating resolver") + + // create cache + cache, err := NewCache( + CacheOptDebug(true), + CacheOptWriter(suite.Out), + CacheOptRoot(filepath.Join(suite.CacheRootDir, CacheRootDir)), + ) + suite.Nil(err, "no error creating cache") + + // init test client + suite.RegistryClient, err = NewClient( + ClientOptDebug(true), + ClientOptWriter(suite.Out), + ClientOptAuthorizer(&Authorizer{ + Client: client, + }), + ClientOptResolver(&Resolver{ + Resolver: resolver, + }), + ClientOptCache(cache), + ) + suite.Nil(err, "no error creating registry client") + + // create htpasswd file (w BCrypt, which is required) + pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) + suite.Nil(err, "no error generating bcrypt password for test htpasswd file") + htpasswdPath := filepath.Join(suite.CacheRootDir, testHtpasswdFileBasename) + err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644) + suite.Nil(err, "no error creating test htpasswd file") + + // Registry config + config := &configuration.Configuration{} + port, err := freeport.GetFreePort() + suite.Nil(err, "no error finding free port for test registry") + suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port) + config.HTTP.Addr = fmt.Sprintf(":%d", port) + config.HTTP.DrainTimeout = time.Duration(10) * time.Second + config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} + config.Auth = configuration.Auth{ + "htpasswd": configuration.Parameters{ + "realm": "localhost", + "path": htpasswdPath, + }, + } + dockerRegistry, err := registry.NewRegistry(context.Background(), config) + suite.Nil(err, "no error creating test registry") + + suite.CompromisedRegistryHost = initCompromisedRegistryTestServer() + + // Start Docker registry + go dockerRegistry.ListenAndServe() +} + +func (suite *RegistryClientTestSuite) TearDownSuite() { + os.RemoveAll(suite.CacheRootDir) +} + +func (suite *RegistryClientTestSuite) Test_0_Login() { + err := suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad", false) + suite.NotNil(err, "error logging into registry with bad credentials") + + err = suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad", true) + suite.NotNil(err, "error logging into registry with bad credentials, insecure mode") + + err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword, false) + suite.Nil(err, "no error logging into registry with good credentials") + + err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword, true) + suite.Nil(err, "no error logging into registry with good credentials, insecure mode") +} + +func (suite *RegistryClientTestSuite) Test_1_SaveChart() { + ref, err := ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) + suite.Nil(err) + + // empty chart + err = suite.RegistryClient.SaveChart(&chart.Chart{}, ref) + suite.NotNil(err) + + // valid chart + ch := &chart.Chart{} + ch.Metadata = &chart.Metadata{ + APIVersion: "v1", + Name: "testchart", + Version: "1.2.3", + } + err = suite.RegistryClient.SaveChart(ch, ref) + suite.Nil(err) +} + +func (suite *RegistryClientTestSuite) Test_2_LoadChart() { + + // non-existent ref + ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) + suite.Nil(err) + _, err = suite.RegistryClient.LoadChart(ref) + suite.NotNil(err) + + // existing ref + ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) + suite.Nil(err) + ch, err := suite.RegistryClient.LoadChart(ref) + suite.Nil(err) + suite.Equal("testchart", ch.Metadata.Name) + suite.Equal("1.2.3", ch.Metadata.Version) +} + +func (suite *RegistryClientTestSuite) Test_3_PushChart() { + + // non-existent ref + ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) + suite.Nil(err) + err = suite.RegistryClient.PushChart(ref) + suite.NotNil(err) + + // existing ref + ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) + suite.Nil(err) + err = suite.RegistryClient.PushChart(ref) + suite.Nil(err) +} + +func (suite *RegistryClientTestSuite) Test_4_PullChart() { + + // non-existent ref + ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) + suite.Nil(err) + _, err = suite.RegistryClient.PullChart(ref) + suite.NotNil(err) + + // existing ref + ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) + suite.Nil(err) + _, err = suite.RegistryClient.PullChart(ref) + suite.Nil(err) +} + +func (suite *RegistryClientTestSuite) Test_5_PrintChartTable() { + err := suite.RegistryClient.PrintChartTable() + suite.Nil(err) +} + +func (suite *RegistryClientTestSuite) Test_6_RemoveChart() { + + // non-existent ref + ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) + suite.Nil(err) + err = suite.RegistryClient.RemoveChart(ref) + suite.NotNil(err) + + // existing ref + ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) + suite.Nil(err) + err = suite.RegistryClient.RemoveChart(ref) + suite.Nil(err) +} + +func (suite *RegistryClientTestSuite) Test_7_Logout() { + err := suite.RegistryClient.Logout("this-host-aint-real:5000") + suite.NotNil(err, "error logging out of registry that has no entry") + + err = suite.RegistryClient.Logout(suite.DockerRegistryHost) + suite.Nil(err, "no error logging out of registry") +} + +func (suite *RegistryClientTestSuite) Test_8_ManInTheMiddle() { + ref, err := ParseReference(fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost)) + suite.Nil(err) + + // returns content that does not match the expected digest + _, err = suite.RegistryClient.PullChart(ref) + suite.NotNil(err) + suite.True(errdefs.IsFailedPrecondition(err)) +} + +func TestRegistryClientTestSuite(t *testing.T) { + suite.Run(t, new(RegistryClientTestSuite)) +} + +func initCompromisedRegistryTestServer() string { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "manifests") { + w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json") + w.WriteHeader(200) + + // layers[0] is the blob []byte("a") + w.Write([]byte( + `{ "schemaVersion": 2, "config": { + "mediaType": "application/vnd.cncf.helm.config.v1+json", + "digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133", + "size": 181 + }, + "layers": [ + { + "mediaType": "application/tar+gzip", + "digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", + "size": 1 + } + ] +}`)) + } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write([]byte("{\"name\":\"mychart\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\\n" + + "an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" + + "\"application\"}")) + } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" { + w.Header().Set("Content-Type", "application/tar+gzip") + w.WriteHeader(200) + w.Write([]byte("b")) + } else { + w.WriteHeader(500) + } + })) + + u, _ := url.Parse(s.URL) + return fmt.Sprintf("localhost:%s", u.Port()) +} diff --git a/internal/experimental/registry/constants.go b/internal/experimental/registry/constants.go new file mode 100644 index 00000000000..dafb3c9e59b --- /dev/null +++ b/internal/experimental/registry/constants.go @@ -0,0 +1,33 @@ +/* +Copyright The Helm Authors. + +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 registry // import "helm.sh/helm/v3/internal/experimental/registry" + +const ( + // HelmChartConfigMediaType is the reserved media type for the Helm chart manifest config + HelmChartConfigMediaType = "application/vnd.cncf.helm.config.v1+json" + + // HelmChartContentLayerMediaType is the reserved media type for Helm chart package content + HelmChartContentLayerMediaType = "application/tar+gzip" +) + +// KnownMediaTypes returns a list of layer mediaTypes that the Helm client knows about +func KnownMediaTypes() []string { + return []string{ + HelmChartConfigMediaType, + HelmChartContentLayerMediaType, + } +} diff --git a/internal/experimental/registry/constants_test.go b/internal/experimental/registry/constants_test.go new file mode 100644 index 00000000000..9f078e632fc --- /dev/null +++ b/internal/experimental/registry/constants_test.go @@ -0,0 +1,29 @@ +/* +Copyright The Helm Authors. + +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 registry + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConstants(t *testing.T) { + knownMediaTypes := KnownMediaTypes() + assert.Contains(t, knownMediaTypes, HelmChartConfigMediaType) + assert.Contains(t, knownMediaTypes, HelmChartContentLayerMediaType) +} diff --git a/internal/experimental/registry/reference.go b/internal/experimental/registry/reference.go new file mode 100644 index 00000000000..f0e91d4ba66 --- /dev/null +++ b/internal/experimental/registry/reference.go @@ -0,0 +1,146 @@ +/* +Copyright The Helm Authors. + +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 registry // import "helm.sh/helm/v3/internal/experimental/registry" + +import ( + "errors" + "fmt" + "net/url" + "regexp" + "strconv" + "strings" +) + +var ( + validPortRegEx = regexp.MustCompile(`^([1-9]\d{0,3}|0|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$`) // adapted from https://stackoverflow.com/a/12968117 + // TODO: Currently we don't support digests, so we are only splitting on the + // colon. However, when we add support for digests, we'll need to use the + // regexp anyway to split on both colons and @, so leaving it like this for + // now + referenceDelimiter = regexp.MustCompile(`[:]`) + errEmptyRepo = errors.New("parsed repo was empty") + errTooManyColons = errors.New("ref may only contain a single colon character (:) unless specifying a port number") +) + +type ( + // Reference defines the main components of a reference specification + Reference struct { + Tag string + Repo string + } +) + +// ParseReference converts a string to a Reference +func ParseReference(s string) (*Reference, error) { + if s == "" { + return nil, errEmptyRepo + } + // Split the components of the string on the colon or @, if it is more than 3, + // immediately return an error. Other validation will be performed later in + // the function + splitComponents := fixSplitComponents(referenceDelimiter.Split(s, -1)) + if len(splitComponents) > 3 { + return nil, errTooManyColons + } + + var ref *Reference + switch len(splitComponents) { + case 1: + ref = &Reference{Repo: splitComponents[0]} + case 2: + ref = &Reference{Repo: splitComponents[0], Tag: splitComponents[1]} + case 3: + ref = &Reference{Repo: strings.Join(splitComponents[:2], ":"), Tag: splitComponents[2]} + } + + // ensure the reference is valid + err := ref.validate() + if err != nil { + return nil, err + } + + return ref, nil +} + +// FullName the full name of a reference (repo:tag) +func (ref *Reference) FullName() string { + if ref.Tag == "" { + return ref.Repo + } + return fmt.Sprintf("%s:%s", ref.Repo, ref.Tag) +} + +// validate makes sure the ref meets our criteria +func (ref *Reference) validate() error { + + err := ref.validateRepo() + if err != nil { + return err + } + return ref.validateNumColons() +} + +// validateRepo checks that the Repo field is non-empty +func (ref *Reference) validateRepo() error { + if ref.Repo == "" { + return errEmptyRepo + } + // Makes sure the repo results in a parsable URL (similar to what is done + // with containerd reference parsing) + _, err := url.Parse("//" + ref.Repo) + return err +} + +// validateNumColons ensures the ref only contains a single colon character (:) +// (or potentially two, there might be a port number specified i.e. :5000) +func (ref *Reference) validateNumColons() error { + if strings.Contains(ref.Tag, ":") { + return errTooManyColons + } + parts := strings.Split(ref.Repo, ":") + lastIndex := len(parts) - 1 + if 1 < lastIndex { + return errTooManyColons + } + if 0 < lastIndex { + port := strings.Split(parts[lastIndex], "/")[0] + if !isValidPort(port) { + return errTooManyColons + } + } + return nil +} + +// isValidPort returns whether or not a string looks like a valid port +func isValidPort(s string) bool { + return validPortRegEx.MatchString(s) +} + +// fixSplitComponents this will modify reference parts based on presence of port +// Example: {localhost, 5000/x/y/z, 0.1.0} => {localhost:5000/x/y/z, 0.1.0} +func fixSplitComponents(c []string) []string { + if len(c) <= 1 { + return c + } + possiblePortParts := strings.Split(c[1], "/") + if _, err := strconv.Atoi(possiblePortParts[0]); err == nil { + components := []string{strings.Join(c[:2], ":")} + components = append(components, c[2:]...) + return components + } + return c +} diff --git a/internal/experimental/registry/reference_test.go b/internal/experimental/registry/reference_test.go new file mode 100644 index 00000000000..aae03ad99d9 --- /dev/null +++ b/internal/experimental/registry/reference_test.go @@ -0,0 +1,126 @@ +/* +Copyright The Helm Authors. + +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 registry + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseReference(t *testing.T) { + is := assert.New(t) + + // bad refs + s := "" + _, err := ParseReference(s) + is.Error(err, "empty ref") + + s = "my:bad:ref" + _, err = ParseReference(s) + is.Error(err, "ref contains too many colons (2)") + + s = "my:really:bad:ref" + _, err = ParseReference(s) + is.Error(err, "ref contains too many colons (3)") + + // good refs + s = "mychart" + ref, err := ParseReference(s) + is.NoError(err) + is.Equal("mychart", ref.Repo) + is.Equal("", ref.Tag) + is.Equal("mychart", ref.FullName()) + + s = "mychart:1.5.0" + ref, err = ParseReference(s) + is.NoError(err) + is.Equal("mychart", ref.Repo) + is.Equal("1.5.0", ref.Tag) + is.Equal("mychart:1.5.0", ref.FullName()) + + s = "myrepo/mychart" + ref, err = ParseReference(s) + is.NoError(err) + is.Equal("myrepo/mychart", ref.Repo) + is.Equal("", ref.Tag) + is.Equal("myrepo/mychart", ref.FullName()) + + s = "myrepo/mychart:1.5.0" + ref, err = ParseReference(s) + is.NoError(err) + is.Equal("myrepo/mychart", ref.Repo) + is.Equal("1.5.0", ref.Tag) + is.Equal("myrepo/mychart:1.5.0", ref.FullName()) + + s = "mychart:5001:1.5.0" + ref, err = ParseReference(s) + is.NoError(err) + is.Equal("mychart:5001", ref.Repo) + is.Equal("1.5.0", ref.Tag) + is.Equal("mychart:5001:1.5.0", ref.FullName()) + + s = "myrepo:5001/mychart:1.5.0" + ref, err = ParseReference(s) + is.NoError(err) + is.Equal("myrepo:5001/mychart", ref.Repo) + is.Equal("1.5.0", ref.Tag) + is.Equal("myrepo:5001/mychart:1.5.0", ref.FullName()) + + s = "127.0.0.1:5001/mychart:1.5.0" + ref, err = ParseReference(s) + is.NoError(err) + is.Equal("127.0.0.1:5001/mychart", ref.Repo) + is.Equal("1.5.0", ref.Tag) + is.Equal("127.0.0.1:5001/mychart:1.5.0", ref.FullName()) + + s = "localhost:5000/mychart:latest" + ref, err = ParseReference(s) + is.NoError(err) + is.Equal("localhost:5000/mychart", ref.Repo) + is.Equal("latest", ref.Tag) + is.Equal("localhost:5000/mychart:latest", ref.FullName()) + + s = "my.host.com/my/nested/repo:1.2.3" + ref, err = ParseReference(s) + is.NoError(err) + is.Equal("my.host.com/my/nested/repo", ref.Repo) + is.Equal("1.2.3", ref.Tag) + is.Equal("my.host.com/my/nested/repo:1.2.3", ref.FullName()) + + s = "localhost:5000/x/y/z" + ref, err = ParseReference(s) + is.NoError(err) + is.Equal("localhost:5000/x/y/z", ref.Repo) + is.Equal("", ref.Tag) + is.Equal("localhost:5000/x/y/z", ref.FullName()) + + s = "localhost:5000/x/y/z:123" + ref, err = ParseReference(s) + is.NoError(err) + is.Equal("localhost:5000/x/y/z", ref.Repo) + is.Equal("123", ref.Tag) + is.Equal("localhost:5000/x/y/z:123", ref.FullName()) + + s = "localhost:5000/x/y/z:123:x" + _, err = ParseReference(s) + is.Error(err, "ref contains too many colons (3)") + + s = "localhost:5000/x/y/z:123:x:y" + _, err = ParseReference(s) + is.Error(err, "ref contains too many colons (4)") +} diff --git a/internal/experimental/registry/resolver.go b/internal/experimental/registry/resolver.go new file mode 100644 index 00000000000..ff8a82633bd --- /dev/null +++ b/internal/experimental/registry/resolver.go @@ -0,0 +1,28 @@ +/* +Copyright The Helm Authors. + +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 registry // import "helm.sh/helm/v3/internal/experimental/registry" + +import ( + "github.com/containerd/containerd/remotes" +) + +type ( + // Resolver provides remotes based on a locator + Resolver struct { + remotes.Resolver + } +) diff --git a/internal/experimental/registry/util.go b/internal/experimental/registry/util.go new file mode 100644 index 00000000000..697a890e37e --- /dev/null +++ b/internal/experimental/registry/util.go @@ -0,0 +1,66 @@ +/* +Copyright The Helm Authors. + +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 registry // import "helm.sh/helm/v3/internal/experimental/registry" + +import ( + "context" + "fmt" + "io" + "time" + + orascontext "github.com/deislabs/oras/pkg/context" + units "github.com/docker/go-units" + "github.com/sirupsen/logrus" +) + +// byteCountBinary produces a human-readable file size +func byteCountBinary(b int64) string { + const unit = 1024 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp]) +} + +// shortDigest returns first 7 characters of a sha256 digest +func shortDigest(digest string) string { + if len(digest) == 64 { + return digest[:7] + } + return digest +} + +// timeAgo returns a human-readable timestamp representing time that has passed +func timeAgo(t time.Time) string { + return units.HumanDuration(time.Now().UTC().Sub(t)) +} + +// ctx retrieves a fresh context. +// disable verbose logging coming from ORAS (unless debug is enabled) +func ctx(out io.Writer, debug bool) context.Context { + if !debug { + return orascontext.Background() + } + ctx := orascontext.WithLoggerFromWriter(context.Background(), out) + orascontext.GetLogger(ctx).Logger.SetLevel(logrus.DebugLevel) + return ctx +} diff --git a/internal/fileutil/fileutil.go b/internal/fileutil/fileutil.go new file mode 100644 index 00000000000..739093f3b3d --- /dev/null +++ b/internal/fileutil/fileutil.go @@ -0,0 +1,51 @@ +/* +Copyright The Helm Authors. + +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 fileutil + +import ( + "io" + "io/ioutil" + "os" + "path/filepath" + + "helm.sh/helm/v3/internal/third_party/dep/fs" +) + +// AtomicWriteFile atomically (as atomic as os.Rename allows) writes a file to a +// disk. +func AtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error { + tempFile, err := ioutil.TempFile(filepath.Split(filename)) + if err != nil { + return err + } + tempName := tempFile.Name() + + if _, err := io.Copy(tempFile, reader); err != nil { + tempFile.Close() // return value is ignored as we are already on error path + return err + } + + if err := tempFile.Close(); err != nil { + return err + } + + if err := os.Chmod(tempName, mode); err != nil { + return err + } + + return fs.RenameWithFallback(tempName, filename) +} diff --git a/internal/fileutil/fileutil_test.go b/internal/fileutil/fileutil_test.go new file mode 100644 index 00000000000..9a4bc32c964 --- /dev/null +++ b/internal/fileutil/fileutil_test.go @@ -0,0 +1,62 @@ +/* +Copyright The Helm Authors. + +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 fileutil + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestAtomicWriteFile(t *testing.T) { + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + testpath := filepath.Join(dir, "test") + stringContent := "Test content" + reader := bytes.NewReader([]byte(stringContent)) + mode := os.FileMode(0644) + + err = AtomicWriteFile(testpath, reader, mode) + if err != nil { + t.Errorf("AtomicWriteFile error: %s", err) + } + + got, err := ioutil.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + + if stringContent != string(got) { + t.Fatalf("expected: %s, got: %s", stringContent, string(got)) + } + + gotinfo, err := os.Stat(testpath) + if err != nil { + t.Fatal(err) + } + + if mode != gotinfo.Mode() { + t.Fatalf("expected %s: to be the same mode as %s", + mode, gotinfo.Mode()) + } +} diff --git a/internal/ignore/doc.go b/internal/ignore/doc.go new file mode 100644 index 00000000000..e6a6a6c7b56 --- /dev/null +++ b/internal/ignore/doc.go @@ -0,0 +1,67 @@ +/* +Copyright The Helm Authors. + +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 ignore provides tools for writing ignore files (a la .gitignore). + +This provides both an ignore parser and a file-aware processor. + +The format of ignore files closely follows, but does not exactly match, the +format for .gitignore files (https://git-scm.com/docs/gitignore). + +The formatting rules are as follows: + + - Parsing is line-by-line + - Empty lines are ignored + - Lines the begin with # (comments) will be ignored + - Leading and trailing spaces are always ignored + - Inline comments are NOT supported ('foo* # Any foo' does not contain a comment) + - There is no support for multi-line patterns + - Shell glob patterns are supported. See Go's "path/filepath".Match + - If a pattern begins with a leading !, the match will be negated. + - If a pattern begins with a leading /, only paths relatively rooted will match. + - If the pattern ends with a trailing /, only directories will match + - If a pattern contains no slashes, file basenames are tested (not paths) + - The pattern sequence "**", while legal in a glob, will cause an error here + (to indicate incompatibility with .gitignore). + +Example: + + # Match any file named foo.txt + foo.txt + + # Match any text file + *.txt + + # Match only directories named mydir + mydir/ + + # Match only text files in the top-level directory + /*.txt + + # Match only the file foo.txt in the top-level directory + /foo.txt + + # Match any file named ab.txt, ac.txt, or ad.txt + a[b-d].txt + +Notable differences from .gitignore: + - The '**' syntax is not supported. + - The globbing library is Go's 'filepath.Match', not fnmatch(3) + - Trailing spaces are always ignored (there is no supported escape sequence) + - The evaluation of escape sequences has not been tested for compatibility + - There is no support for '\!' as a special leading sequence. +*/ +package ignore // import "helm.sh/helm/v3/internal/ignore" diff --git a/pkg/ignore/rules.go b/internal/ignore/rules.go similarity index 92% rename from pkg/ignore/rules.go rename to internal/ignore/rules.go index 76f45fc7a2f..a80923baf0c 100644 --- a/pkg/ignore/rules.go +++ b/internal/ignore/rules.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,12 +18,14 @@ package ignore import ( "bufio" - "errors" + "bytes" "io" "log" "os" "path/filepath" "strings" + + "github.com/pkg/errors" ) // HelmIgnore default name of an ignorefile. @@ -64,20 +66,25 @@ func Parse(file io.Reader) (*Rules, error) { r := &Rules{patterns: []*pattern{}} s := bufio.NewScanner(file) + currentLine := 0 + utf8bom := []byte{0xEF, 0xBB, 0xBF} for s.Scan() { - if err := r.parseRule(s.Text()); err != nil { + scannedBytes := s.Bytes() + // We trim UTF8 BOM + if currentLine == 0 { + scannedBytes = bytes.TrimPrefix(scannedBytes, utf8bom) + } + line := string(scannedBytes) + currentLine++ + + if err := r.parseRule(line); err != nil { return r, err } } return r, s.Err() } -// Len returns the number of patterns in this rule set. -func (r *Rules) Len() int { - return len(r.patterns) -} - -// Ignore evalutes the file at the given path, and returns true if it should be ignored. +// Ignore evaluates the file at the given path, and returns true if it should be ignored. // // Ignore evaluates path against the rules in order. Evaluation stops when a match // is found. Matching a negative rule will stop evaluation. diff --git a/pkg/ignore/rules_test.go b/internal/ignore/rules_test.go similarity index 96% rename from pkg/ignore/rules_test.go rename to internal/ignore/rules_test.go index 17b8bf403ab..9581cf09fa2 100644 --- a/pkg/ignore/rules_test.go +++ b/internal/ignore/rules_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -99,7 +99,7 @@ func TestIgnore(t *testing.T) { {`cargo/*.txt`, "mast/a.txt", false}, {`ru[c-e]?er.txt`, "rudder.txt", true}, {`templates/.?*`, "templates/.dotfile", true}, - // "." should never get ignored. https://github.com/kubernetes/helm/issues/1776 + // "." should never get ignored. https://github.com/helm/helm/issues/1776 {`.*`, ".", false}, {`.*`, "./", false}, {`.*`, ".joonix", true}, diff --git a/pkg/ignore/testdata/.helmignore b/internal/ignore/testdata/.helmignore similarity index 100% rename from pkg/ignore/testdata/.helmignore rename to internal/ignore/testdata/.helmignore diff --git a/pkg/ignore/testdata/cargo/c.txt b/internal/ignore/testdata/.joonix similarity index 100% rename from pkg/ignore/testdata/cargo/c.txt rename to internal/ignore/testdata/.joonix diff --git a/pkg/ignore/testdata/mast/a.txt b/internal/ignore/testdata/a.txt similarity index 100% rename from pkg/ignore/testdata/mast/a.txt rename to internal/ignore/testdata/a.txt diff --git a/pkg/ignore/testdata/helm.txt b/internal/ignore/testdata/cargo/a.txt similarity index 100% rename from pkg/ignore/testdata/helm.txt rename to internal/ignore/testdata/cargo/a.txt diff --git a/pkg/ignore/testdata/mast/b.txt b/internal/ignore/testdata/cargo/b.txt similarity index 100% rename from pkg/ignore/testdata/mast/b.txt rename to internal/ignore/testdata/cargo/b.txt diff --git a/pkg/ignore/testdata/mast/c.txt b/internal/ignore/testdata/cargo/c.txt similarity index 100% rename from pkg/ignore/testdata/mast/c.txt rename to internal/ignore/testdata/cargo/c.txt diff --git a/pkg/ignore/testdata/rudder.txt b/internal/ignore/testdata/helm.txt similarity index 100% rename from pkg/ignore/testdata/rudder.txt rename to internal/ignore/testdata/helm.txt diff --git a/pkg/ignore/testdata/templates/.dotfile b/internal/ignore/testdata/mast/a.txt similarity index 100% rename from pkg/ignore/testdata/templates/.dotfile rename to internal/ignore/testdata/mast/a.txt diff --git a/pkg/ignore/testdata/tiller.txt b/internal/ignore/testdata/mast/b.txt similarity index 100% rename from pkg/ignore/testdata/tiller.txt rename to internal/ignore/testdata/mast/b.txt diff --git a/internal/ignore/testdata/mast/c.txt b/internal/ignore/testdata/mast/c.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/ignore/testdata/rudder.txt b/internal/ignore/testdata/rudder.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/ignore/testdata/templates/.dotfile b/internal/ignore/testdata/templates/.dotfile new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/ignore/testdata/tiller.txt b/internal/ignore/testdata/tiller.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/monocular/client.go b/internal/monocular/client.go new file mode 100644 index 00000000000..88a2564b941 --- /dev/null +++ b/internal/monocular/client.go @@ -0,0 +1,68 @@ +/* +Copyright The Helm Authors. + +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 monocular + +import ( + "errors" + "net/url" +) + +// ErrHostnameNotProvided indicates the url is missing a hostname +var ErrHostnameNotProvided = errors.New("no hostname provided") + +// Client represents a client capable of communicating with the Monocular API. +type Client struct { + + // The base URL for requests + BaseURL string + + // The internal logger to use + Log func(string, ...interface{}) +} + +// New creates a new client +func New(u string) (*Client, error) { + + // Validate we have a URL + if err := validate(u); err != nil { + return nil, err + } + + return &Client{ + BaseURL: u, + Log: nopLogger, + }, nil +} + +var nopLogger = func(_ string, _ ...interface{}) {} + +// Validate if the base URL for monocular is valid. +func validate(u string) error { + + // Check if it is parsable + p, err := url.Parse(u) + if err != nil { + return err + } + + // Check that a host is attached + if p.Hostname() == "" { + return ErrHostnameNotProvided + } + + return nil +} diff --git a/internal/monocular/client_test.go b/internal/monocular/client_test.go new file mode 100644 index 00000000000..abf914ef5c3 --- /dev/null +++ b/internal/monocular/client_test.go @@ -0,0 +1,31 @@ +/* +Copyright The Helm Authors. + +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 monocular + +import ( + "testing" +) + +func TestNew(t *testing.T) { + c, err := New("https://hub.helm.sh") + if err != nil { + t.Errorf("error creating client: %s", err) + } + if c.BaseURL != "https://hub.helm.sh" { + t.Errorf("incorrect BaseURL. Expected \"https://hub.helm.sh\" but got %q", c.BaseURL) + } +} diff --git a/internal/monocular/doc.go b/internal/monocular/doc.go new file mode 100644 index 00000000000..5d402d35fd6 --- /dev/null +++ b/internal/monocular/doc.go @@ -0,0 +1,22 @@ +/* +Copyright The Helm Authors. + +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 monocular contains the logic for interacting with a Monocular +// compatible search API endpoint. For example, as implemented by the Artifact +// Hub. +// +// This is a library for interacting with a monocular compatible search API +package monocular diff --git a/internal/monocular/search.go b/internal/monocular/search.go new file mode 100644 index 00000000000..3082ff361e6 --- /dev/null +++ b/internal/monocular/search.go @@ -0,0 +1,145 @@ +/* +Copyright The Helm Authors. + +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 monocular + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "time" + + "helm.sh/helm/v3/internal/version" + "helm.sh/helm/v3/pkg/chart" +) + +// SearchPath is the url path to the search API in monocular. +const SearchPath = "api/chartsvc/v1/charts/search" + +// The structs below represent the structure of the response from the monocular +// search API. The structs were not imported from monocular because monocular +// imports from Helm v2 (avoiding circular version dependency) and the mappings +// are slightly different (monocular search results do not directly reflect +// the struct definitions). + +// SearchResult represents an individual chart result +type SearchResult struct { + ID string `json:"id"` + ArtifactHub ArtifactHub `json:"artifactHub"` + Type string `json:"type"` + Attributes Chart `json:"attributes"` + Links Links `json:"links"` + Relationships Relationships `json:"relationships"` +} + +// ArtifactHub represents data specific to Artifact Hub instances +type ArtifactHub struct { + PackageURL string `json:"packageUrl"` +} + +// Chart is the attributes for the chart +type Chart struct { + Name string `json:"name"` + Repo Repo `json:"repo"` + Description string `json:"description"` + Home string `json:"home"` + Keywords []string `json:"keywords"` + Maintainers []chart.Maintainer `json:"maintainers"` + Sources []string `json:"sources"` + Icon string `json:"icon"` +} + +// Repo contains the name in monocular the url for the repository +type Repo struct { + Name string `json:"name"` + URL string `json:"url"` +} + +// Links provides a set of links relative to the chartsvc base +type Links struct { + Self string `json:"self"` +} + +// Relationships provides information on the latest version of the chart +type Relationships struct { + LatestChartVersion LatestChartVersion `json:"latestChartVersion"` +} + +// LatestChartVersion provides the details on the latest version of the chart +type LatestChartVersion struct { + Data ChartVersion `json:"data"` + Links Links `json:"links"` +} + +// ChartVersion provides the specific data on the chart version +type ChartVersion struct { + Version string `json:"version"` + AppVersion string `json:"app_version"` + Created time.Time `json:"created"` + Digest string `json:"digest"` + Urls []string `json:"urls"` + Readme string `json:"readme"` + Values string `json:"values"` +} + +// Search performs a search against the monocular search API +func (c *Client) Search(term string) ([]SearchResult, error) { + + // Create the URL to the search endpoint + // Note, this is currently an internal API for the Hub. This should be + // formatted without showing how monocular operates. + p, err := url.Parse(c.BaseURL) + if err != nil { + return nil, err + } + + // Set the path to the monocular API endpoint for search + p.Path = path.Join(p.Path, SearchPath) + + p.RawQuery = "q=" + url.QueryEscape(term) + + // Create request + req, err := http.NewRequest("GET", p.String(), nil) + if err != nil { + return nil, err + } + + // Set the user agent so that monocular can identify where the request + // is coming from + req.Header.Set("User-Agent", version.GetUserAgent()) + + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + return nil, fmt.Errorf("failed to fetch %s : %s", p.String(), res.Status) + } + + result := &searchResponse{} + + json.NewDecoder(res.Body).Decode(result) + + return result.Data, nil +} + +type searchResponse struct { + Data []SearchResult `json:"data"` +} diff --git a/internal/monocular/search_test.go b/internal/monocular/search_test.go new file mode 100644 index 00000000000..9f6954af7de --- /dev/null +++ b/internal/monocular/search_test.go @@ -0,0 +1,49 @@ +/* +Copyright The Helm Authors. + +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 monocular + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +// A search response for phpmyadmin containing 2 results +var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}` + +func TestSearch(t *testing.T) { + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, searchResult) + })) + defer ts.Close() + + c, err := New(ts.URL) + if err != nil { + t.Errorf("unable to create monocular client: %s", err) + } + + results, err := c.Search("phpmyadmin") + if err != nil { + t.Errorf("unable to search monocular: %s", err) + } + + if len(results) != 2 { + t.Error("Did not receive the expected number of results") + } +} diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go new file mode 100644 index 00000000000..de06340931f --- /dev/null +++ b/internal/resolver/resolver.go @@ -0,0 +1,225 @@ +/* +Copyright The Helm Authors. +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 resolver + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/gates" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/provenance" + "helm.sh/helm/v3/pkg/repo" +) + +const FeatureGateOCI = gates.Gate("HELM_EXPERIMENTAL_OCI") + +// Resolver resolves dependencies from semantic version ranges to a particular version. +type Resolver struct { + chartpath string + cachepath string +} + +// New creates a new resolver for a given chart and a given helm home. +func New(chartpath, cachepath string) *Resolver { + return &Resolver{ + chartpath: chartpath, + cachepath: cachepath, + } +} + +// Resolve resolves dependencies and returns a lock file with the resolution. +func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) { + + // Now we clone the dependencies, locking as we go. + locked := make([]*chart.Dependency, len(reqs)) + missing := []string{} + for i, d := range reqs { + if d.Repository == "" { + // Local chart subfolder + if _, err := GetLocalPath(filepath.Join("charts", d.Name), r.chartpath); err != nil { + return nil, err + } + + locked[i] = &chart.Dependency{ + Name: d.Name, + Repository: "", + Version: d.Version, + } + continue + } + if strings.HasPrefix(d.Repository, "file://") { + + chartpath, err := GetLocalPath(d.Repository, r.chartpath) + if err != nil { + return nil, err + } + + // The version of the chart locked will be the version of the chart + // currently listed in the file system within the chart. + ch, err := loader.LoadDir(chartpath) + if err != nil { + return nil, err + } + + locked[i] = &chart.Dependency{ + Name: d.Name, + Repository: d.Repository, + Version: ch.Metadata.Version, + } + continue + } + + constraint, err := semver.NewConstraint(d.Version) + if err != nil { + return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name) + } + + repoName := repoNames[d.Name] + // if the repository was not defined, but the dependency defines a repository url, bypass the cache + if repoName == "" && d.Repository != "" { + locked[i] = &chart.Dependency{ + Name: d.Name, + Repository: d.Repository, + Version: d.Version, + } + continue + } + + var vs repo.ChartVersions + var version string + var ok bool + found := true + if !strings.HasPrefix(d.Repository, "oci://") { + repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName))) + if err != nil { + return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName) + } + + vs, ok = repoIndex.Entries[d.Name] + if !ok { + return nil, errors.Errorf("%s chart not found in repo %s", d.Name, d.Repository) + } + found = false + } else { + version = d.Version + if !FeatureGateOCI.IsEnabled() { + return nil, errors.Wrapf(FeatureGateOCI.Error(), + "repository %s is an OCI registry", d.Repository) + } + } + + locked[i] = &chart.Dependency{ + Name: d.Name, + Repository: d.Repository, + Version: version, + } + // The version are already sorted and hence the first one to satisfy the constraint is used + for _, ver := range vs { + v, err := semver.NewVersion(ver.Version) + if err != nil || len(ver.URLs) == 0 { + // Not a legit entry. + continue + } + if constraint.Check(v) { + found = true + locked[i].Version = v.Original() + break + } + } + + if !found { + missing = append(missing, d.Name) + } + } + if len(missing) > 0 { + return nil, errors.Errorf("can't get a valid version for repositories %s. Try changing the version constraint in Chart.yaml", strings.Join(missing, ", ")) + } + + digest, err := HashReq(reqs, locked) + if err != nil { + return nil, err + } + + return &chart.Lock{ + Generated: time.Now(), + Digest: digest, + Dependencies: locked, + }, nil +} + +// HashReq generates a hash of the dependencies. +// +// This should be used only to compare against another hash generated by this +// function. +func HashReq(req, lock []*chart.Dependency) (string, error) { + data, err := json.Marshal([2][]*chart.Dependency{req, lock}) + if err != nil { + return "", err + } + s, err := provenance.Digest(bytes.NewBuffer(data)) + return "sha256:" + s, err +} + +// HashV2Req generates a hash of requirements generated in Helm v2. +// +// This should be used only to compare against another hash generated by the +// Helm v2 hash function. It is to handle issue: +// https://github.com/helm/helm/issues/7233 +func HashV2Req(req []*chart.Dependency) (string, error) { + dep := make(map[string][]*chart.Dependency) + dep["dependencies"] = req + data, err := json.Marshal(dep) + if err != nil { + return "", err + } + s, err := provenance.Digest(bytes.NewBuffer(data)) + return "sha256:" + s, err +} + +// GetLocalPath generates absolute local path when use +// "file://" in repository of dependencies +func GetLocalPath(repo, chartpath string) (string, error) { + var depPath string + var err error + p := strings.TrimPrefix(repo, "file://") + + // root path is absolute + if strings.HasPrefix(p, "/") { + if depPath, err = filepath.Abs(p); err != nil { + return "", err + } + } else { + depPath = filepath.Join(chartpath, p) + } + + if _, err = os.Stat(depPath); os.IsNotExist(err) { + return "", errors.Errorf("directory %s not found", depPath) + } else if err != nil { + return "", err + } + + return depPath, nil +} diff --git a/internal/resolver/resolver_test.go b/internal/resolver/resolver_test.go new file mode 100644 index 00000000000..204a72622ee --- /dev/null +++ b/internal/resolver/resolver_test.go @@ -0,0 +1,287 @@ +/* +Copyright The Helm Authors. +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 resolver + +import ( + "testing" + + "helm.sh/helm/v3/pkg/chart" +) + +func TestResolve(t *testing.T) { + tests := []struct { + name string + req []*chart.Dependency + expect *chart.Lock + err bool + }{ + { + name: "version failure", + req: []*chart.Dependency{ + {Name: "oedipus-rex", Repository: "http://example.com", Version: ">a1"}, + }, + err: true, + }, + { + name: "cache index failure", + req: []*chart.Dependency{ + {Name: "oedipus-rex", Repository: "http://example.com", Version: "1.0.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "oedipus-rex", Repository: "http://example.com", Version: "1.0.0"}, + }, + }, + }, + { + name: "chart not found failure", + req: []*chart.Dependency{ + {Name: "redis", Repository: "http://example.com", Version: "1.0.0"}, + }, + err: true, + }, + { + name: "constraint not satisfied failure", + req: []*chart.Dependency{ + {Name: "alpine", Repository: "http://example.com", Version: ">=1.0.0"}, + }, + err: true, + }, + { + name: "valid lock", + req: []*chart.Dependency{ + {Name: "alpine", Repository: "http://example.com", Version: ">=0.1.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "alpine", Repository: "http://example.com", Version: "0.2.0"}, + }, + }, + }, + { + name: "repo from valid local path", + req: []*chart.Dependency{ + {Name: "base", Repository: "file://base", Version: "0.1.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "base", Repository: "file://base", Version: "0.1.0"}, + }, + }, + }, + { + name: "repo from valid local path with range resolution", + req: []*chart.Dependency{ + {Name: "base", Repository: "file://base", Version: "^0.1.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "base", Repository: "file://base", Version: "0.1.0"}, + }, + }, + }, + { + name: "repo from invalid local path", + req: []*chart.Dependency{ + {Name: "nonexistent", Repository: "file://testdata/nonexistent", Version: "0.1.0"}, + }, + err: true, + }, + { + name: "repo from valid path under charts path", + req: []*chart.Dependency{ + {Name: "localdependency", Repository: "", Version: "0.1.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "localdependency", Repository: "", Version: "0.1.0"}, + }, + }, + }, + { + name: "repo from invalid path under charts path", + req: []*chart.Dependency{ + {Name: "nonexistentdependency", Repository: "", Version: "0.1.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "nonexistentlocaldependency", Repository: "", Version: "0.1.0"}, + }, + }, + err: true, + }, + } + + repoNames := map[string]string{"alpine": "kubernetes-charts", "redis": "kubernetes-charts"} + r := New("testdata/chartpath", "testdata/repository") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l, err := r.Resolve(tt.req, repoNames) + if err != nil { + if tt.err { + return + } + t.Fatal(err) + } + + if tt.err { + t.Fatalf("Expected error in test %q", tt.name) + } + + if h, err := HashReq(tt.req, tt.expect.Dependencies); err != nil { + t.Fatal(err) + } else if h != l.Digest { + t.Errorf("%q: hashes don't match.", tt.name) + } + + // Check fields. + if len(l.Dependencies) != len(tt.req) { + t.Errorf("%s: wrong number of dependencies in lock", tt.name) + } + d0 := l.Dependencies[0] + e0 := tt.expect.Dependencies[0] + if d0.Name != e0.Name { + t.Errorf("%s: expected name %s, got %s", tt.name, e0.Name, d0.Name) + } + if d0.Repository != e0.Repository { + t.Errorf("%s: expected repo %s, got %s", tt.name, e0.Repository, d0.Repository) + } + if d0.Version != e0.Version { + t.Errorf("%s: expected version %s, got %s", tt.name, e0.Version, d0.Version) + } + }) + } +} + +func TestHashReq(t *testing.T) { + expect := "sha256:fb239e836325c5fa14b29d1540a13b7d3ba13151b67fe719f820e0ef6d66aaaf" + + tests := []struct { + name string + chartVersion string + lockVersion string + wantError bool + }{ + { + name: "chart with the expected digest", + chartVersion: "0.1.0", + lockVersion: "0.1.0", + wantError: false, + }, + { + name: "ranged version but same resolved lock version", + chartVersion: "^0.1.0", + lockVersion: "0.1.0", + wantError: true, + }, + { + name: "ranged version resolved as higher version", + chartVersion: "^0.1.0", + lockVersion: "0.1.2", + wantError: true, + }, + { + name: "different version", + chartVersion: "0.1.2", + lockVersion: "0.1.2", + wantError: true, + }, + { + name: "different version with a range", + chartVersion: "^0.1.2", + lockVersion: "0.1.2", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := []*chart.Dependency{ + {Name: "alpine", Version: tt.chartVersion, Repository: "http://localhost:8879/charts"}, + } + lock := []*chart.Dependency{ + {Name: "alpine", Version: tt.lockVersion, Repository: "http://localhost:8879/charts"}, + } + h, err := HashReq(req, lock) + if err != nil { + t.Fatal(err) + } + if !tt.wantError && expect != h { + t.Errorf("Expected %q, got %q", expect, h) + } else if tt.wantError && expect == h { + t.Errorf("Expected not %q, but same", expect) + } + }) + } +} + +func TestGetLocalPath(t *testing.T) { + tests := []struct { + name string + repo string + chartpath string + expect string + err bool + }{ + { + name: "absolute path", + repo: "file:////tmp", + expect: "/tmp", + }, + { + name: "relative path", + repo: "file://../../testdata/chartpath/base", + chartpath: "foo/bar", + expect: "testdata/chartpath/base", + }, + { + name: "current directory path", + repo: "../charts/localdependency", + chartpath: "testdata/chartpath/charts", + expect: "testdata/chartpath/charts/localdependency", + }, + { + name: "invalid local path", + repo: "file://testdata/nonexistent", + chartpath: "testdata/chartpath", + err: true, + }, + { + name: "invalid path under current directory", + repo: "charts/nonexistentdependency", + chartpath: "testdata/chartpath/charts", + err: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p, err := GetLocalPath(tt.repo, tt.chartpath) + if err != nil { + if tt.err { + return + } + t.Fatal(err) + } + if tt.err { + t.Fatalf("Expected error in test %q", tt.name) + } + if p != tt.expect { + t.Errorf("%q: expected %q, got %q", tt.name, tt.expect, p) + } + }) + } +} diff --git a/internal/resolver/testdata/chartpath/base/Chart.yaml b/internal/resolver/testdata/chartpath/base/Chart.yaml new file mode 100644 index 00000000000..860b0909134 --- /dev/null +++ b/internal/resolver/testdata/chartpath/base/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: base +version: 0.1.0 diff --git a/internal/resolver/testdata/chartpath/charts/localdependency/Chart.yaml b/internal/resolver/testdata/chartpath/charts/localdependency/Chart.yaml new file mode 100644 index 00000000000..083c51ee535 --- /dev/null +++ b/internal/resolver/testdata/chartpath/charts/localdependency/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: localdependency +version: 0.1.0 diff --git a/internal/resolver/testdata/repository/kubernetes-charts-index.yaml b/internal/resolver/testdata/repository/kubernetes-charts-index.yaml new file mode 100644 index 00000000000..c6b7962a1fb --- /dev/null +++ b/internal/resolver/testdata/repository/kubernetes-charts-index.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +entries: + alpine: + - name: alpine + urls: + - https://charts.helm.sh/stable/alpine-0.1.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://helm.sh/helm + sources: + - https://github.com/helm/helm + version: 0.2.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + icon: "" + apiVersion: v2 + - name: alpine + urls: + - https://charts.helm.sh/stable/alpine-0.2.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://helm.sh/helm + sources: + - https://github.com/helm/helm + version: 0.1.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + icon: "" + apiVersion: v2 + mariadb: + - name: mariadb + urls: + - https://charts.helm.sh/stable/mariadb-0.3.0.tgz + checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 + home: https://mariadb.org + sources: + - https://github.com/bitnami/bitnami-docker-mariadb + version: 0.3.0 + description: Chart for MariaDB + keywords: + - mariadb + - mysql + - database + - sql + maintainers: + - name: Bitnami + email: containers@bitnami.com + icon: "" + apiVersion: v2 diff --git a/pkg/sympath/walk.go b/internal/sympath/walk.go similarity index 89% rename from pkg/sympath/walk.go rename to internal/sympath/walk.go index 77fa04153f6..752526fe939 100644 --- a/pkg/sympath/walk.go +++ b/internal/sympath/walk.go @@ -1,10 +1,10 @@ /* -Copyright (c) for portions of walk.go are held by The Go Authors, 2009 and are provided under -the BSD license. +Copyright (c) for portions of walk.go are held by The Go Authors, 2009 and are +provided under the BSD license. https://github.com/golang/go/blob/master/LICENSE -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -21,10 +21,12 @@ limitations under the License. package sympath import ( - "fmt" + "log" "os" "path/filepath" "sort" + + "github.com/pkg/errors" ) // Walk walks the file tree rooted at root, calling walkFn for each file or directory @@ -67,14 +69,16 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error { if IsSymlink(info) { resolved, err := filepath.EvalSymlinks(path) if err != nil { - return fmt.Errorf("error evaluating symlink %s: %s", path, err) + return errors.Wrapf(err, "error evaluating symlink %s", path) } + log.Printf("found symbolic link in path: %s resolves to %s", path, resolved) if info, err = os.Lstat(resolved); err != nil { return err } - if err := symwalk(resolved, info, walkFn); err != nil && err != filepath.SkipDir { + if err := symwalk(path, info, walkFn); err != nil && err != filepath.SkipDir { return err } + return nil } if err := walkFn(path, info, nil); err != nil { diff --git a/internal/sympath/walk_test.go b/internal/sympath/walk_test.go new file mode 100644 index 00000000000..25f737134a2 --- /dev/null +++ b/internal/sympath/walk_test.go @@ -0,0 +1,151 @@ +/* +Copyright (c) for portions of walk_test.go are held by The Go Authors, 2009 and are +provided under the BSD license. + +https://github.com/golang/go/blob/master/LICENSE + +Copyright The Helm Authors. +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 sympath + +import ( + "os" + "path/filepath" + "testing" +) + +type Node struct { + name string + entries []*Node // nil if the entry is a file + marks int + expectedMarks int + symLinkedTo string +} + +var tree = &Node{ + "testdata", + []*Node{ + {"a", nil, 0, 1, ""}, + {"b", []*Node{}, 0, 1, ""}, + {"c", nil, 0, 2, ""}, + {"d", nil, 0, 0, "c"}, + { + "e", + []*Node{ + {"x", nil, 0, 1, ""}, + {"y", []*Node{}, 0, 1, ""}, + { + "z", + []*Node{ + {"u", nil, 0, 1, ""}, + {"v", nil, 0, 1, ""}, + {"w", nil, 0, 1, ""}, + }, + 0, + 1, + "", + }, + }, + 0, + 1, + "", + }, + }, + 0, + 1, + "", +} + +func walkTree(n *Node, path string, f func(path string, n *Node)) { + f(path, n) + for _, e := range n.entries { + walkTree(e, filepath.Join(path, e.name), f) + } +} + +func makeTree(t *testing.T) { + walkTree(tree, tree.name, func(path string, n *Node) { + if n.entries == nil { + if n.symLinkedTo != "" { + if err := os.Symlink(n.symLinkedTo, path); err != nil { + t.Fatalf("makeTree: %v", err) + } + } else { + fd, err := os.Create(path) + if err != nil { + t.Fatalf("makeTree: %v", err) + return + } + fd.Close() + } + } else { + if err := os.Mkdir(path, 0770); err != nil { + t.Fatalf("makeTree: %v", err) + } + } + }) +} + +func checkMarks(t *testing.T, report bool) { + walkTree(tree, tree.name, func(path string, n *Node) { + if n.marks != n.expectedMarks && report { + t.Errorf("node %s mark = %d; expected %d", path, n.marks, n.expectedMarks) + } + n.marks = 0 + }) +} + +// Assumes that each node name is unique. Good enough for a test. +// If clear is true, any incoming error is cleared before return. The errors +// are always accumulated, though. +func mark(info os.FileInfo, err error, errors *[]error, clear bool) error { + if err != nil { + *errors = append(*errors, err) + if clear { + return nil + } + return err + } + name := info.Name() + walkTree(tree, tree.name, func(path string, n *Node) { + if n.name == name { + n.marks++ + } + }) + return nil +} + +func TestWalk(t *testing.T) { + makeTree(t) + errors := make([]error, 0, 10) + clear := true + markFn := func(path string, info os.FileInfo, err error) error { + return mark(info, err, &errors, clear) + } + // Expect no errors. + err := Walk(tree.name, markFn) + if err != nil { + t.Fatalf("no error expected, found: %s", err) + } + if len(errors) != 0 { + t.Fatalf("unexpected errors: %s", errors) + } + checkMarks(t, true) + + // cleanup + if err := os.RemoveAll(tree.name); err != nil { + t.Errorf("removeTree: %v", err) + } +} diff --git a/internal/test/ensure/ensure.go b/internal/test/ensure/ensure.go new file mode 100644 index 00000000000..3c0e4575cb4 --- /dev/null +++ b/internal/test/ensure/ensure.go @@ -0,0 +1,70 @@ +/* +Copyright The Helm Authors. + +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 ensure + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/helmpath/xdg" +) + +// HelmHome sets up a Helm Home in a temp dir. +func HelmHome(t *testing.T) func() { + t.Helper() + base := TempDir(t) + os.Setenv(xdg.CacheHomeEnvVar, base) + os.Setenv(xdg.ConfigHomeEnvVar, base) + os.Setenv(xdg.DataHomeEnvVar, base) + os.Setenv(helmpath.CacheHomeEnvVar, "") + os.Setenv(helmpath.ConfigHomeEnvVar, "") + os.Setenv(helmpath.DataHomeEnvVar, "") + return func() { + os.RemoveAll(base) + } +} + +// TempDir ensures a scratch test directory for unit testing purposes. +func TempDir(t *testing.T) string { + t.Helper() + d, err := ioutil.TempDir("", "helm") + if err != nil { + t.Fatal(err) + } + return d +} + +// TempFile ensures a temp file for unit testing purposes. +// +// It returns the path to the directory (to which you will still need to join the filename) +// +// You must clean up the directory that is returned. +// +// tempdir := TempFile(t, "foo", []byte("bar")) +// defer os.RemoveAll(tempdir) +// filename := filepath.Join(tempdir, "foo") +func TempFile(t *testing.T, name string, data []byte) string { + path := TempDir(t) + filename := filepath.Join(path, name) + if err := ioutil.WriteFile(filename, data, 0755); err != nil { + t.Fatal(err) + } + return path +} diff --git a/internal/test/test.go b/internal/test/test.go new file mode 100644 index 00000000000..646037606d1 --- /dev/null +++ b/internal/test/test.go @@ -0,0 +1,105 @@ +/* +Copyright The Helm Authors. + +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 test + +import ( + "bytes" + "flag" + "io/ioutil" + "path/filepath" + + "github.com/pkg/errors" +) + +// UpdateGolden writes out the golden files with the latest values, rather than failing the test. +var updateGolden = flag.Bool("update", false, "update golden files") + +// TestingT describes a testing object compatible with the critical functions from the testing.T type +type TestingT interface { + Fatal(...interface{}) + Fatalf(string, ...interface{}) + HelperT +} + +// HelperT describes a test with a helper function +type HelperT interface { + Helper() +} + +// AssertGoldenBytes asserts that the give actual content matches the contents of the given filename +func AssertGoldenBytes(t TestingT, actual []byte, filename string) { + t.Helper() + + if err := compare(actual, path(filename)); err != nil { + t.Fatalf("%v", err) + } +} + +// AssertGoldenString asserts that the given string matches the contents of the given file. +func AssertGoldenString(t TestingT, actual, filename string) { + t.Helper() + + if err := compare([]byte(actual), path(filename)); err != nil { + t.Fatalf("%v", err) + } +} + +// AssertGoldenFile asserts that the content of the actual file matches the contents of the expected file +func AssertGoldenFile(t TestingT, actualFileName string, expectedFilename string) { + t.Helper() + + actual, err := ioutil.ReadFile(actualFileName) + if err != nil { + t.Fatalf("%v", err) + } + AssertGoldenBytes(t, actual, expectedFilename) +} + +func path(filename string) string { + if filepath.IsAbs(filename) { + return filename + } + return filepath.Join("testdata", filename) +} + +func compare(actual []byte, filename string) error { + actual = normalize(actual) + if err := update(filename, actual); err != nil { + return err + } + + expected, err := ioutil.ReadFile(filename) + if err != nil { + return errors.Wrapf(err, "unable to read testdata %s", filename) + } + expected = normalize(expected) + if !bytes.Equal(expected, actual) { + return errors.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'\n", filename, expected, actual) + } + return nil +} + +func update(filename string, in []byte) error { + if !*updateGolden { + return nil + } + return ioutil.WriteFile(filename, normalize(in), 0666) +} + +func normalize(in []byte) []byte { + return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1) +} diff --git a/internal/third_party/dep/fs/fs.go b/internal/third_party/dep/fs/fs.go new file mode 100644 index 00000000000..83259219736 --- /dev/null +++ b/internal/third_party/dep/fs/fs.go @@ -0,0 +1,373 @@ +/* +Copyright (c) for portions of fs.go are held by The Go Authors, 2016 and are provided under +the BSD license. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package fs + +import ( + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "syscall" + + "github.com/pkg/errors" +) + +// fs contains a copy of a few functions from dep tool code to avoid a dependency on golang/dep. +// This code is copied from https://github.com/golang/dep/blob/37d6c560cdf407be7b6cd035b23dba89df9275cf/internal/fs/fs.go +// No changes to the code were made other than removing some unused functions + +// RenameWithFallback attempts to rename a file or directory, but falls back to +// copying in the event of a cross-device link error. If the fallback copy +// succeeds, src is still removed, emulating normal rename behavior. +func RenameWithFallback(src, dst string) error { + _, err := os.Stat(src) + if err != nil { + return errors.Wrapf(err, "cannot stat %s", src) + } + + err = os.Rename(src, dst) + if err == nil { + return nil + } + + return renameFallback(err, src, dst) +} + +// renameByCopy attempts to rename a file or directory by copying it to the +// destination and then removing the src thus emulating the rename behavior. +func renameByCopy(src, dst string) error { + var cerr error + if dir, _ := IsDir(src); dir { + cerr = CopyDir(src, dst) + if cerr != nil { + cerr = errors.Wrap(cerr, "copying directory failed") + } + } else { + cerr = copyFile(src, dst) + if cerr != nil { + cerr = errors.Wrap(cerr, "copying file failed") + } + } + + if cerr != nil { + return errors.Wrapf(cerr, "rename fallback failed: cannot rename %s to %s", src, dst) + } + + return errors.Wrapf(os.RemoveAll(src), "cannot delete %s", src) +} + +var ( + errSrcNotDir = errors.New("source is not a directory") + errDstExist = errors.New("destination already exists") +) + +// CopyDir recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory must *not* exist. +func CopyDir(src, dst string) error { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + // We use os.Lstat() here to ensure we don't fall in a loop where a symlink + // actually links to a one of its parent directories. + fi, err := os.Lstat(src) + if err != nil { + return err + } + if !fi.IsDir() { + return errSrcNotDir + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + return errDstExist + } + + if err = os.MkdirAll(dst, fi.Mode()); err != nil { + return errors.Wrapf(err, "cannot mkdir %s", dst) + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return errors.Wrapf(err, "cannot read directory %s", dst) + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + if err = CopyDir(srcPath, dstPath); err != nil { + return errors.Wrap(err, "copying directory failed") + } + } else { + // This will include symlinks, which is what we want when + // copying things. + if err = copyFile(srcPath, dstPath); err != nil { + return errors.Wrap(err, "copying file failed") + } + } + } + + return nil +} + +// copyFile copies the contents of the file named src to the file named +// by dst. The file will be created if it does not already exist. If the +// destination file exists, all its contents will be replaced by the contents +// of the source file. The file mode will be copied from the source. +func copyFile(src, dst string) (err error) { + if sym, err := IsSymlink(src); err != nil { + return errors.Wrap(err, "symlink check failed") + } else if sym { + if err := cloneSymlink(src, dst); err != nil { + if runtime.GOOS == "windows" { + // If cloning the symlink fails on Windows because the user + // does not have the required privileges, ignore the error and + // fall back to copying the file contents. + // + // ERROR_PRIVILEGE_NOT_HELD is 1314 (0x522): + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx + if lerr, ok := err.(*os.LinkError); ok && lerr.Err != syscall.Errno(1314) { + return err + } + } else { + return err + } + } else { + return nil + } + } + + in, err := os.Open(src) + if err != nil { + return + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return + } + + if _, err = io.Copy(out, in); err != nil { + out.Close() + return + } + + // Check for write errors on Close + if err = out.Close(); err != nil { + return + } + + si, err := os.Stat(src) + if err != nil { + return + } + + // Temporary fix for Go < 1.9 + // + // See: https://github.com/golang/dep/issues/774 + // and https://github.com/golang/go/issues/20829 + if runtime.GOOS == "windows" { + dst = fixLongPath(dst) + } + err = os.Chmod(dst, si.Mode()) + + return +} + +// cloneSymlink will create a new symlink that points to the resolved path of sl. +// If sl is a relative symlink, dst will also be a relative symlink. +func cloneSymlink(sl, dst string) error { + resolved, err := os.Readlink(sl) + if err != nil { + return err + } + + return os.Symlink(resolved, dst) +} + +// IsDir determines is the path given is a directory or not. +func IsDir(name string) (bool, error) { + fi, err := os.Stat(name) + if err != nil { + return false, err + } + if !fi.IsDir() { + return false, errors.Errorf("%q is not a directory", name) + } + return true, nil +} + +// IsSymlink determines if the given path is a symbolic link. +func IsSymlink(path string) (bool, error) { + l, err := os.Lstat(path) + if err != nil { + return false, err + } + + return l.Mode()&os.ModeSymlink == os.ModeSymlink, nil +} + +// fixLongPath returns the extended-length (\\?\-prefixed) form of +// path when needed, in order to avoid the default 260 character file +// path limit imposed by Windows. If path is not easily converted to +// the extended-length form (for example, if path is a relative path +// or contains .. elements), or is short enough, fixLongPath returns +// path unmodified. +// +// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath +func fixLongPath(path string) string { + // Do nothing (and don't allocate) if the path is "short". + // Empirically (at least on the Windows Server 2013 builder), + // the kernel is arbitrarily okay with < 248 bytes. That + // matches what the docs above say: + // "When using an API to create a directory, the specified + // path cannot be so long that you cannot append an 8.3 file + // name (that is, the directory name cannot exceed MAX_PATH + // minus 12)." Since MAX_PATH is 260, 260 - 12 = 248. + // + // The MSDN docs appear to say that a normal path that is 248 bytes long + // will work; empirically the path must be less then 248 bytes long. + if len(path) < 248 { + // Don't fix. (This is how Go 1.7 and earlier worked, + // not automatically generating the \\?\ form) + return path + } + + // The extended form begins with \\?\, as in + // \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt. + // The extended form disables evaluation of . and .. path + // elements and disables the interpretation of / as equivalent + // to \. The conversion here rewrites / to \ and elides + // . elements as well as trailing or duplicate separators. For + // simplicity it avoids the conversion entirely for relative + // paths or paths containing .. elements. For now, + // \\server\share paths are not converted to + // \\?\UNC\server\share paths because the rules for doing so + // are less well-specified. + if len(path) >= 2 && path[:2] == `\\` { + // Don't canonicalize UNC paths. + return path + } + if !isAbs(path) { + // Relative path + return path + } + + const prefix = `\\?` + + pathbuf := make([]byte, len(prefix)+len(path)+len(`\`)) + copy(pathbuf, prefix) + n := len(path) + r, w := 0, len(prefix) + for r < n { + switch { + case os.IsPathSeparator(path[r]): + // empty block + r++ + case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])): + // /./ + r++ + case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])): + // /../ is currently unhandled + return path + default: + pathbuf[w] = '\\' + w++ + for ; r < n && !os.IsPathSeparator(path[r]); r++ { + pathbuf[w] = path[r] + w++ + } + } + } + // A drive's root directory needs a trailing \ + if w == len(`\\?\c:`) { + pathbuf[w] = '\\' + w++ + } + return string(pathbuf[:w]) +} + +func isAbs(path string) (b bool) { + v := volumeName(path) + if v == "" { + return false + } + path = path[len(v):] + if path == "" { + return false + } + return os.IsPathSeparator(path[0]) +} + +func volumeName(path string) (v string) { + if len(path) < 2 { + return "" + } + // with drive letter + c := path[0] + if path[1] == ':' && + ('0' <= c && c <= '9' || 'a' <= c && c <= 'z' || + 'A' <= c && c <= 'Z') { + return path[:2] + } + // is it UNC + if l := len(path); l >= 5 && os.IsPathSeparator(path[0]) && os.IsPathSeparator(path[1]) && + !os.IsPathSeparator(path[2]) && path[2] != '.' { + // first, leading `\\` and next shouldn't be `\`. its server name. + for n := 3; n < l-1; n++ { + // second, next '\' shouldn't be repeated. + if os.IsPathSeparator(path[n]) { + n++ + // third, following something characters. its share name. + if !os.IsPathSeparator(path[n]) { + if path[n] == '.' { + break + } + for ; n < l; n++ { + if os.IsPathSeparator(path[n]) { + break + } + } + return path[:n] + } + break + } + } + } + return "" +} diff --git a/internal/third_party/dep/fs/fs_test.go b/internal/third_party/dep/fs/fs_test.go new file mode 100644 index 00000000000..98a31aec67f --- /dev/null +++ b/internal/third_party/dep/fs/fs_test.go @@ -0,0 +1,694 @@ +/* +Copyright (c) for portions of fs_test.go are held by The Go Authors, 2016 and are provided under +the BSD license. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package fs + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" + "testing" +) + +var ( + mu sync.Mutex +) + +func TestRenameWithFallback(t *testing.T) { + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + if err = RenameWithFallback(filepath.Join(dir, "does_not_exists"), filepath.Join(dir, "dst")); err == nil { + t.Fatal("expected an error for non existing file, but got nil") + } + + srcpath := filepath.Join(dir, "src") + + if srcf, err := os.Create(srcpath); err != nil { + t.Fatal(err) + } else { + srcf.Close() + } + + if err = RenameWithFallback(srcpath, filepath.Join(dir, "dst")); err != nil { + t.Fatal(err) + } + + srcpath = filepath.Join(dir, "a") + if err = os.MkdirAll(srcpath, 0777); err != nil { + t.Fatal(err) + } + + dstpath := filepath.Join(dir, "b") + if err = os.MkdirAll(dstpath, 0777); err != nil { + t.Fatal(err) + } + + if err = RenameWithFallback(srcpath, dstpath); err == nil { + t.Fatal("expected an error if dst is an existing directory, but got nil") + } +} + +func TestCopyDir(t *testing.T) { + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcdir := filepath.Join(dir, "src") + if err := os.MkdirAll(srcdir, 0755); err != nil { + t.Fatal(err) + } + + files := []struct { + path string + contents string + fi os.FileInfo + }{ + {path: "myfile", contents: "hello world"}, + {path: filepath.Join("subdir", "file"), contents: "subdir file"}, + } + + // Create structure indicated in 'files' + for i, file := range files { + fn := filepath.Join(srcdir, file.path) + dn := filepath.Dir(fn) + if err = os.MkdirAll(dn, 0755); err != nil { + t.Fatal(err) + } + + fh, err := os.Create(fn) + if err != nil { + t.Fatal(err) + } + + if _, err = fh.Write([]byte(file.contents)); err != nil { + t.Fatal(err) + } + fh.Close() + + files[i].fi, err = os.Stat(fn) + if err != nil { + t.Fatal(err) + } + } + + destdir := filepath.Join(dir, "dest") + if err := CopyDir(srcdir, destdir); err != nil { + t.Fatal(err) + } + + // Compare copy against structure indicated in 'files' + for _, file := range files { + fn := filepath.Join(srcdir, file.path) + dn := filepath.Dir(fn) + dirOK, err := IsDir(dn) + if err != nil { + t.Fatal(err) + } + if !dirOK { + t.Fatalf("expected %s to be a directory", dn) + } + + got, err := ioutil.ReadFile(fn) + if err != nil { + t.Fatal(err) + } + + if file.contents != string(got) { + t.Fatalf("expected: %s, got: %s", file.contents, string(got)) + } + + gotinfo, err := os.Stat(fn) + if err != nil { + t.Fatal(err) + } + + if file.fi.Mode() != gotinfo.Mode() { + t.Fatalf("expected %s: %#v\n to be the same mode as %s: %#v", + file.path, file.fi.Mode(), fn, gotinfo.Mode()) + } + } +} + +func TestCopyDirFail_SrcInaccessible(t *testing.T) { + if runtime.GOOS == "windows" { + // XXX: setting permissions works differently in + // Microsoft Windows. Skipping this until a + // compatible implementation is provided. + t.Skip("skipping on windows") + } + + var currentUID = os.Getuid() + + if currentUID == 0 { + // Skipping if root, because all files are accessible + t.Skip("Skipping for root user") + } + + var srcdir, dstdir string + + cleanup := setupInaccessibleDir(t, func(dir string) error { + srcdir = filepath.Join(dir, "src") + return os.MkdirAll(srcdir, 0755) + }) + defer cleanup() + + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + dstdir = filepath.Join(dir, "dst") + if err = CopyDir(srcdir, dstdir); err == nil { + t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) + } +} + +func TestCopyDirFail_DstInaccessible(t *testing.T) { + if runtime.GOOS == "windows" { + // XXX: setting permissions works differently in + // Microsoft Windows. Skipping this until a + // compatible implementation is provided. + t.Skip("skipping on windows") + } + + var currentUID = os.Getuid() + + if currentUID == 0 { + // Skipping if root, because all files are accessible + t.Skip("Skipping for root user") + } + + var srcdir, dstdir string + + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcdir = filepath.Join(dir, "src") + if err = os.MkdirAll(srcdir, 0755); err != nil { + t.Fatal(err) + } + + cleanup := setupInaccessibleDir(t, func(dir string) error { + dstdir = filepath.Join(dir, "dst") + return nil + }) + defer cleanup() + + if err := CopyDir(srcdir, dstdir); err == nil { + t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) + } +} + +func TestCopyDirFail_SrcIsNotDir(t *testing.T) { + var srcdir, dstdir string + + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcdir = filepath.Join(dir, "src") + if _, err = os.Create(srcdir); err != nil { + t.Fatal(err) + } + + dstdir = filepath.Join(dir, "dst") + + if err = CopyDir(srcdir, dstdir); err == nil { + t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) + } + + if err != errSrcNotDir { + t.Fatalf("expected %v error for CopyDir(%s, %s), got %s", errSrcNotDir, srcdir, dstdir, err) + } + +} + +func TestCopyDirFail_DstExists(t *testing.T) { + var srcdir, dstdir string + + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcdir = filepath.Join(dir, "src") + if err = os.MkdirAll(srcdir, 0755); err != nil { + t.Fatal(err) + } + + dstdir = filepath.Join(dir, "dst") + if err = os.MkdirAll(dstdir, 0755); err != nil { + t.Fatal(err) + } + + if err = CopyDir(srcdir, dstdir); err == nil { + t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) + } + + if err != errDstExist { + t.Fatalf("expected %v error for CopyDir(%s, %s), got %s", errDstExist, srcdir, dstdir, err) + } +} + +func TestCopyDirFailOpen(t *testing.T) { + if runtime.GOOS == "windows" { + // XXX: setting permissions works differently in + // Microsoft Windows. os.Chmod(..., 0222) below is not + // enough for the file to be readonly, and os.Chmod(..., + // 0000) returns an invalid argument error. Skipping + // this until a compatible implementation is + // provided. + t.Skip("skipping on windows") + } + + var currentUID = os.Getuid() + + if currentUID == 0 { + // Skipping if root, because all files are accessible + t.Skip("Skipping for root user") + } + + var srcdir, dstdir string + + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcdir = filepath.Join(dir, "src") + if err = os.MkdirAll(srcdir, 0755); err != nil { + t.Fatal(err) + } + + srcfn := filepath.Join(srcdir, "file") + srcf, err := os.Create(srcfn) + if err != nil { + t.Fatal(err) + } + srcf.Close() + + // setup source file so that it cannot be read + if err = os.Chmod(srcfn, 0222); err != nil { + t.Fatal(err) + } + + dstdir = filepath.Join(dir, "dst") + + if err = CopyDir(srcdir, dstdir); err == nil { + t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) + } +} + +func TestCopyFile(t *testing.T) { + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcf, err := os.Create(filepath.Join(dir, "srcfile")) + if err != nil { + t.Fatal(err) + } + + want := "hello world" + if _, err := srcf.Write([]byte(want)); err != nil { + t.Fatal(err) + } + srcf.Close() + + destf := filepath.Join(dir, "destf") + if err := copyFile(srcf.Name(), destf); err != nil { + t.Fatal(err) + } + + got, err := ioutil.ReadFile(destf) + if err != nil { + t.Fatal(err) + } + + if want != string(got) { + t.Fatalf("expected: %s, got: %s", want, string(got)) + } + + wantinfo, err := os.Stat(srcf.Name()) + if err != nil { + t.Fatal(err) + } + + gotinfo, err := os.Stat(destf) + if err != nil { + t.Fatal(err) + } + + if wantinfo.Mode() != gotinfo.Mode() { + t.Fatalf("expected %s: %#v\n to be the same mode as %s: %#v", srcf.Name(), wantinfo.Mode(), destf, gotinfo.Mode()) + } +} + +func cleanUpDir(dir string) { + // NOTE(mattn): It seems that sometimes git.exe is not dead + // when cleanUpDir() is called. But we do not know any way to wait for it. + if runtime.GOOS == "windows" { + mu.Lock() + exec.Command(`taskkill`, `/F`, `/IM`, `git.exe`).Run() + mu.Unlock() + } + if dir != "" { + os.RemoveAll(dir) + } +} + +func TestCopyFileSymlink(t *testing.T) { + var tempdir, err = ioutil.TempDir("", "gotest") + + if err != nil { + t.Fatalf("failed to create directory: %s", err) + } + + defer cleanUpDir(tempdir) + + testcases := map[string]string{ + filepath.Join("./testdata/symlinks/file-symlink"): filepath.Join(tempdir, "dst-file"), + filepath.Join("./testdata/symlinks/windows-file-symlink"): filepath.Join(tempdir, "windows-dst-file"), + filepath.Join("./testdata/symlinks/invalid-symlink"): filepath.Join(tempdir, "invalid-symlink"), + } + + for symlink, dst := range testcases { + t.Run(symlink, func(t *testing.T) { + var err error + if err = copyFile(symlink, dst); err != nil { + t.Fatalf("failed to copy symlink: %s", err) + } + + var want, got string + + if runtime.GOOS == "windows" { + // Creating symlinks on Windows require an additional permission + // regular users aren't granted usually. So we copy the file + // content as a fall back instead of creating a real symlink. + srcb, err := ioutil.ReadFile(symlink) + if err != nil { + t.Fatalf("%+v", err) + } + dstb, err := ioutil.ReadFile(dst) + if err != nil { + t.Fatalf("%+v", err) + } + + want = string(srcb) + got = string(dstb) + } else { + want, err = os.Readlink(symlink) + if err != nil { + t.Fatalf("%+v", err) + } + + got, err = os.Readlink(dst) + if err != nil { + t.Fatalf("could not resolve symlink: %s", err) + } + } + + if want != got { + t.Fatalf("resolved path is incorrect. expected %s, got %s", want, got) + } + }) + } +} + +func TestCopyFileFail(t *testing.T) { + if runtime.GOOS == "windows" { + // XXX: setting permissions works differently in + // Microsoft Windows. Skipping this until a + // compatible implementation is provided. + t.Skip("skipping on windows") + } + + var currentUID = os.Getuid() + + if currentUID == 0 { + // Skipping if root, because all files are accessible + t.Skip("Skipping for root user") + } + + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + srcf, err := os.Create(filepath.Join(dir, "srcfile")) + if err != nil { + t.Fatal(err) + } + srcf.Close() + + var dstdir string + + cleanup := setupInaccessibleDir(t, func(dir string) error { + dstdir = filepath.Join(dir, "dir") + return os.Mkdir(dstdir, 0777) + }) + defer cleanup() + + fn := filepath.Join(dstdir, "file") + if err := copyFile(srcf.Name(), fn); err == nil { + t.Fatalf("expected error for %s, got none", fn) + } +} + +// setupInaccessibleDir creates a temporary location with a single +// directory in it, in such a way that directory is not accessible +// after this function returns. +// +// op is called with the directory as argument, so that it can create +// files or other test artifacts. +// +// If setupInaccessibleDir fails in its preparation, or op fails, t.Fatal +// will be invoked. +// +// This function returns a cleanup function that removes all the temporary +// files this function creates. It is the caller's responsibility to call +// this function before the test is done running, whether there's an error or not. +func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() { + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + return nil // keep compiler happy + } + + subdir := filepath.Join(dir, "dir") + + cleanup := func() { + if err := os.Chmod(subdir, 0777); err != nil { + t.Error(err) + } + if err := os.RemoveAll(dir); err != nil { + t.Error(err) + } + } + + if err := os.Mkdir(subdir, 0777); err != nil { + cleanup() + t.Fatal(err) + return nil + } + + if err := op(subdir); err != nil { + cleanup() + t.Fatal(err) + return nil + } + + if err := os.Chmod(subdir, 0666); err != nil { + cleanup() + t.Fatal(err) + return nil + } + + return cleanup +} + +func TestIsDir(t *testing.T) { + + var currentUID = os.Getuid() + + if currentUID == 0 { + // Skipping if root, because all files are accessible + t.Skip("Skipping for root user") + } + + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + var dn string + + cleanup := setupInaccessibleDir(t, func(dir string) error { + dn = filepath.Join(dir, "dir") + return os.Mkdir(dn, 0777) + }) + defer cleanup() + + tests := map[string]struct { + exists bool + err bool + }{ + wd: {true, false}, + filepath.Join(wd, "testdata"): {true, false}, + filepath.Join(wd, "main.go"): {false, true}, + filepath.Join(wd, "this_file_does_not_exist.thing"): {false, true}, + dn: {false, true}, + } + + if runtime.GOOS == "windows" { + // This test doesn't work on Microsoft Windows because + // of the differences in how file permissions are + // implemented. For this to work, the directory where + // the directory exists should be inaccessible. + delete(tests, dn) + } + + for f, want := range tests { + got, err := IsDir(f) + if err != nil && !want.err { + t.Fatalf("expected no error, got %v", err) + } + + if got != want.exists { + t.Fatalf("expected %t for %s, got %t", want.exists, f, got) + } + } +} + +func TestIsSymlink(t *testing.T) { + + var currentUID = os.Getuid() + + if currentUID == 0 { + // Skipping if root, because all files are accessible + t.Skip("Skipping for root user") + } + + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + dirPath := filepath.Join(dir, "directory") + if err = os.MkdirAll(dirPath, 0777); err != nil { + t.Fatal(err) + } + + filePath := filepath.Join(dir, "file") + f, err := os.Create(filePath) + if err != nil { + t.Fatal(err) + } + f.Close() + + dirSymlink := filepath.Join(dir, "dirSymlink") + fileSymlink := filepath.Join(dir, "fileSymlink") + + if err = os.Symlink(dirPath, dirSymlink); err != nil { + t.Fatal(err) + } + if err = os.Symlink(filePath, fileSymlink); err != nil { + t.Fatal(err) + } + + var ( + inaccessibleFile string + inaccessibleSymlink string + ) + + cleanup := setupInaccessibleDir(t, func(dir string) error { + inaccessibleFile = filepath.Join(dir, "file") + if fh, err := os.Create(inaccessibleFile); err != nil { + return err + } else if err = fh.Close(); err != nil { + return err + } + + inaccessibleSymlink = filepath.Join(dir, "symlink") + return os.Symlink(inaccessibleFile, inaccessibleSymlink) + }) + defer cleanup() + + tests := map[string]struct{ expected, err bool }{ + dirPath: {false, false}, + filePath: {false, false}, + dirSymlink: {true, false}, + fileSymlink: {true, false}, + inaccessibleFile: {false, true}, + inaccessibleSymlink: {false, true}, + } + + if runtime.GOOS == "windows" { + // XXX: setting permissions works differently in Windows. Skipping + // these cases until a compatible implementation is provided. + delete(tests, inaccessibleFile) + delete(tests, inaccessibleSymlink) + } + + for path, want := range tests { + got, err := IsSymlink(path) + if err != nil { + if !want.err { + t.Errorf("expected no error, got %v", err) + } + } + + if got != want.expected { + t.Errorf("expected %t for %s, got %t", want.expected, path, got) + } + } +} diff --git a/internal/third_party/dep/fs/rename.go b/internal/third_party/dep/fs/rename.go new file mode 100644 index 00000000000..0bb600949e6 --- /dev/null +++ b/internal/third_party/dep/fs/rename.go @@ -0,0 +1,58 @@ +// +build !windows + +/* +Copyright (c) for portions of rename.go are held by The Go Authors, 2016 and are provided under +the BSD license. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package fs + +import ( + "os" + "syscall" + + "github.com/pkg/errors" +) + +// renameFallback attempts to determine the appropriate fallback to failed rename +// operation depending on the resulting error. +func renameFallback(err error, src, dst string) error { + // Rename may fail if src and dst are on different devices; fall back to + // copy if we detect that case. syscall.EXDEV is the common name for the + // cross device link error which has varying output text across different + // operating systems. + terr, ok := err.(*os.LinkError) + if !ok { + return err + } else if terr.Err != syscall.EXDEV { + return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst) + } + + return renameByCopy(src, dst) +} diff --git a/internal/third_party/dep/fs/rename_windows.go b/internal/third_party/dep/fs/rename_windows.go new file mode 100644 index 00000000000..14f017d0956 --- /dev/null +++ b/internal/third_party/dep/fs/rename_windows.go @@ -0,0 +1,69 @@ +// +build windows + +/* +Copyright (c) for portions of rename_windows.go are held by The Go Authors, 2016 and are provided under +the BSD license. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +package fs + +import ( + "os" + "syscall" + + "github.com/pkg/errors" +) + +// renameFallback attempts to determine the appropriate fallback to failed rename +// operation depending on the resulting error. +func renameFallback(err error, src, dst string) error { + // Rename may fail if src and dst are on different devices; fall back to + // copy if we detect that case. syscall.EXDEV is the common name for the + // cross device link error which has varying output text across different + // operating systems. + terr, ok := err.(*os.LinkError) + if !ok { + return err + } + + if terr.Err != syscall.EXDEV { + // In windows it can drop down to an operating system call that + // returns an operating system error with a different number and + // message. Checking for that as a fall back. + noerr, ok := terr.Err.(syscall.Errno) + + // 0x11 (ERROR_NOT_SAME_DEVICE) is the windows error. + // See https://msdn.microsoft.com/en-us/library/cc231199.aspx + if ok && noerr != 0x11 { + return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst) + } + } + + return renameByCopy(src, dst) +} diff --git a/internal/third_party/dep/fs/testdata/symlinks/file-symlink b/internal/third_party/dep/fs/testdata/symlinks/file-symlink new file mode 120000 index 00000000000..4c52274de03 --- /dev/null +++ b/internal/third_party/dep/fs/testdata/symlinks/file-symlink @@ -0,0 +1 @@ +../test.file \ No newline at end of file diff --git a/internal/third_party/dep/fs/testdata/symlinks/invalid-symlink b/internal/third_party/dep/fs/testdata/symlinks/invalid-symlink new file mode 120000 index 00000000000..0edf4f301b1 --- /dev/null +++ b/internal/third_party/dep/fs/testdata/symlinks/invalid-symlink @@ -0,0 +1 @@ +/non/existing/file \ No newline at end of file diff --git a/internal/third_party/dep/fs/testdata/symlinks/windows-file-symlink b/internal/third_party/dep/fs/testdata/symlinks/windows-file-symlink new file mode 120000 index 00000000000..af1d6c8f573 --- /dev/null +++ b/internal/third_party/dep/fs/testdata/symlinks/windows-file-symlink @@ -0,0 +1 @@ +C:/Users/ibrahim/go/src/github.com/golang/dep/internal/fs/testdata/test.file \ No newline at end of file diff --git a/internal/third_party/dep/fs/testdata/test.file b/internal/third_party/dep/fs/testdata/test.file new file mode 100644 index 00000000000..e69de29bb2d diff --git a/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go b/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go new file mode 100644 index 00000000000..103db35c406 --- /dev/null +++ b/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go @@ -0,0 +1,178 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 util + +import ( + "context" + "sort" + + apps "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + intstrutil "k8s.io/apimachinery/pkg/util/intstr" + appsclient "k8s.io/client-go/kubernetes/typed/apps/v1" +) + +// deploymentutil contains a copy of a few functions from Kubernetes controller code to avoid a dependency on k8s.io/kubernetes. +// This code is copied from https://github.com/kubernetes/kubernetes/blob/e856613dd5bb00bcfaca6974431151b5c06cbed5/pkg/controller/deployment/util/deployment_util.go +// No changes to the code were made other than removing some unused functions + +// RsListFunc returns the ReplicaSet from the ReplicaSet namespace and the List metav1.ListOptions. +type RsListFunc func(string, metav1.ListOptions) ([]*apps.ReplicaSet, error) + +// ListReplicaSets returns a slice of RSes the given deployment targets. +// Note that this does NOT attempt to reconcile ControllerRef (adopt/orphan), +// because only the controller itself should do that. +// However, it does filter out anything whose ControllerRef doesn't match. +func ListReplicaSets(deployment *apps.Deployment, getRSList RsListFunc) ([]*apps.ReplicaSet, error) { + // TODO: Right now we list replica sets by their labels. We should list them by selector, i.e. the replica set's selector + // should be a superset of the deployment's selector, see https://github.com/kubernetes/kubernetes/issues/19830. + namespace := deployment.Namespace + selector, err := metav1.LabelSelectorAsSelector(deployment.Spec.Selector) + if err != nil { + return nil, err + } + options := metav1.ListOptions{LabelSelector: selector.String()} + all, err := getRSList(namespace, options) + if err != nil { + return nil, err + } + // Only include those whose ControllerRef matches the Deployment. + owned := make([]*apps.ReplicaSet, 0, len(all)) + for _, rs := range all { + if metav1.IsControlledBy(rs, deployment) { + owned = append(owned, rs) + } + } + return owned, nil +} + +// ReplicaSetsByCreationTimestamp sorts a list of ReplicaSet by creation timestamp, using their names as a tie breaker. +type ReplicaSetsByCreationTimestamp []*apps.ReplicaSet + +func (o ReplicaSetsByCreationTimestamp) Len() int { return len(o) } +func (o ReplicaSetsByCreationTimestamp) Swap(i, j int) { o[i], o[j] = o[j], o[i] } +func (o ReplicaSetsByCreationTimestamp) Less(i, j int) bool { + if o[i].CreationTimestamp.Equal(&o[j].CreationTimestamp) { + return o[i].Name < o[j].Name + } + return o[i].CreationTimestamp.Before(&o[j].CreationTimestamp) +} + +// FindNewReplicaSet returns the new RS this given deployment targets (the one with the same pod template). +func FindNewReplicaSet(deployment *apps.Deployment, rsList []*apps.ReplicaSet) *apps.ReplicaSet { + sort.Sort(ReplicaSetsByCreationTimestamp(rsList)) + for i := range rsList { + if EqualIgnoreHash(&rsList[i].Spec.Template, &deployment.Spec.Template) { + // In rare cases, such as after cluster upgrades, Deployment may end up with + // having more than one new ReplicaSets that have the same template as its template, + // see https://github.com/kubernetes/kubernetes/issues/40415 + // We deterministically choose the oldest new ReplicaSet. + return rsList[i] + } + } + // new ReplicaSet does not exist. + return nil +} + +// EqualIgnoreHash returns true if two given podTemplateSpec are equal, ignoring the diff in value of Labels[pod-template-hash] +// We ignore pod-template-hash because: +// 1. The hash result would be different upon podTemplateSpec API changes +// (e.g. the addition of a new field will cause the hash code to change) +// 2. The deployment template won't have hash labels +func EqualIgnoreHash(template1, template2 *v1.PodTemplateSpec) bool { + t1Copy := template1.DeepCopy() + t2Copy := template2.DeepCopy() + // Remove hash labels from template.Labels before comparing + delete(t1Copy.Labels, apps.DefaultDeploymentUniqueLabelKey) + delete(t2Copy.Labels, apps.DefaultDeploymentUniqueLabelKey) + return apiequality.Semantic.DeepEqual(t1Copy, t2Copy) +} + +// GetNewReplicaSet returns a replica set that matches the intent of the given deployment; get ReplicaSetList from client interface. +// Returns nil if the new replica set doesn't exist yet. +func GetNewReplicaSet(deployment *apps.Deployment, c appsclient.AppsV1Interface) (*apps.ReplicaSet, error) { + rsList, err := ListReplicaSets(deployment, RsListFromClient(c)) + if err != nil { + return nil, err + } + return FindNewReplicaSet(deployment, rsList), nil +} + +// RsListFromClient returns an rsListFunc that wraps the given client. +func RsListFromClient(c appsclient.AppsV1Interface) RsListFunc { + return func(namespace string, options metav1.ListOptions) ([]*apps.ReplicaSet, error) { + rsList, err := c.ReplicaSets(namespace).List(context.Background(), options) + if err != nil { + return nil, err + } + var ret []*apps.ReplicaSet + for i := range rsList.Items { + ret = append(ret, &rsList.Items[i]) + } + return ret, err + } +} + +// IsRollingUpdate returns true if the strategy type is a rolling update. +func IsRollingUpdate(deployment *apps.Deployment) bool { + return deployment.Spec.Strategy.Type == apps.RollingUpdateDeploymentStrategyType +} + +// MaxUnavailable returns the maximum unavailable pods a rolling deployment can take. +func MaxUnavailable(deployment apps.Deployment) int32 { + if !IsRollingUpdate(&deployment) || *(deployment.Spec.Replicas) == 0 { + return int32(0) + } + // Error caught by validation + _, maxUnavailable, _ := ResolveFenceposts(deployment.Spec.Strategy.RollingUpdate.MaxSurge, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable, *(deployment.Spec.Replicas)) + if maxUnavailable > *deployment.Spec.Replicas { + return *deployment.Spec.Replicas + } + return maxUnavailable +} + +// ResolveFenceposts resolves both maxSurge and maxUnavailable. This needs to happen in one +// step. For example: +// +// 2 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1), then old(-1), then new(+1) +// 1 desired, max unavailable 1%, surge 0% - should scale old(-1), then new(+1) +// 2 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1) +// 1 desired, max unavailable 25%, surge 1% - should scale new(+1), then old(-1) +// 2 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1), then new(+1), then old(-1) +// 1 desired, max unavailable 0%, surge 1% - should scale new(+1), then old(-1) +func ResolveFenceposts(maxSurge, maxUnavailable *intstrutil.IntOrString, desired int32) (int32, int32, error) { + surge, err := intstrutil.GetValueFromIntOrPercent(intstrutil.ValueOrDefault(maxSurge, intstrutil.FromInt(0)), int(desired), true) + if err != nil { + return 0, 0, err + } + unavailable, err := intstrutil.GetValueFromIntOrPercent(intstrutil.ValueOrDefault(maxUnavailable, intstrutil.FromInt(0)), int(desired), false) + if err != nil { + return 0, 0, err + } + + if surge == 0 && unavailable == 0 { + // Validation should never allow the user to explicitly use zero values for both maxSurge + // maxUnavailable. Due to rounding down maxUnavailable though, it may resolve to zero. + // If both fenceposts resolve to zero, then we should set maxUnavailable to 1 on the + // theory that surge might not work due to quota. + unavailable = 1 + } + + return int32(surge), int32(unavailable), nil +} diff --git a/internal/tlsutil/cfg.go b/internal/tlsutil/cfg.go new file mode 100644 index 00000000000..8b9d4329fe9 --- /dev/null +++ b/internal/tlsutil/cfg.go @@ -0,0 +1,58 @@ +/* +Copyright The Helm Authors. + +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 tlsutil + +import ( + "crypto/tls" + "crypto/x509" + "os" + + "github.com/pkg/errors" +) + +// Options represents configurable options used to create client and server TLS configurations. +type Options struct { + CaCertFile string + // If either the KeyFile or CertFile is empty, ClientConfig() will not load them. + KeyFile string + CertFile string + // Client-only options + InsecureSkipVerify bool +} + +// ClientConfig returns a TLS configuration for use by a Helm client. +func ClientConfig(opts Options) (cfg *tls.Config, err error) { + var cert *tls.Certificate + var pool *x509.CertPool + + if opts.CertFile != "" || opts.KeyFile != "" { + if cert, err = CertFromFilePair(opts.CertFile, opts.KeyFile); err != nil { + if os.IsNotExist(err) { + return nil, errors.Wrapf(err, "could not load x509 key pair (cert: %q, key: %q)", opts.CertFile, opts.KeyFile) + } + return nil, errors.Wrapf(err, "could not read x509 key pair (cert: %q, key: %q)", opts.CertFile, opts.KeyFile) + } + } + if !opts.InsecureSkipVerify && opts.CaCertFile != "" { + if pool, err = CertPoolFromFile(opts.CaCertFile); err != nil { + return nil, err + } + } + + cfg = &tls.Config{InsecureSkipVerify: opts.InsecureSkipVerify, Certificates: []tls.Certificate{*cert}, RootCAs: pool} + return cfg, nil +} diff --git a/pkg/tlsutil/tls.go b/internal/tlsutil/tls.go similarity index 77% rename from pkg/tlsutil/tls.go rename to internal/tlsutil/tls.go index df698fd4ebb..ed7795dbeae 100644 --- a/pkg/tlsutil/tls.go +++ b/internal/tlsutil/tls.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,19 +19,23 @@ package tlsutil import ( "crypto/tls" "crypto/x509" - "fmt" "io/ioutil" + + "github.com/pkg/errors" ) // NewClientTLS returns tls.Config appropriate for client auth. func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) { - cert, err := CertFromFilePair(certFile, keyFile) - if err != nil { - return nil, err - } - config := tls.Config{ - Certificates: []tls.Certificate{*cert}, + config := tls.Config{} + + if certFile != "" && keyFile != "" { + cert, err := CertFromFilePair(certFile, keyFile) + if err != nil { + return nil, err + } + config.Certificates = []tls.Certificate{*cert} } + if caFile != "" { cp, err := CertPoolFromFile(caFile) if err != nil { @@ -39,6 +43,7 @@ func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) { } config.RootCAs = cp } + return &config, nil } @@ -49,11 +54,11 @@ func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) { func CertPoolFromFile(filename string) (*x509.CertPool, error) { b, err := ioutil.ReadFile(filename) if err != nil { - return nil, fmt.Errorf("can't read CA file: %v", filename) + return nil, errors.Errorf("can't read CA file: %v", filename) } cp := x509.NewCertPool() if !cp.AppendCertsFromPEM(b) { - return nil, fmt.Errorf("failed to append certificates from file: %s", filename) + return nil, errors.Errorf("failed to append certificates from file: %s", filename) } return cp, nil } @@ -65,7 +70,7 @@ func CertPoolFromFile(filename string) (*x509.CertPool, error) { func CertFromFilePair(certFile, keyFile string) (*tls.Certificate, error) { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { - return nil, fmt.Errorf("can't load key pair from cert %s and key %s: %s", certFile, keyFile, err) + return nil, errors.Wrapf(err, "can't load key pair from cert %s and key %s", certFile, keyFile) } return &cert, err } diff --git a/internal/tlsutil/tlsutil_test.go b/internal/tlsutil/tlsutil_test.go new file mode 100644 index 00000000000..e660c030c6c --- /dev/null +++ b/internal/tlsutil/tlsutil_test.go @@ -0,0 +1,113 @@ +/* +Copyright The Helm Authors. + +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 tlsutil + +import ( + "path/filepath" + "testing" +) + +const tlsTestDir = "../../testdata" + +const ( + testCaCertFile = "rootca.crt" + testCertFile = "crt.pem" + testKeyFile = "key.pem" +) + +func TestClientConfig(t *testing.T) { + opts := Options{ + CaCertFile: testfile(t, testCaCertFile), + CertFile: testfile(t, testCertFile), + KeyFile: testfile(t, testKeyFile), + InsecureSkipVerify: false, + } + + cfg, err := ClientConfig(opts) + if err != nil { + t.Fatalf("error building tls client config: %v", err) + } + + if got := len(cfg.Certificates); got != 1 { + t.Fatalf("expecting 1 client certificates, got %d", got) + } + if cfg.InsecureSkipVerify { + t.Fatalf("insecure skip verify mismatch, expecting false") + } + if cfg.RootCAs == nil { + t.Fatalf("mismatch tls RootCAs, expecting non-nil") + } +} + +func testfile(t *testing.T, file string) (path string) { + var err error + if path, err = filepath.Abs(filepath.Join(tlsTestDir, file)); err != nil { + t.Fatalf("error getting absolute path to test file %q: %v", file, err) + } + return path +} + +func TestNewClientTLS(t *testing.T) { + certFile := testfile(t, testCertFile) + keyFile := testfile(t, testKeyFile) + caCertFile := testfile(t, testCaCertFile) + + cfg, err := NewClientTLS(certFile, keyFile, caCertFile) + if err != nil { + t.Error(err) + } + + if got := len(cfg.Certificates); got != 1 { + t.Fatalf("expecting 1 client certificates, got %d", got) + } + if cfg.InsecureSkipVerify { + t.Fatalf("insecure skip verify mismatch, expecting false") + } + if cfg.RootCAs == nil { + t.Fatalf("mismatch tls RootCAs, expecting non-nil") + } + + cfg, err = NewClientTLS("", "", caCertFile) + if err != nil { + t.Error(err) + } + + if got := len(cfg.Certificates); got != 0 { + t.Fatalf("expecting 0 client certificates, got %d", got) + } + if cfg.InsecureSkipVerify { + t.Fatalf("insecure skip verify mismatch, expecting false") + } + if cfg.RootCAs == nil { + t.Fatalf("mismatch tls RootCAs, expecting non-nil") + } + + cfg, err = NewClientTLS(certFile, keyFile, "") + if err != nil { + t.Error(err) + } + + if got := len(cfg.Certificates); got != 1 { + t.Fatalf("expecting 1 client certificates, got %d", got) + } + if cfg.InsecureSkipVerify { + t.Fatalf("insecure skip verify mismatch, expecting false") + } + if cfg.RootCAs != nil { + t.Fatalf("mismatch tls RootCAs, expecting nil") + } +} diff --git a/pkg/urlutil/urlutil.go b/internal/urlutil/urlutil.go similarity index 81% rename from pkg/urlutil/urlutil.go rename to internal/urlutil/urlutil.go index fb67708ae17..a8cf7398c07 100644 --- a/pkg/urlutil/urlutil.go +++ b/internal/urlutil/urlutil.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import ( "net/url" "path" "path/filepath" - "strings" ) // URLJoin joins a base URL to one or more path components. @@ -70,18 +69,5 @@ func ExtractHostname(addr string) (string, error) { if err != nil { return "", err } - return stripPort(u.Host), nil -} - -// Backported from Go 1.8 because Circle is still on 1.7 -func stripPort(hostport string) string { - colon := strings.IndexByte(hostport, ':') - if colon == -1 { - return hostport - } - if i := strings.IndexByte(hostport, ']'); i != -1 { - return strings.TrimPrefix(hostport[:i], "[") - } - return hostport[:colon] - + return u.Hostname(), nil } diff --git a/pkg/urlutil/urlutil_test.go b/internal/urlutil/urlutil_test.go similarity index 91% rename from pkg/urlutil/urlutil_test.go rename to internal/urlutil/urlutil_test.go index f0c82c0a929..82acc40fee3 100644 --- a/pkg/urlutil/urlutil_test.go +++ b/internal/urlutil/urlutil_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ package urlutil import "testing" -func TestUrlJoin(t *testing.T) { +func TestURLJoin(t *testing.T) { tests := []struct { name, url, expect string paths []string @@ -56,6 +56,9 @@ func TestEqual(t *testing.T) { {"/foo", "/foo", true}, {"/foo", "/foo/", true}, {"/foo/.", "/foo/", true}, + {"%/1234", "%/1234", true}, + {"%/1234", "%/123", false}, + {"/1234", "%/1234", false}, } { if tt.match != Equal(tt.a, tt.b) { t.Errorf("Expected %q==%q to be %t", tt.a, tt.b, tt.match) @@ -65,8 +68,9 @@ func TestEqual(t *testing.T) { func TestExtractHostname(t *testing.T) { tests := map[string]string{ - "http://example.com": "example.com", - "https://example.com/foo": "example.com", + "http://example.com": "example.com", + "https://example.com/foo": "example.com", + "https://example.com:31337/not/with/a/bang/but/a/whimper": "example.com", } for start, expect := range tests { diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 00000000000..15822e91457 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,81 @@ +/* +Copyright The Helm Authors. + +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 version // import "helm.sh/helm/v3/internal/version" + +import ( + "flag" + "runtime" + "strings" +) + +var ( + // version is the current version of Helm. + // Update this whenever making a new release. + // The version is of the format Major.Minor.Patch[-Prerelease][+BuildMetadata] + // + // Increment major number for new feature additions and behavioral changes. + // Increment minor number for bug fixes and performance enhancements. + version = "v3.5" + + // metadata is extra build time data + metadata = "" + // gitCommit is the git sha1 + gitCommit = "" + // gitTreeState is the state of the git tree + gitTreeState = "" +) + +// BuildInfo describes the compile time information. +type BuildInfo struct { + // Version is the current semver. + Version string `json:"version,omitempty"` + // GitCommit is the git sha1. + GitCommit string `json:"git_commit,omitempty"` + // GitTreeState is the state of the git tree. + GitTreeState string `json:"git_tree_state,omitempty"` + // GoVersion is the version of the Go compiler used. + GoVersion string `json:"go_version,omitempty"` +} + +// GetVersion returns the semver string of the version +func GetVersion() string { + if metadata == "" { + return version + } + return version + "+" + metadata +} + +// GetUserAgent returns a user agent for user with an HTTP client +func GetUserAgent() string { + return "Helm/" + strings.TrimPrefix(GetVersion(), "v") +} + +// Get returns build info +func Get() BuildInfo { + v := BuildInfo{ + Version: GetVersion(), + GitCommit: gitCommit, + GitTreeState: gitTreeState, + GoVersion: runtime.Version(), + } + + // HACK(bacongobbler): strip out GoVersion during a test run for consistent test output + if flag.Lookup("test.v") != nil { + v.GoVersion = "" + } + return v +} diff --git a/pkg/action/action.go b/pkg/action/action.go new file mode 100644 index 00000000000..38ba638e4cd --- /dev/null +++ b/pkg/action/action.go @@ -0,0 +1,420 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "bytes" + "fmt" + "os" + "path" + "path/filepath" + "regexp" + "strings" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/discovery" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/engine" + "helm.sh/helm/v3/pkg/kube" + "helm.sh/helm/v3/pkg/postrender" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/releaseutil" + "helm.sh/helm/v3/pkg/storage" + "helm.sh/helm/v3/pkg/storage/driver" + "helm.sh/helm/v3/pkg/time" +) + +// Timestamper is a function capable of producing a timestamp.Timestamper. +// +// By default, this is a time.Time function from the Helm time package. This can +// be overridden for testing though, so that timestamps are predictable. +var Timestamper = time.Now + +var ( + // errMissingChart indicates that a chart was not provided. + errMissingChart = errors.New("no chart provided") + // errMissingRelease indicates that a release (name) was not provided. + errMissingRelease = errors.New("no release provided") + // errInvalidRevision indicates that an invalid release revision number was provided. + errInvalidRevision = errors.New("invalid release revision") + // errPending indicates that another instance of Helm is already applying an operation on a release. + errPending = errors.New("another operation (install/upgrade/rollback) is in progress") +) + +// ValidName is a regular expression for resource names. +// +// DEPRECATED: This will be removed in Helm 4, and is no longer used here. See +// pkg/lint/rules.validateMetadataNameFunc for the replacement. +// +// According to the Kubernetes help text, the regular expression it uses is: +// +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +var ValidName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) + +// Configuration injects the dependencies that all actions share. +type Configuration struct { + // RESTClientGetter is an interface that loads Kubernetes clients. + RESTClientGetter RESTClientGetter + + // Releases stores records of releases. + Releases *storage.Storage + + // KubeClient is a Kubernetes API client. + KubeClient kube.Interface + + // RegistryClient is a client for working with registries + RegistryClient *registry.Client + + // Capabilities describes the capabilities of the Kubernetes cluster. + Capabilities *chartutil.Capabilities + + Log func(string, ...interface{}) +} + +// renderResources renders the templates in a chart +// +// TODO: This function is badly in need of a refactor. +// TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed +// This code has to do with writing files to disk. +func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) { + hs := []*release.Hook{} + b := bytes.NewBuffer(nil) + + caps, err := c.getCapabilities() + if err != nil { + return hs, b, "", err + } + + if ch.Metadata.KubeVersion != "" { + if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) { + return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String()) + } + } + + var files map[string]string + var err2 error + + // A `helm template` or `helm install --dry-run` should not talk to the remote cluster. + // It will break in interesting and exotic ways because other data (e.g. discovery) + // is mocked. It is not up to the template author to decide when the user wants to + // connect to the cluster. So when the user says to dry run, respect the user's + // wishes and do not connect to the cluster. + if !dryRun && c.RESTClientGetter != nil { + rest, err := c.RESTClientGetter.ToRESTConfig() + if err != nil { + return hs, b, "", err + } + files, err2 = engine.RenderWithClient(ch, values, rest) + } else { + files, err2 = engine.Render(ch, values) + } + + if err2 != nil { + return hs, b, "", err2 + } + + // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, + // pull it out of here into a separate file so that we can actually use the output of the rendered + // text file. We have to spin through this map because the file contains path information, so we + // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip + // it in the sortHooks. + var notesBuffer bytes.Buffer + for k, v := range files { + if strings.HasSuffix(k, notesFileSuffix) { + if subNotes || (k == path.Join(ch.Name(), "templates", notesFileSuffix)) { + // If buffer contains data, add newline before adding more + if notesBuffer.Len() > 0 { + notesBuffer.WriteString("\n") + } + notesBuffer.WriteString(v) + } + delete(files, k) + } + } + notes := notesBuffer.String() + + // Sort hooks, manifests, and partials. Only hooks and manifests are returned, + // as partials are not used after renderer.Render. Empty manifests are also + // removed here. + hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder) + if err != nil { + // By catching parse errors here, we can prevent bogus releases from going + // to Kubernetes. + // + // We return the files as a big blob of data to help the user debug parser + // errors. + for name, content := range files { + if strings.TrimSpace(content) == "" { + continue + } + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) + } + return hs, b, "", err + } + + // Aggregate all valid manifests into one big doc. + fileWritten := make(map[string]bool) + + if includeCrds { + for _, crd := range ch.CRDObjects() { + if outputDir == "" { + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:])) + } else { + err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name]) + if err != nil { + return hs, b, "", err + } + fileWritten[crd.Name] = true + } + } + } + + for _, m := range manifests { + if outputDir == "" { + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) + } else { + newDir := outputDir + if useReleaseName { + newDir = filepath.Join(outputDir, releaseName) + } + // NOTE: We do not have to worry about the post-renderer because + // output dir is only used by `helm template`. In the next major + // release, we should move this logic to template only as it is not + // used by install or upgrade + err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name]) + if err != nil { + return hs, b, "", err + } + fileWritten[m.Name] = true + } + } + + if pr != nil { + b, err = pr.Run(b) + if err != nil { + return hs, b, notes, errors.Wrap(err, "error while running post render on files") + } + } + + return hs, b, notes, nil +} + +// RESTClientGetter gets the rest client +type RESTClientGetter interface { + ToRESTConfig() (*rest.Config, error) + ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) + ToRESTMapper() (meta.RESTMapper, error) +} + +// DebugLog sets the logger that writes debug strings +type DebugLog func(format string, v ...interface{}) + +// capabilities builds a Capabilities from discovery information. +func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) { + if c.Capabilities != nil { + return c.Capabilities, nil + } + dc, err := c.RESTClientGetter.ToDiscoveryClient() + if err != nil { + return nil, errors.Wrap(err, "could not get Kubernetes discovery client") + } + // force a discovery cache invalidation to always fetch the latest server version/capabilities. + dc.Invalidate() + kubeVersion, err := dc.ServerVersion() + if err != nil { + return nil, errors.Wrap(err, "could not get server version from Kubernetes") + } + // Issue #6361: + // Client-Go emits an error when an API service is registered but unimplemented. + // We trap that error here and print a warning. But since the discovery client continues + // building the API object, it is correctly populated with all valid APIs. + // See https://github.com/kubernetes/kubernetes/issues/72051#issuecomment-521157642 + apiVersions, err := GetVersionSet(dc) + if err != nil { + if discovery.IsGroupDiscoveryFailedError(err) { + c.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err) + c.Log("WARNING: To fix this, kubectl delete apiservice ") + } else { + return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes") + } + } + + c.Capabilities = &chartutil.Capabilities{ + APIVersions: apiVersions, + KubeVersion: chartutil.KubeVersion{ + Version: kubeVersion.GitVersion, + Major: kubeVersion.Major, + Minor: kubeVersion.Minor, + }, + } + return c.Capabilities, nil +} + +// KubernetesClientSet creates a new kubernetes ClientSet based on the configuration +func (c *Configuration) KubernetesClientSet() (kubernetes.Interface, error) { + conf, err := c.RESTClientGetter.ToRESTConfig() + if err != nil { + return nil, errors.Wrap(err, "unable to generate config for kubernetes client") + } + + return kubernetes.NewForConfig(conf) +} + +// Now generates a timestamp +// +// If the configuration has a Timestamper on it, that will be used. +// Otherwise, this will use time.Now(). +func (c *Configuration) Now() time.Time { + return Timestamper() +} + +func (c *Configuration) releaseContent(name string, version int) (*release.Release, error) { + if err := chartutil.ValidateReleaseName(name); err != nil { + return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name) + } + + if version <= 0 { + return c.Releases.Last(name) + } + + return c.Releases.Get(name, version) +} + +// GetVersionSet retrieves a set of available k8s API versions +func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.VersionSet, error) { + groups, resources, err := client.ServerGroupsAndResources() + if err != nil && !discovery.IsGroupDiscoveryFailedError(err) { + return chartutil.DefaultVersionSet, errors.Wrap(err, "could not get apiVersions from Kubernetes") + } + + // FIXME: The Kubernetes test fixture for cli appears to always return nil + // for calls to Discovery().ServerGroupsAndResources(). So in this case, we + // return the default API list. This is also a safe value to return in any + // other odd-ball case. + if len(groups) == 0 && len(resources) == 0 { + return chartutil.DefaultVersionSet, nil + } + + versionMap := make(map[string]interface{}) + versions := []string{} + + // Extract the groups + for _, g := range groups { + for _, gv := range g.Versions { + versionMap[gv.GroupVersion] = struct{}{} + } + } + + // Extract the resources + var id string + var ok bool + for _, r := range resources { + for _, rl := range r.APIResources { + + // A Kind at a GroupVersion can show up more than once. We only want + // it displayed once in the final output. + id = path.Join(r.GroupVersion, rl.Kind) + if _, ok = versionMap[id]; !ok { + versionMap[id] = struct{}{} + } + } + } + + // Convert to a form that NewVersionSet can use + for k := range versionMap { + versions = append(versions, k) + } + + return chartutil.VersionSet(versions), nil +} + +// recordRelease with an update operation in case reuse has been set. +func (c *Configuration) recordRelease(r *release.Release) { + if err := c.Releases.Update(r); err != nil { + c.Log("warning: Failed to update release %s: %s", r.Name, err) + } +} + +// Init initializes the action configuration +func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error { + kc := kube.New(getter) + kc.Log = log + + lazyClient := &lazyClient{ + namespace: namespace, + clientFn: kc.Factory.KubernetesClientSet, + } + + var store *storage.Storage + switch helmDriver { + case "secret", "secrets", "": + d := driver.NewSecrets(newSecretClient(lazyClient)) + d.Log = log + store = storage.Init(d) + case "configmap", "configmaps": + d := driver.NewConfigMaps(newConfigMapClient(lazyClient)) + d.Log = log + store = storage.Init(d) + case "memory": + var d *driver.Memory + if c.Releases != nil { + if mem, ok := c.Releases.Driver.(*driver.Memory); ok { + // This function can be called more than once (e.g., helm list --all-namespaces). + // If a memory driver was already initialized, re-use it but set the possibly new namespace. + // We re-use it in case some releases where already created in the existing memory driver. + d = mem + } + } + if d == nil { + d = driver.NewMemory() + } + d.SetNamespace(namespace) + store = storage.Init(d) + case "sql": + d, err := driver.NewSQL( + os.Getenv("HELM_DRIVER_SQL_CONNECTION_STRING"), + log, + namespace, + ) + if err != nil { + panic(fmt.Sprintf("Unable to instantiate SQL driver: %v", err)) + } + store = storage.Init(d) + default: + // Not sure what to do here. + panic("Unknown driver in HELM_DRIVER: " + helmDriver) + } + + c.RESTClientGetter = getter + c.KubeClient = kc + c.Releases = store + c.Log = log + + return nil +} diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go new file mode 100644 index 00000000000..fedf260fbca --- /dev/null +++ b/pkg/action/action_test.go @@ -0,0 +1,321 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "context" + "flag" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "testing" + + dockerauth "github.com/deislabs/oras/pkg/auth/docker" + fakeclientset "k8s.io/client-go/kubernetes/fake" + + "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage" + "helm.sh/helm/v3/pkg/storage/driver" + "helm.sh/helm/v3/pkg/time" +) + +var verbose = flag.Bool("test.log", false, "enable test logging") + +func actionConfigFixture(t *testing.T) *Configuration { + t.Helper() + + client, err := dockerauth.NewClient() + if err != nil { + t.Fatal(err) + } + + resolver, err := client.Resolver(context.Background(), http.DefaultClient, false) + if err != nil { + t.Fatal(err) + } + + tdir, err := ioutil.TempDir("", "helm-action-test") + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { os.RemoveAll(tdir) }) + + cache, err := registry.NewCache( + registry.CacheOptDebug(true), + registry.CacheOptRoot(filepath.Join(tdir, registry.CacheRootDir)), + ) + if err != nil { + t.Fatal(err) + } + + registryClient, err := registry.NewClient( + registry.ClientOptAuthorizer(®istry.Authorizer{ + Client: client, + }), + registry.ClientOptResolver(®istry.Resolver{ + Resolver: resolver, + }), + registry.ClientOptCache(cache), + ) + if err != nil { + t.Fatal(err) + } + + return &Configuration{ + Releases: storage.Init(driver.NewMemory()), + KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: ioutil.Discard}}, + Capabilities: chartutil.DefaultCapabilities, + RegistryClient: registryClient, + Log: func(format string, v ...interface{}) { + t.Helper() + if *verbose { + t.Logf(format, v...) + } + }, + } +} + +var manifestWithHook = `kind: ConfigMap +metadata: + name: test-cm + annotations: + "helm.sh/hook": post-install,pre-delete,post-upgrade +data: + name: value` + +var manifestWithTestHook = `kind: Pod + metadata: + name: finding-nemo, + annotations: + "helm.sh/hook": test + spec: + containers: + - name: nemo-test + image: fake-image + cmd: fake-command + ` + +var rbacManifests = `apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: schedule-agents +rules: +- apiGroups: [""] + resources: ["pods", "pods/exec", "pods/log"] + verbs: ["*"] + +--- + +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: schedule-agents + namespace: {{ default .Release.Namespace}} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: schedule-agents +subjects: +- kind: ServiceAccount + name: schedule-agents + namespace: {{ .Release.Namespace }} +` + +type chartOptions struct { + *chart.Chart +} + +type chartOption func(*chartOptions) + +func buildChart(opts ...chartOption) *chart.Chart { + c := &chartOptions{ + Chart: &chart.Chart{ + // TODO: This should be more complete. + Metadata: &chart.Metadata{ + APIVersion: "v1", + Name: "hello", + Version: "0.1.0", + }, + // This adds a basic template and hooks. + Templates: []*chart.File{ + {Name: "templates/hello", Data: []byte("hello: world")}, + {Name: "templates/hooks", Data: []byte(manifestWithHook)}, + }, + }, + } + + for _, opt := range opts { + opt(c) + } + + return c.Chart +} + +func withName(name string) chartOption { + return func(opts *chartOptions) { + opts.Metadata.Name = name + } +} + +func withSampleValues() chartOption { + values := map[string]interface{}{ + "someKey": "someValue", + "nestedKey": map[string]interface{}{ + "simpleKey": "simpleValue", + "anotherNestedKey": map[string]interface{}{ + "yetAnotherNestedKey": map[string]interface{}{ + "youReadyForAnotherNestedKey": "No", + }, + }, + }, + } + return func(opts *chartOptions) { + opts.Values = values + } +} + +func withValues(values map[string]interface{}) chartOption { + return func(opts *chartOptions) { + opts.Values = values + } +} + +func withNotes(notes string) chartOption { + return func(opts *chartOptions) { + opts.Templates = append(opts.Templates, &chart.File{ + Name: "templates/NOTES.txt", + Data: []byte(notes), + }) + } +} + +func withDependency(dependencyOpts ...chartOption) chartOption { + return func(opts *chartOptions) { + opts.AddDependency(buildChart(dependencyOpts...)) + } +} + +func withMetadataDependency(dependency chart.Dependency) chartOption { + return func(opts *chartOptions) { + opts.Metadata.Dependencies = append(opts.Metadata.Dependencies, &dependency) + } +} + +func withSampleTemplates() chartOption { + return func(opts *chartOptions) { + sampleTemplates := []*chart.File{ + // This adds basic templates and partials. + {Name: "templates/goodbye", Data: []byte("goodbye: world")}, + {Name: "templates/empty", Data: []byte("")}, + {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, + {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, + } + opts.Templates = append(opts.Templates, sampleTemplates...) + } +} + +func withSampleIncludingIncorrectTemplates() chartOption { + return func(opts *chartOptions) { + sampleTemplates := []*chart.File{ + // This adds basic templates and partials. + {Name: "templates/goodbye", Data: []byte("goodbye: world")}, + {Name: "templates/empty", Data: []byte("")}, + {Name: "templates/incorrect", Data: []byte("{{ .Values.bad.doh }}")}, + {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, + {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, + } + opts.Templates = append(opts.Templates, sampleTemplates...) + } +} + +func withMultipleManifestTemplate() chartOption { + return func(opts *chartOptions) { + sampleTemplates := []*chart.File{ + {Name: "templates/rbac", Data: []byte(rbacManifests)}, + } + opts.Templates = append(opts.Templates, sampleTemplates...) + } +} + +func withKube(version string) chartOption { + return func(opts *chartOptions) { + opts.Metadata.KubeVersion = version + } +} + +// releaseStub creates a release stub, complete with the chartStub as its chart. +func releaseStub() *release.Release { + return namedReleaseStub("angry-panda", release.StatusDeployed) +} + +func namedReleaseStub(name string, status release.Status) *release.Release { + now := time.Now() + return &release.Release{ + Name: name, + Info: &release.Info{ + FirstDeployed: now, + LastDeployed: now, + Status: status, + Description: "Named Release Stub", + }, + Chart: buildChart(withSampleTemplates()), + Config: map[string]interface{}{"name": "value"}, + Version: 1, + Hooks: []*release.Hook{ + { + Name: "test-cm", + Kind: "ConfigMap", + Path: "test-cm", + Manifest: manifestWithHook, + Events: []release.HookEvent{ + release.HookPostInstall, + release.HookPreDelete, + }, + }, + { + Name: "finding-nemo", + Kind: "Pod", + Path: "finding-nemo", + Manifest: manifestWithTestHook, + Events: []release.HookEvent{ + release.HookTest, + }, + }, + }, + } +} + +func TestGetVersionSet(t *testing.T) { + client := fakeclientset.NewSimpleClientset() + + vs, err := GetVersionSet(client.Discovery()) + if err != nil { + t.Error(err) + } + + if !vs.Has("v1") { + t.Errorf("Expected supported versions to at least include v1.") + } + if vs.Has("nosuchversion/v1") { + t.Error("Non-existent version is reported found.") + } +} diff --git a/pkg/action/chart_export.go b/pkg/action/chart_export.go new file mode 100644 index 00000000000..75840d8bce4 --- /dev/null +++ b/pkg/action/chart_export.go @@ -0,0 +1,63 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "fmt" + "io" + "path/filepath" + + "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/pkg/chartutil" +) + +// ChartExport performs a chart export operation. +type ChartExport struct { + cfg *Configuration + + Destination string +} + +// NewChartExport creates a new ChartExport object with the given configuration. +func NewChartExport(cfg *Configuration) *ChartExport { + return &ChartExport{ + cfg: cfg, + } +} + +// Run executes the chart export operation +func (a *ChartExport) Run(out io.Writer, ref string) error { + r, err := registry.ParseReference(ref) + if err != nil { + return err + } + + ch, err := a.cfg.RegistryClient.LoadChart(r) + if err != nil { + return err + } + + // Save the chart to local destination directory + err = chartutil.SaveDir(ch, a.Destination) + if err != nil { + return err + } + + d := filepath.Join(a.Destination, ch.Metadata.Name) + fmt.Fprintf(out, "Exported chart to %s/\n", d) + return nil +} diff --git a/pkg/action/chart_list.go b/pkg/action/chart_list.go new file mode 100644 index 00000000000..db764b3a311 --- /dev/null +++ b/pkg/action/chart_list.go @@ -0,0 +1,38 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "io" +) + +// ChartList performs a chart list operation. +type ChartList struct { + cfg *Configuration +} + +// NewChartList creates a new ChartList object with the given configuration. +func NewChartList(cfg *Configuration) *ChartList { + return &ChartList{ + cfg: cfg, + } +} + +// Run executes the chart list operation +func (a *ChartList) Run(out io.Writer) error { + return a.cfg.RegistryClient.PrintChartTable() +} diff --git a/pkg/action/chart_pull.go b/pkg/action/chart_pull.go new file mode 100644 index 00000000000..89675520136 --- /dev/null +++ b/pkg/action/chart_pull.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "io" + + "helm.sh/helm/v3/internal/experimental/registry" +) + +// ChartPull performs a chart pull operation. +type ChartPull struct { + cfg *Configuration +} + +// NewChartPull creates a new ChartPull object with the given configuration. +func NewChartPull(cfg *Configuration) *ChartPull { + return &ChartPull{ + cfg: cfg, + } +} + +// Run executes the chart pull operation +func (a *ChartPull) Run(out io.Writer, ref string) error { + r, err := registry.ParseReference(ref) + if err != nil { + return err + } + return a.cfg.RegistryClient.PullChartToCache(r) +} diff --git a/pkg/action/chart_push.go b/pkg/action/chart_push.go new file mode 100644 index 00000000000..91ec49d3880 --- /dev/null +++ b/pkg/action/chart_push.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "io" + + "helm.sh/helm/v3/internal/experimental/registry" +) + +// ChartPush performs a chart push operation. +type ChartPush struct { + cfg *Configuration +} + +// NewChartPush creates a new ChartPush object with the given configuration. +func NewChartPush(cfg *Configuration) *ChartPush { + return &ChartPush{ + cfg: cfg, + } +} + +// Run executes the chart push operation +func (a *ChartPush) Run(out io.Writer, ref string) error { + r, err := registry.ParseReference(ref) + if err != nil { + return err + } + return a.cfg.RegistryClient.PushChart(r) +} diff --git a/pkg/action/chart_remove.go b/pkg/action/chart_remove.go new file mode 100644 index 00000000000..3c0fc2ed75d --- /dev/null +++ b/pkg/action/chart_remove.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "io" + + "helm.sh/helm/v3/internal/experimental/registry" +) + +// ChartRemove performs a chart remove operation. +type ChartRemove struct { + cfg *Configuration +} + +// NewChartRemove creates a new ChartRemove object with the given configuration. +func NewChartRemove(cfg *Configuration) *ChartRemove { + return &ChartRemove{ + cfg: cfg, + } +} + +// Run executes the chart remove operation +func (a *ChartRemove) Run(out io.Writer, ref string) error { + r, err := registry.ParseReference(ref) + if err != nil { + return err + } + return a.cfg.RegistryClient.RemoveChart(r) +} diff --git a/pkg/action/chart_save.go b/pkg/action/chart_save.go new file mode 100644 index 00000000000..14a2d7c3c1c --- /dev/null +++ b/pkg/action/chart_save.go @@ -0,0 +1,51 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "io" + + "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/pkg/chart" +) + +// ChartSave performs a chart save operation. +type ChartSave struct { + cfg *Configuration +} + +// NewChartSave creates a new ChartSave object with the given configuration. +func NewChartSave(cfg *Configuration) *ChartSave { + return &ChartSave{ + cfg: cfg, + } +} + +// Run executes the chart save operation +func (a *ChartSave) Run(out io.Writer, ch *chart.Chart, ref string) error { + r, err := registry.ParseReference(ref) + if err != nil { + return err + } + + // If no tag is present, use the chart version + if r.Tag == "" { + r.Tag = ch.Metadata.Version + } + + return a.cfg.RegistryClient.SaveChart(ch, r) +} diff --git a/pkg/action/chart_save_test.go b/pkg/action/chart_save_test.go new file mode 100644 index 00000000000..4fd991a4e01 --- /dev/null +++ b/pkg/action/chart_save_test.go @@ -0,0 +1,70 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "io/ioutil" + "testing" + + "helm.sh/helm/v3/internal/experimental/registry" +) + +func chartSaveAction(t *testing.T) *ChartSave { + t.Helper() + config := actionConfigFixture(t) + action := NewChartSave(config) + return action +} + +func TestChartSave(t *testing.T) { + action := chartSaveAction(t) + + input := buildChart() + if err := action.Run(ioutil.Discard, input, "localhost:5000/test:0.2.0"); err != nil { + t.Error(err) + } + + ref, err := registry.ParseReference("localhost:5000/test:0.2.0") + if err != nil { + t.Fatal(err) + } + + if _, err := action.cfg.RegistryClient.LoadChart(ref); err != nil { + t.Error(err) + } + + // now let's check if `helm chart save` can use the chart version when the tag is not present + if err := action.Run(ioutil.Discard, input, "localhost:5000/test"); err != nil { + t.Error(err) + } + + ref, err = registry.ParseReference("localhost:5000/test") + if err != nil { + t.Fatal(err) + } + + // TODO: guess latest based on semver? + _, err = action.cfg.RegistryClient.LoadChart(ref) + if err == nil { + t.Error("Expected error parsing ref without tag") + } + + ref.Tag = "0.1.0" + if _, err := action.cfg.RegistryClient.LoadChart(ref); err != nil { + t.Error(err) + } +} diff --git a/pkg/action/dependency.go b/pkg/action/dependency.go new file mode 100644 index 00000000000..578a46aecbd --- /dev/null +++ b/pkg/action/dependency.go @@ -0,0 +1,227 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/gosuri/uitable" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" +) + +// Dependency is the action for building a given chart's dependency tree. +// +// It provides the implementation of 'helm dependency' and its respective subcommands. +type Dependency struct { + Verify bool + Keyring string + SkipRefresh bool +} + +// NewDependency creates a new Dependency object with the given configuration. +func NewDependency() *Dependency { + return &Dependency{} +} + +// List executes 'helm dependency list'. +func (d *Dependency) List(chartpath string, out io.Writer) error { + c, err := loader.Load(chartpath) + if err != nil { + return err + } + + if c.Metadata.Dependencies == nil { + fmt.Fprintf(out, "WARNING: no dependencies at %s\n", filepath.Join(chartpath, "charts")) + return nil + } + + d.printDependencies(chartpath, out, c) + fmt.Fprintln(out) + d.printMissing(chartpath, out, c.Metadata.Dependencies) + return nil +} + +// dependencyStatus returns a string describing the status of a dependency viz a viz the parent chart. +func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, parent *chart.Chart) string { + filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") + + // If a chart is unpacked, this will check the unpacked chart's `charts/` directory for tarballs. + // Technically, this is COMPLETELY unnecessary, and should be removed in Helm 4. It is here + // to preserved backward compatibility. In Helm 2/3, there is a "difference" between + // the tgz version (which outputs "ok" if it unpacks) and the loaded version (which outputs + // "unpacked"). Early in Helm 2's history, this would have made a difference. But it no + // longer does. However, since this code shipped with Helm 3, the output must remain stable + // until Helm 4. + switch archives, err := filepath.Glob(filepath.Join(chartpath, "charts", filename)); { + case err != nil: + return "bad pattern" + case len(archives) > 1: + // See if the second part is a SemVer + found := []string{} + for _, arc := range archives { + // we need to trip the prefix dirs and the extension off. + filename = strings.TrimSuffix(filepath.Base(arc), ".tgz") + maybeVersion := strings.TrimPrefix(filename, fmt.Sprintf("%s-", dep.Name)) + + if _, err := semver.StrictNewVersion(maybeVersion); err == nil { + // If the version parsed without an error, it is possibly a valid + // version. + found = append(found, arc) + } + } + + if l := len(found); l == 1 { + // If we get here, we do the same thing as in len(archives) == 1. + if r := statArchiveForStatus(found[0], dep); r != "" { + return r + } + + // Fall through and look for directories + } else if l > 1 { + return "too many matches" + } + + // The sanest thing to do here is to fall through and see if we have any directory + // matches. + + case len(archives) == 1: + archive := archives[0] + if r := statArchiveForStatus(archive, dep); r != "" { + return r + } + + } + // End unnecessary code. + + var depChart *chart.Chart + for _, item := range parent.Dependencies() { + if item.Name() == dep.Name { + depChart = item + } + } + + if depChart == nil { + return "missing" + } + + if depChart.Metadata.Version != dep.Version { + constraint, err := semver.NewConstraint(dep.Version) + if err != nil { + return "invalid version" + } + + v, err := semver.NewVersion(depChart.Metadata.Version) + if err != nil { + return "invalid version" + } + + if !constraint.Check(v) { + return "wrong version" + } + } + + return "unpacked" +} + +// stat an archive and return a message if the stat is successful +// +// This is a refactor of the code originally in dependencyStatus. It is here to +// support legacy behavior, and should be removed in Helm 4. +func statArchiveForStatus(archive string, dep *chart.Dependency) string { + if _, err := os.Stat(archive); err == nil { + c, err := loader.Load(archive) + if err != nil { + return "corrupt" + } + if c.Name() != dep.Name { + return "misnamed" + } + + if c.Metadata.Version != dep.Version { + constraint, err := semver.NewConstraint(dep.Version) + if err != nil { + return "invalid version" + } + + v, err := semver.NewVersion(c.Metadata.Version) + if err != nil { + return "invalid version" + } + + if !constraint.Check(v) { + return "wrong version" + } + } + return "ok" + } + return "" +} + +// printDependencies prints all of the dependencies in the yaml file. +func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart.Chart) { + table := uitable.New() + table.MaxColWidth = 80 + table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") + for _, row := range c.Metadata.Dependencies { + table.AddRow(row.Name, row.Version, row.Repository, d.dependencyStatus(chartpath, row, c)) + } + fmt.Fprintln(out, table) +} + +// printMissing prints warnings about charts that are present on disk, but are +// not in Charts.yaml. +func (d *Dependency) printMissing(chartpath string, out io.Writer, reqs []*chart.Dependency) { + folder := filepath.Join(chartpath, "charts/*") + files, err := filepath.Glob(folder) + if err != nil { + fmt.Fprintln(out, err) + return + } + + for _, f := range files { + fi, err := os.Stat(f) + if err != nil { + fmt.Fprintf(out, "Warning: %s\n", err) + } + // Skip anything that is not a directory and not a tgz file. + if !fi.IsDir() && filepath.Ext(f) != ".tgz" { + continue + } + c, err := loader.Load(f) + if err != nil { + fmt.Fprintf(out, "WARNING: %q is not a chart.\n", f) + continue + } + found := false + for _, d := range reqs { + if d.Name == c.Name() { + found = true + break + } + } + if !found { + fmt.Fprintf(out, "WARNING: %q is not in Chart.yaml.\n", f) + } + } +} diff --git a/pkg/action/dependency_test.go b/pkg/action/dependency_test.go new file mode 100644 index 00000000000..b5032a377cc --- /dev/null +++ b/pkg/action/dependency_test.go @@ -0,0 +1,161 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v3/internal/test" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" +) + +func TestList(t *testing.T) { + for _, tcase := range []struct { + chart string + golden string + }{ + { + chart: "testdata/charts/chart-with-compressed-dependencies", + golden: "output/list-compressed-deps.txt", + }, + { + chart: "testdata/charts/chart-with-compressed-dependencies-2.1.8.tgz", + golden: "output/list-compressed-deps-tgz.txt", + }, + { + chart: "testdata/charts/chart-with-uncompressed-dependencies", + golden: "output/list-uncompressed-deps.txt", + }, + { + chart: "testdata/charts/chart-with-uncompressed-dependencies-2.1.8.tgz", + golden: "output/list-uncompressed-deps-tgz.txt", + }, + { + chart: "testdata/charts/chart-missing-deps", + golden: "output/list-missing-deps.txt", + }, + } { + buf := bytes.Buffer{} + if err := NewDependency().List(tcase.chart, &buf); err != nil { + t.Fatal(err) + } + test.AssertGoldenBytes(t, buf.Bytes(), tcase.golden) + } +} + +// TestDependencyStatus_Dashes is a regression test to make sure that dashes in +// chart names do not cause resolution problems. +func TestDependencyStatus_Dashes(t *testing.T) { + // Make a temp dir + dir, err := ioutil.TempDir("", "helmtest-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + chartpath := filepath.Join(dir, "charts") + if err := os.MkdirAll(chartpath, 0700); err != nil { + t.Fatal(err) + } + + // Add some fake charts + first := buildChart(withName("first-chart")) + _, err = chartutil.Save(first, chartpath) + if err != nil { + t.Fatal(err) + } + + second := buildChart(withName("first-chart-second-chart")) + _, err = chartutil.Save(second, chartpath) + if err != nil { + t.Fatal(err) + } + + dep := &chart.Dependency{ + Name: "first-chart", + Version: "0.1.0", + } + + // Now try to get the deps + stat := NewDependency().dependencyStatus(dir, dep, first) + if stat != "ok" { + t.Errorf("Unexpected status: %q", stat) + } +} + +func TestStatArchiveForStatus(t *testing.T) { + // Make a temp dir + dir, err := ioutil.TempDir("", "helmtest-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + chartpath := filepath.Join(dir, "charts") + if err := os.MkdirAll(chartpath, 0700); err != nil { + t.Fatal(err) + } + + // unsaved chart + lilith := buildChart(withName("lilith")) + + // dep referring to chart + dep := &chart.Dependency{ + Name: "lilith", + Version: "1.2.3", + } + + is := assert.New(t) + + lilithpath := filepath.Join(chartpath, "lilith-1.2.3.tgz") + is.Empty(statArchiveForStatus(lilithpath, dep)) + + // save the chart (version 0.1.0, because that is the default) + where, err := chartutil.Save(lilith, chartpath) + is.NoError(err) + + // Should get "wrong version" because we asked for 1.2.3 and got 0.1.0 + is.Equal("wrong version", statArchiveForStatus(where, dep)) + + // Break version on dep + dep = &chart.Dependency{ + Name: "lilith", + Version: "1.2.3.4.5", + } + is.Equal("invalid version", statArchiveForStatus(where, dep)) + + // Break the name + dep = &chart.Dependency{ + Name: "lilith2", + Version: "1.2.3", + } + is.Equal("misnamed", statArchiveForStatus(where, dep)) + + // Now create the right version + dep = &chart.Dependency{ + Name: "lilith", + Version: "0.1.0", + } + is.Equal("ok", statArchiveForStatus(where, dep)) +} diff --git a/pkg/action/doc.go b/pkg/action/doc.go new file mode 100644 index 00000000000..3c91bd61848 --- /dev/null +++ b/pkg/action/doc.go @@ -0,0 +1,22 @@ +/* +Copyright The Helm Authors. + +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 action contains the logic for each action that Helm can perform. +// +// This is a library for calling top-level Helm actions like 'install', +// 'upgrade', or 'list'. Actions approximately match the command line +// invocations that the Helm client uses. +package action diff --git a/pkg/action/get.go b/pkg/action/get.go new file mode 100644 index 00000000000..f44b53307fb --- /dev/null +++ b/pkg/action/get.go @@ -0,0 +1,47 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "helm.sh/helm/v3/pkg/release" +) + +// Get is the action for checking a given release's information. +// +// It provides the implementation of 'helm get' and its respective subcommands (except `helm get values`). +type Get struct { + cfg *Configuration + + // Initializing Version to 0 will get the latest revision of the release. + Version int +} + +// NewGet creates a new Get object with the given configuration. +func NewGet(cfg *Configuration) *Get { + return &Get{ + cfg: cfg, + } +} + +// Run executes 'helm get' against the given release. +func (g *Get) Run(name string) (*release.Release, error) { + if err := g.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + + return g.cfg.releaseContent(name, g.Version) +} diff --git a/pkg/action/get_values.go b/pkg/action/get_values.go new file mode 100644 index 00000000000..9c32db21328 --- /dev/null +++ b/pkg/action/get_values.go @@ -0,0 +1,60 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "helm.sh/helm/v3/pkg/chartutil" +) + +// GetValues is the action for checking a given release's values. +// +// It provides the implementation of 'helm get values'. +type GetValues struct { + cfg *Configuration + + Version int + AllValues bool +} + +// NewGetValues creates a new GetValues object with the given configuration. +func NewGetValues(cfg *Configuration) *GetValues { + return &GetValues{ + cfg: cfg, + } +} + +// Run executes 'helm get values' against the given release. +func (g *GetValues) Run(name string) (map[string]interface{}, error) { + if err := g.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + + rel, err := g.cfg.releaseContent(name, g.Version) + if err != nil { + return nil, err + } + + // If the user wants all values, compute the values and return. + if g.AllValues { + cfg, err := chartutil.CoalesceValues(rel.Chart, rel.Config) + if err != nil { + return nil, err + } + return cfg, nil + } + return rel.Config, nil +} diff --git a/pkg/action/history.go b/pkg/action/history.go new file mode 100644 index 00000000000..0430aaf7a5d --- /dev/null +++ b/pkg/action/history.go @@ -0,0 +1,58 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/release" +) + +// History is the action for checking the release's ledger. +// +// It provides the implementation of 'helm history'. +// It returns all the revisions for a specific release. +// To list up to one revision of every release in one specific, or in all, +// namespaces, see the List action. +type History struct { + cfg *Configuration + + Max int + Version int +} + +// NewHistory creates a new History object with the given configuration. +func NewHistory(cfg *Configuration) *History { + return &History{ + cfg: cfg, + } +} + +// Run executes 'helm history' against the given release. +func (h *History) Run(name string) ([]*release.Release, error) { + if err := h.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + + if err := chartutil.ValidateReleaseName(name); err != nil { + return nil, errors.Errorf("release name is invalid: %s", name) + } + + h.cfg.Log("getting history for release %s", name) + return h.cfg.Releases.History(name) +} diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go new file mode 100644 index 00000000000..40c1ffdb6dd --- /dev/null +++ b/pkg/action/hooks.go @@ -0,0 +1,151 @@ +/* +Copyright The Helm Authors. +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 action + +import ( + "bytes" + "sort" + "time" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" +) + +// execHook executes all of the hooks for the given hook event. +func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, timeout time.Duration) error { + executingHooks := []*release.Hook{} + + for _, h := range rl.Hooks { + for _, e := range h.Events { + if e == hook { + executingHooks = append(executingHooks, h) + } + } + } + + // hooke are pre-ordered by kind, so keep order stable + sort.Stable(hookByWeight(executingHooks)) + + for _, h := range executingHooks { + // Set default delete policy to before-hook-creation + if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 { + // TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion + // resources. For all other resource types update in place if a + // resource with the same name already exists and is owned by the + // current release. + h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation} + } + + if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil { + return err + } + + resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), true) + if err != nil { + return errors.Wrapf(err, "unable to build kubernetes object for %s hook %s", hook, h.Path) + } + + // Record the time at which the hook was applied to the cluster + h.LastRun = release.HookExecution{ + StartedAt: helmtime.Now(), + Phase: release.HookPhaseRunning, + } + cfg.recordRelease(rl) + + // As long as the implementation of WatchUntilReady does not panic, HookPhaseFailed or HookPhaseSucceeded + // should always be set by this function. If we fail to do that for any reason, then HookPhaseUnknown is + // the most appropriate value to surface. + h.LastRun.Phase = release.HookPhaseUnknown + + // Create hook resources + if _, err := cfg.KubeClient.Create(resources); err != nil { + h.LastRun.CompletedAt = helmtime.Now() + h.LastRun.Phase = release.HookPhaseFailed + return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path) + } + + // Watch hook resources until they have completed + err = cfg.KubeClient.WatchUntilReady(resources, timeout) + // Note the time of success/failure + h.LastRun.CompletedAt = helmtime.Now() + // Mark hook as succeeded or failed + if err != nil { + h.LastRun.Phase = release.HookPhaseFailed + // If a hook is failed, check 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 err := cfg.deleteHookByPolicy(h, release.HookFailed); err != nil { + return err + } + return err + } + h.LastRun.Phase = release.HookPhaseSucceeded + } + + // If all hooks are successful, check 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 { + if err := cfg.deleteHookByPolicy(h, release.HookSucceeded); err != nil { + return err + } + } + + return nil +} + +// hookByWeight is a sorter for hooks +type hookByWeight []*release.Hook + +func (x hookByWeight) Len() int { return len(x) } +func (x hookByWeight) Swap(i, j int) { x[i], x[j] = x[j], x[i] } +func (x hookByWeight) Less(i, j int) bool { + if x[i].Weight == x[j].Weight { + return x[i].Name < x[j].Name + } + return x[i].Weight < x[j].Weight +} + +// deleteHookByPolicy deletes a hook if the hook policy instructs it to +func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy) error { + // Never delete CustomResourceDefinitions; this could cause lots of + // cascading garbage collection. + if h.Kind == "CustomResourceDefinition" { + return nil + } + if hookHasDeletePolicy(h, policy) { + resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), false) + if err != nil { + return errors.Wrapf(err, "unable to build kubernetes object for deleting hook %s", h.Path) + } + _, errs := cfg.KubeClient.Delete(resources) + if len(errs) > 0 { + return errors.New(joinErrors(errs)) + } + } + return nil +} + +// hookHasDeletePolicy 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 hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool { + for _, v := range h.DeletePolicies { + if policy == v { + return true + } + } + return false +} diff --git a/pkg/action/install.go b/pkg/action/install.go new file mode 100644 index 00000000000..4de0b64e68b --- /dev/null +++ b/pkg/action/install.go @@ -0,0 +1,691 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "text/template" + "time" + + "github.com/Masterminds/sprig/v3" + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/resource" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/kube" + kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/postrender" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/releaseutil" + "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v3/pkg/storage" + "helm.sh/helm/v3/pkg/storage/driver" +) + +// releaseNameMaxLen is the maximum length of a release name. +// +// As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for +// charts to add data. Effectively, that gives us 53 chars. +// See https://github.com/helm/helm/issues/1528 +const releaseNameMaxLen = 53 + +// NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine +// but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually +// wants to see this file after rendering in the status command. However, it must be a suffix +// since there can be filepath in front of it. +const notesFileSuffix = "NOTES.txt" + +const defaultDirectoryPermission = 0755 + +// Install performs an installation operation. +type Install struct { + cfg *Configuration + + ChartPathOptions + + ClientOnly bool + CreateNamespace bool + DryRun bool + DisableHooks bool + Replace bool + Wait bool + WaitForJobs bool + Devel bool + DependencyUpdate bool + Timeout time.Duration + Namespace string + ReleaseName string + GenerateName bool + NameTemplate string + Description string + OutputDir string + Atomic bool + SkipCRDs bool + SubNotes bool + DisableOpenAPIValidation bool + IncludeCRDs bool + // APIVersions allows a manual set of supported API Versions to be passed + // (for things like templating). These are ignored if ClientOnly is false + APIVersions chartutil.VersionSet + // Used by helm template to render charts with .Release.IsUpgrade. Ignored if Dry-Run is false + IsUpgrade bool + // Used by helm template to add the release as part of OutputDir path + // OutputDir/ + UseReleaseName bool + PostRenderer postrender.PostRenderer +} + +// ChartPathOptions captures common options used for controlling chart paths +type ChartPathOptions struct { + CaFile string // --ca-file + CertFile string // --cert-file + KeyFile string // --key-file + InsecureSkipTLSverify bool // --insecure-skip-verify + Keyring string // --keyring + Password string // --password + RepoURL string // --repo + Username string // --username + Verify bool // --verify + Version string // --version +} + +// NewInstall creates a new Install object with the given configuration. +func NewInstall(cfg *Configuration) *Install { + return &Install{ + cfg: cfg, + } +} + +func (i *Install) installCRDs(crds []chart.CRD) error { + // We do these one file at a time in the order they were read. + totalItems := []*resource.Info{} + for _, obj := range crds { + // Read in the resources + res, err := i.cfg.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false) + if err != nil { + return errors.Wrapf(err, "failed to install CRD %s", obj.Name) + } + + // Send them to Kube + if _, err := i.cfg.KubeClient.Create(res); err != nil { + // If the error is CRD already exists, continue. + if apierrors.IsAlreadyExists(err) { + crdName := res[0].Name + i.cfg.Log("CRD %s is already present. Skipping.", crdName) + continue + } + return errors.Wrapf(err, "failed to install CRD %s", obj.Name) + } + totalItems = append(totalItems, res...) + } + if len(totalItems) > 0 { + // Invalidate the local cache, since it will not have the new CRDs + // present. + discoveryClient, err := i.cfg.RESTClientGetter.ToDiscoveryClient() + if err != nil { + return err + } + i.cfg.Log("Clearing discovery cache") + discoveryClient.Invalidate() + // Give time for the CRD to be recognized. + + if err := i.cfg.KubeClient.Wait(totalItems, 60*time.Second); err != nil { + return err + } + + // Make sure to force a rebuild of the cache. + discoveryClient.ServerGroups() + } + return nil +} + +// Run executes the installation +// +// If DryRun is set to true, this will prepare the release, but not install it +func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + // Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`) + if !i.ClientOnly { + if err := i.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + } + + if err := i.availableName(); err != nil { + return nil, err + } + + // Pre-install anything in the crd/ directory. We do this before Helm + // contacts the upstream server and builds the capabilities object. + if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 { + // On dry run, bail here + if i.DryRun { + i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.") + } else if err := i.installCRDs(crds); err != nil { + return nil, err + } + } + + if i.ClientOnly { + // Add mock objects in here so it doesn't use Kube API server + // NOTE(bacongobbler): used for `helm template` + i.cfg.Capabilities = chartutil.DefaultCapabilities + i.cfg.Capabilities.APIVersions = append(i.cfg.Capabilities.APIVersions, i.APIVersions...) + i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard} + + mem := driver.NewMemory() + mem.SetNamespace(i.Namespace) + i.cfg.Releases = storage.Init(mem) + } else if !i.ClientOnly && len(i.APIVersions) > 0 { + i.cfg.Log("API Version list given outside of client only mode, this list will be ignored") + } + + if err := chartutil.ProcessDependencies(chrt, vals); err != nil { + return nil, err + } + + // Make sure if Atomic is set, that wait is set as well. This makes it so + // the user doesn't have to specify both + i.Wait = i.Wait || i.Atomic + + caps, err := i.cfg.getCapabilities() + if err != nil { + return nil, err + } + + //special case for helm template --is-upgrade + isUpgrade := i.IsUpgrade && i.DryRun + options := chartutil.ReleaseOptions{ + Name: i.ReleaseName, + Namespace: i.Namespace, + Revision: 1, + IsInstall: !isUpgrade, + IsUpgrade: isUpgrade, + } + valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps) + if err != nil { + return nil, err + } + + rel := i.createRelease(chrt, vals) + + var manifestDoc *bytes.Buffer + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, i.DryRun) + // Even for errors, attach this if available + if manifestDoc != nil { + rel.Manifest = manifestDoc.String() + } + // Check error from render + if err != nil { + rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error())) + // Return a release with partial data so that the client can show debugging information. + return rel, err + } + + // Mark this release as in-progress + rel.SetStatus(release.StatusPendingInstall, "Initial install underway") + + var toBeAdopted kube.ResourceList + resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation) + if err != nil { + return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest") + } + + // It is safe to use "force" here because these are resources currently rendered by the chart. + err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace, true)) + if err != nil { + return nil, err + } + + // Install requires an extra validation step of checking that resources + // don't already exist before we actually create resources. If we continue + // forward and create the release object with resources that already exist, + // we'll end up in a state where we will delete those resources upon + // deleting the release because the manifest will be pointing at that + // resource + if !i.ClientOnly && !isUpgrade && len(resources) > 0 { + toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace) + if err != nil { + return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with install") + } + } + + // Bail out here if it is a dry run + if i.DryRun { + rel.Info.Description = "Dry run complete" + return rel, nil + } + + if i.CreateNamespace { + ns := &v1.Namespace{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Namespace", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: i.Namespace, + Labels: map[string]string{ + "name": i.Namespace, + }, + }, + } + buf, err := yaml.Marshal(ns) + if err != nil { + return nil, err + } + resourceList, err := i.cfg.KubeClient.Build(bytes.NewBuffer(buf), true) + if err != nil { + return nil, err + } + if _, err := i.cfg.KubeClient.Create(resourceList); err != nil && !apierrors.IsAlreadyExists(err) { + return nil, err + } + } + + // If Replace is true, we need to supercede the last release. + if i.Replace { + if err := i.replaceRelease(rel); err != nil { + return nil, err + } + } + + // Store the release in history before continuing (new in Helm 3). We always know + // that this is a create operation. + if err := i.cfg.Releases.Create(rel); err != nil { + // We could try to recover gracefully here, but since nothing has been installed + // yet, this is probably safer than trying to continue when we know storage is + // not working. + return rel, err + } + + // pre-install hooks + if !i.DisableHooks { + if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil { + return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err)) + } + } + + // At this point, we can do the install. Note that before we were detecting whether to + // do an update, but it's not clear whether we WANT to do an update if the re-use is set + // to true, since that is basically an upgrade operation. + if len(toBeAdopted) == 0 && len(resources) > 0 { + if _, err := i.cfg.KubeClient.Create(resources); err != nil { + return i.failRelease(rel, err) + } + } else if len(resources) > 0 { + if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil { + return i.failRelease(rel, err) + } + } + + if i.Wait { + if i.WaitForJobs { + if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil { + return i.failRelease(rel, err) + } + } else { + if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil { + return i.failRelease(rel, err) + } + } + } + + if !i.DisableHooks { + if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil { + return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err)) + } + } + + if len(i.Description) > 0 { + rel.SetStatus(release.StatusDeployed, i.Description) + } else { + rel.SetStatus(release.StatusDeployed, "Install complete") + } + + // This is a tricky case. The release has been created, but the result + // cannot be recorded. The truest thing to tell the user is that the + // release was created. However, the user will not be able to do anything + // further with this release. + // + // One possible strategy would be to do a timed retry to see if we can get + // this stored in the future. + if err := i.recordRelease(rel); err != nil { + i.cfg.Log("failed to record the release: %s", err) + } + + return rel, nil +} + +func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) { + rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) + if i.Atomic { + i.cfg.Log("Install failed and atomic is set, uninstalling release") + uninstall := NewUninstall(i.cfg) + uninstall.DisableHooks = i.DisableHooks + uninstall.KeepHistory = false + uninstall.Timeout = i.Timeout + if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil { + return rel, errors.Wrapf(uninstallErr, "an error occurred while uninstalling the release. original install error: %s", err) + } + return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled due to atomic being set", i.ReleaseName) + } + i.recordRelease(rel) // Ignore the error, since we have another error to deal with. + return rel, err +} + +// availableName tests whether a name is available +// +// Roughly, this will return an error if name is +// +// - empty +// - too long +// - already in use, and not deleted +// - used by a deleted release, and i.Replace is false +func (i *Install) availableName() error { + start := i.ReleaseName + if start == "" { + return errors.New("name is required") + } + + if len(start) > releaseNameMaxLen { + return errors.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen) + } + + if i.DryRun { + return nil + } + + h, err := i.cfg.Releases.History(start) + if err != nil || len(h) < 1 { + return nil + } + releaseutil.Reverse(h, releaseutil.SortByRevision) + rel := h[0] + + if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) { + return nil + } + return errors.New("cannot re-use a name that is still in use") +} + +// createRelease creates a new release object +func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}) *release.Release { + ts := i.cfg.Now() + return &release.Release{ + Name: i.ReleaseName, + Namespace: i.Namespace, + Chart: chrt, + Config: rawVals, + Info: &release.Info{ + FirstDeployed: ts, + LastDeployed: ts, + Status: release.StatusUnknown, + }, + Version: 1, + } +} + +// recordRelease with an update operation in case reuse has been set. +func (i *Install) recordRelease(r *release.Release) error { + // This is a legacy function which has been reduced to a oneliner. Could probably + // refactor it out. + return i.cfg.Releases.Update(r) +} + +// replaceRelease replaces an older release with this one +// +// This allows us to re-use names by superseding an existing release with a new one +func (i *Install) replaceRelease(rel *release.Release) error { + hist, err := i.cfg.Releases.History(rel.Name) + if err != nil || len(hist) == 0 { + // No releases exist for this name, so we can return early + return nil + } + + releaseutil.Reverse(hist, releaseutil.SortByRevision) + last := hist[0] + + // Update version to the next available + rel.Version = last.Version + 1 + + // Do not change the status of a failed release. + if last.Info.Status == release.StatusFailed { + return nil + } + + // For any other status, mark it as superseded and store the old record + last.SetStatus(release.StatusSuperseded, "superseded by new release") + return i.recordRelease(last) +} + +// write the to /. controls if the file is created or content will be appended +func writeToFile(outputDir string, name string, data string, append bool) error { + outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator)) + + err := ensureDirectoryForFile(outfileName) + if err != nil { + return err + } + + f, err := createOrOpenFile(outfileName, append) + if err != nil { + return err + } + + defer f.Close() + + _, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data)) + + if err != nil { + return err + } + + fmt.Printf("wrote %s\n", outfileName) + return nil +} + +func createOrOpenFile(filename string, append bool) (*os.File, error) { + if append { + return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600) + } + return os.Create(filename) +} + +// check if the directory exists to create file. creates if don't exists +func ensureDirectoryForFile(file string) error { + baseDir := path.Dir(file) + _, err := os.Stat(baseDir) + if err != nil && !os.IsNotExist(err) { + return err + } + + return os.MkdirAll(baseDir, defaultDirectoryPermission) +} + +// NameAndChart returns the name and chart that should be used. +// +// This will read the flags and handle name generation if necessary. +func (i *Install) NameAndChart(args []string) (string, string, error) { + flagsNotSet := func() error { + if i.GenerateName { + return errors.New("cannot set --generate-name and also specify a name") + } + if i.NameTemplate != "" { + return errors.New("cannot set --name-template and also specify a name") + } + return nil + } + + if len(args) > 2 { + return args[0], args[1], errors.Errorf("expected at most two arguments, unexpected arguments: %v", strings.Join(args[2:], ", ")) + } + + if len(args) == 2 { + return args[0], args[1], flagsNotSet() + } + + if i.NameTemplate != "" { + name, err := TemplateName(i.NameTemplate) + return name, args[0], err + } + + if i.ReleaseName != "" { + return i.ReleaseName, args[0], nil + } + + if !i.GenerateName { + return "", args[0], errors.New("must either provide a name or specify --generate-name") + } + + base := filepath.Base(args[0]) + if base == "." || base == "" { + base = "chart" + } + // if present, strip out the file extension from the name + if idx := strings.Index(base, "."); idx != -1 { + base = base[0:idx] + } + + return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil +} + +// TemplateName renders a name template, returning the name or an error. +func TemplateName(nameTemplate string) (string, error) { + if nameTemplate == "" { + return "", nil + } + + t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) + if err != nil { + return "", err + } + var b bytes.Buffer + if err := t.Execute(&b, nil); err != nil { + return "", err + } + + return b.String(), nil +} + +// CheckDependencies checks the dependencies for a chart. +func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error { + var missing []string + +OUTER: + for _, r := range reqs { + for _, d := range ch.Dependencies() { + if d.Name() == r.Name { + continue OUTER + } + } + missing = append(missing, r.Name) + } + + if len(missing) > 0 { + return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) + } + return nil +} + +// LocateChart looks for a chart directory in known places, and returns either the full path or an error. +// +// This does not ensure that the chart is well-formed; only that the requested filename exists. +// +// Order of resolution: +// - relative to current working directory +// - if path is absolute or begins with '.', error out here +// - URL +// +// If 'verify' was set on ChartPathOptions, this will attempt to also verify the chart. +func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (string, error) { + name = strings.TrimSpace(name) + version := strings.TrimSpace(c.Version) + + if _, err := os.Stat(name); err == nil { + abs, err := filepath.Abs(name) + if err != nil { + return abs, err + } + if c.Verify { + if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil { + return "", err + } + } + return abs, nil + } + if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { + return name, errors.Errorf("path %q not found", name) + } + + dl := downloader.ChartDownloader{ + Out: os.Stdout, + Keyring: c.Keyring, + Getters: getter.All(settings), + Options: []getter.Option{ + getter.WithBasicAuth(c.Username, c.Password), + getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile), + getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify), + }, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + } + if c.Verify { + dl.Verify = downloader.VerifyAlways + } + if c.RepoURL != "" { + chartURL, err := repo.FindChartInAuthAndTLSRepoURL(c.RepoURL, c.Username, c.Password, name, version, + c.CertFile, c.KeyFile, c.CaFile, c.InsecureSkipTLSverify, getter.All(settings)) + if err != nil { + return "", err + } + name = chartURL + } + + if err := os.MkdirAll(settings.RepositoryCache, 0755); err != nil { + return "", err + } + + filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache) + if err == nil { + lname, err := filepath.Abs(filename) + if err != nil { + return filename, err + } + return lname, nil + } else if settings.Debug { + return filename, err + } + + atVersion := "" + if version != "" { + atVersion = fmt.Sprintf(" at version %q", version) + } + return filename, errors.Errorf("failed to download %q%s (hint: running `helm repo update` may help)", name, atVersion) +} diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go new file mode 100644 index 00000000000..63383d77821 --- /dev/null +++ b/pkg/action/install_test.go @@ -0,0 +1,670 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v3/internal/test" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage/driver" + "helm.sh/helm/v3/pkg/time" +) + +type nameTemplateTestCase struct { + tpl string + expected string + expectedErrorStr string +} + +func installAction(t *testing.T) *Install { + config := actionConfigFixture(t) + instAction := NewInstall(config) + instAction.Namespace = "spaced" + instAction.ReleaseName = "test-install-release" + + return instAction +} + +func TestInstallRelease(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + is.Equal(res.Name, "test-install-release", "Expected release name.") + is.Equal(res.Namespace, "spaced") + + rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + + is.Len(rel.Hooks, 1) + is.Equal(rel.Hooks[0].Manifest, manifestWithHook) + is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall) + is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete") + + is.NotEqual(len(res.Manifest), 0) + is.NotEqual(len(rel.Manifest), 0) + is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") + is.Equal(rel.Info.Description, "Install complete") +} + +func TestInstallReleaseWithValues(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + userVals := map[string]interface{}{ + "nestedKey": map[string]interface{}{ + "simpleKey": "simpleValue", + }, + } + expectedUserValues := map[string]interface{}{ + "nestedKey": map[string]interface{}{ + "simpleKey": "simpleValue", + }, + } + res, err := instAction.Run(buildChart(withSampleValues()), userVals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + is.Equal(res.Name, "test-install-release", "Expected release name.") + is.Equal(res.Namespace, "spaced") + + rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + + is.Len(rel.Hooks, 1) + is.Equal(rel.Hooks[0].Manifest, manifestWithHook) + is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall) + is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete") + + is.NotEqual(len(res.Manifest), 0) + is.NotEqual(len(rel.Manifest), 0) + is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") + is.Equal("Install complete", rel.Info.Description) + is.Equal(expectedUserValues, rel.Config) +} + +func TestInstallReleaseClientOnly(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ClientOnly = true + instAction.Run(buildChart(), nil) // disregard output + + is.Equal(instAction.cfg.Capabilities, chartutil.DefaultCapabilities) + is.Equal(instAction.cfg.KubeClient, &kubefake.PrintingKubeClient{Out: ioutil.Discard}) +} + +func TestInstallRelease_NoName(t *testing.T) { + instAction := installAction(t) + instAction.ReleaseName = "" + vals := map[string]interface{}{} + _, err := instAction.Run(buildChart(), vals) + if err == nil { + t.Fatal("expected failure when no name is specified") + } + assert.Contains(t, err.Error(), "name is required") +} + +func TestInstallRelease_WithNotes(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "with-notes" + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(withNotes("note here")), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + is.Equal(res.Name, "with-notes") + is.Equal(res.Namespace, "spaced") + + rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + is.Len(rel.Hooks, 1) + is.Equal(rel.Hooks[0].Manifest, manifestWithHook) + is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall) + is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete") + is.NotEqual(len(res.Manifest), 0) + is.NotEqual(len(rel.Manifest), 0) + is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") + is.Equal(rel.Info.Description, "Install complete") + + is.Equal(rel.Info.Notes, "note here") +} + +func TestInstallRelease_WithNotesRendered(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "with-notes" + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + + expectedNotes := fmt.Sprintf("got-%s", res.Name) + is.Equal(expectedNotes, rel.Info.Notes) + is.Equal(rel.Info.Description, "Install complete") +} + +func TestInstallRelease_WithChartAndDependencyParentNotes(t *testing.T) { + // Regression: Make sure that the child's notes don't override the parent's + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "with-notes" + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.Equal("with-notes", rel.Name) + is.NoError(err) + is.Equal("parent", rel.Info.Notes) + is.Equal(rel.Info.Description, "Install complete") +} + +func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) { + // Regression: Make sure that the child's notes don't override the parent's + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "with-notes" + instAction.SubNotes = true + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.Equal("with-notes", rel.Name) + is.NoError(err) + // test run can return as either 'parent\nchild' or 'child\nparent' + if !strings.Contains(rel.Info.Notes, "parent") && !strings.Contains(rel.Info.Notes, "child") { + t.Fatalf("Expected 'parent\nchild' or 'child\nparent', got '%s'", rel.Info.Notes) + } + is.Equal(rel.Info.Description, "Install complete") +} + +func TestInstallRelease_DryRun(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.DryRun = true + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(withSampleTemplates()), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world") + is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world") + is.Contains(res.Manifest, "hello: Earth") + is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}") + is.NotContains(res.Manifest, "empty") + + _, err = instAction.cfg.Releases.Get(res.Name, res.Version) + is.Error(err) + is.Len(res.Hooks, 1) + is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run") + is.Equal(res.Info.Description, "Dry run complete") +} + +// Regression test for #7955: Lookup must not connect to Kubernetes on a dry-run. +func TestInstallRelease_DryRun_Lookup(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.DryRun = true + vals := map[string]interface{}{} + + mockChart := buildChart(withSampleTemplates()) + mockChart.Templates = append(mockChart.Templates, &chart.File{ + Name: "templates/lookup", + Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), + }) + + res, err := instAction.Run(mockChart, vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + is.Contains(res.Manifest, "goodbye: map[]") +} + +func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.DryRun = true + vals := map[string]interface{}{} + _, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals) + expectedErr := "\"hello/templates/incorrect\" at <.Values.bad.doh>: nil pointer evaluating interface {}.doh" + if err == nil { + t.Fatalf("Install should fail containing error: %s", expectedErr) + } + if err != nil { + is.Contains(err.Error(), expectedErr) + } +} + +func TestInstallRelease_NoHooks(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.DisableHooks = true + instAction.ReleaseName = "no-hooks" + instAction.cfg.Releases.Create(releaseStub()) + + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "hooks should not run with no-hooks") +} + +func TestInstallRelease_FailedHooks(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "failed-hooks" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WatchUntilReadyError = fmt.Errorf("Failed watch") + instAction.cfg.KubeClient = failer + + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(), vals) + is.Error(err) + is.Contains(res.Info.Description, "failed post-install") + is.Equal(release.StatusFailed, res.Info.Status) +} + +func TestInstallRelease_ReplaceRelease(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.Replace = true + + rel := releaseStub() + rel.Info.Status = release.StatusUninstalled + instAction.cfg.Releases.Create(rel) + instAction.ReleaseName = rel.Name + + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(), vals) + is.NoError(err) + + // This should have been auto-incremented + is.Equal(2, res.Version) + is.Equal(res.Name, rel.Name) + + getres, err := instAction.cfg.Releases.Get(rel.Name, res.Version) + is.NoError(err) + is.Equal(getres.Info.Status, release.StatusDeployed) +} + +func TestInstallRelease_KubeVersion(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + vals := map[string]interface{}{} + _, err := instAction.Run(buildChart(withKube(">=0.0.0")), vals) + is.NoError(err) + + // This should fail for a few hundred years + instAction.ReleaseName = "should-fail" + vals = map[string]interface{}{} + _, err = instAction.Run(buildChart(withKube(">=99.0.0")), vals) + is.Error(err) + is.Contains(err.Error(), "chart requires kubeVersion") +} + +func TestInstallRelease_Wait(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "come-fail-away" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + instAction.cfg.KubeClient = failer + instAction.Wait = true + vals := map[string]interface{}{} + + res, err := instAction.Run(buildChart(), vals) + is.Error(err) + is.Contains(res.Info.Description, "I timed out") + is.Equal(res.Info.Status, release.StatusFailed) +} + +func TestInstallRelease_WaitForJobs(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "come-fail-away" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + instAction.cfg.KubeClient = failer + instAction.Wait = true + instAction.WaitForJobs = true + vals := map[string]interface{}{} + + res, err := instAction.Run(buildChart(), vals) + is.Error(err) + is.Contains(res.Info.Description, "I timed out") + is.Equal(res.Info.Status, release.StatusFailed) +} + +func TestInstallRelease_Atomic(t *testing.T) { + is := assert.New(t) + + t.Run("atomic uninstall succeeds", func(t *testing.T) { + instAction := installAction(t) + instAction.ReleaseName = "come-fail-away" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + instAction.cfg.KubeClient = failer + instAction.Atomic = true + vals := map[string]interface{}{} + + res, err := instAction.Run(buildChart(), vals) + is.Error(err) + is.Contains(err.Error(), "I timed out") + is.Contains(err.Error(), "atomic") + + // Now make sure it isn't in storage any more + _, err = instAction.cfg.Releases.Get(res.Name, res.Version) + is.Error(err) + is.Equal(err, driver.ErrReleaseNotFound) + }) + + t.Run("atomic uninstall fails", func(t *testing.T) { + instAction := installAction(t) + instAction.ReleaseName = "come-fail-away-with-me" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + failer.DeleteError = fmt.Errorf("uninstall fail") + instAction.cfg.KubeClient = failer + instAction.Atomic = true + vals := map[string]interface{}{} + + _, err := instAction.Run(buildChart(), vals) + is.Error(err) + is.Contains(err.Error(), "I timed out") + is.Contains(err.Error(), "uninstall fail") + is.Contains(err.Error(), "an error occurred while uninstalling the release") + }) +} + +func TestNameTemplate(t *testing.T) { + testCases := []nameTemplateTestCase{ + // Just a straight up nop please + { + tpl: "foobar", + expected: "foobar", + expectedErrorStr: "", + }, + // Random numbers at the end for fun & profit + { + tpl: "foobar-{{randNumeric 6}}", + expected: "foobar-[0-9]{6}$", + expectedErrorStr: "", + }, + // Random numbers in the middle for fun & profit + { + tpl: "foobar-{{randNumeric 4}}-baz", + expected: "foobar-[0-9]{4}-baz$", + expectedErrorStr: "", + }, + // No such function + { + tpl: "foobar-{{randInteger}}", + expected: "", + expectedErrorStr: "function \"randInteger\" not defined", + }, + // Invalid template + { + tpl: "foobar-{{", + expected: "", + expectedErrorStr: "template: name-template:1: unclosed action", + }, + } + + for _, tc := range testCases { + + n, err := TemplateName(tc.tpl) + if err != nil { + if tc.expectedErrorStr == "" { + t.Errorf("Was not expecting error, but got: %v", err) + continue + } + re, compErr := regexp.Compile(tc.expectedErrorStr) + if compErr != nil { + t.Errorf("Expected error string failed to compile: %v", compErr) + continue + } + if !re.MatchString(err.Error()) { + t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err) + continue + } + } + if err == nil && tc.expectedErrorStr != "" { + t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr) + } + + if tc.expected != "" { + re, err := regexp.Compile(tc.expected) + if err != nil { + t.Errorf("Expected string failed to compile: %v", err) + continue + } + if !re.MatchString(n) { + t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n) + } + } + } +} + +func TestInstallReleaseOutputDir(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + vals := map[string]interface{}{} + + dir, err := ioutil.TempDir("", "output-dir") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(dir) + + instAction.OutputDir = dir + + _, err = instAction.Run(buildChart(withSampleTemplates(), withMultipleManifestTemplate()), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + _, err = os.Stat(filepath.Join(dir, "hello/templates/goodbye")) + is.NoError(err) + + _, err = os.Stat(filepath.Join(dir, "hello/templates/hello")) + is.NoError(err) + + _, err = os.Stat(filepath.Join(dir, "hello/templates/with-partials")) + is.NoError(err) + + _, err = os.Stat(filepath.Join(dir, "hello/templates/rbac")) + is.NoError(err) + + test.AssertGoldenFile(t, filepath.Join(dir, "hello/templates/rbac"), "rbac.txt") + + _, err = os.Stat(filepath.Join(dir, "hello/templates/empty")) + is.True(os.IsNotExist(err)) +} + +func TestInstallOutputDirWithReleaseName(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + vals := map[string]interface{}{} + + dir, err := ioutil.TempDir("", "output-dir") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(dir) + + instAction.OutputDir = dir + instAction.UseReleaseName = true + instAction.ReleaseName = "madra" + + newDir := filepath.Join(dir, instAction.ReleaseName) + + _, err = instAction.Run(buildChart(withSampleTemplates(), withMultipleManifestTemplate()), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + _, err = os.Stat(filepath.Join(newDir, "hello/templates/goodbye")) + is.NoError(err) + + _, err = os.Stat(filepath.Join(newDir, "hello/templates/hello")) + is.NoError(err) + + _, err = os.Stat(filepath.Join(newDir, "hello/templates/with-partials")) + is.NoError(err) + + _, err = os.Stat(filepath.Join(newDir, "hello/templates/rbac")) + is.NoError(err) + + test.AssertGoldenFile(t, filepath.Join(newDir, "hello/templates/rbac"), "rbac.txt") + + _, err = os.Stat(filepath.Join(newDir, "hello/templates/empty")) + is.True(os.IsNotExist(err)) +} + +func TestNameAndChart(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + chartName := "./foo" + + name, chrt, err := instAction.NameAndChart([]string{chartName}) + if err != nil { + t.Fatal(err) + } + is.Equal(instAction.ReleaseName, name) + is.Equal(chartName, chrt) + + instAction.GenerateName = true + _, _, err = instAction.NameAndChart([]string{"foo", chartName}) + if err == nil { + t.Fatal("expected an error") + } + is.Equal("cannot set --generate-name and also specify a name", err.Error()) + + instAction.GenerateName = false + instAction.NameTemplate = "{{ . }}" + _, _, err = instAction.NameAndChart([]string{"foo", chartName}) + if err == nil { + t.Fatal("expected an error") + } + is.Equal("cannot set --name-template and also specify a name", err.Error()) + + instAction.NameTemplate = "" + instAction.ReleaseName = "" + _, _, err = instAction.NameAndChart([]string{chartName}) + if err == nil { + t.Fatal("expected an error") + } + is.Equal("must either provide a name or specify --generate-name", err.Error()) + + instAction.NameTemplate = "" + instAction.ReleaseName = "" + _, _, err = instAction.NameAndChart([]string{"foo", chartName, "bar"}) + if err == nil { + t.Fatal("expected an error") + } + is.Equal("expected at most two arguments, unexpected arguments: bar", err.Error()) +} + +func TestNameAndChartGenerateName(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + + instAction.ReleaseName = "" + instAction.GenerateName = true + + tests := []struct { + Name string + Chart string + ExpectedName string + }{ + { + "local filepath", + "./chart", + fmt.Sprintf("chart-%d", time.Now().Unix()), + }, + { + "dot filepath", + ".", + fmt.Sprintf("chart-%d", time.Now().Unix()), + }, + { + "empty filepath", + "", + fmt.Sprintf("chart-%d", time.Now().Unix()), + }, + { + "packaged chart", + "chart.tgz", + fmt.Sprintf("chart-%d", time.Now().Unix()), + }, + { + "packaged chart with .tar.gz extension", + "chart.tar.gz", + fmt.Sprintf("chart-%d", time.Now().Unix()), + }, + { + "packaged chart with local extension", + "./chart.tgz", + fmt.Sprintf("chart-%d", time.Now().Unix()), + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + name, chrt, err := instAction.NameAndChart([]string{tc.Chart}) + if err != nil { + t.Fatal(err) + } + + is.Equal(tc.ExpectedName, name) + is.Equal(tc.Chart, chrt) + }) + } +} diff --git a/pkg/action/lazyclient.go b/pkg/action/lazyclient.go new file mode 100644 index 00000000000..0bd57ff5b8f --- /dev/null +++ b/pkg/action/lazyclient.go @@ -0,0 +1,182 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "context" + "sync" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" +) + +// lazyClient is a workaround to deal with Kubernetes having an unstable client API. +// In Kubernetes v1.18 the defaults where removed which broke creating a +// client without an explicit configuration. ➠_➠ +type lazyClient struct { + // client caches an initialized kubernetes client + initClient sync.Once + client kubernetes.Interface + clientErr error + + // clientFn loads a kubernetes client + clientFn func() (*kubernetes.Clientset, error) + + // namespace passed to each client request + namespace string +} + +func (s *lazyClient) init() error { + s.initClient.Do(func() { + s.client, s.clientErr = s.clientFn() + }) + return s.clientErr +} + +// secretClient implements a corev1.SecretsInterface +type secretClient struct{ *lazyClient } + +var _ corev1.SecretInterface = (*secretClient)(nil) + +func newSecretClient(lc *lazyClient) *secretClient { + return &secretClient{lazyClient: lc} +} + +func (s *secretClient) Create(ctx context.Context, secret *v1.Secret, opts metav1.CreateOptions) (*v1.Secret, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).Create(ctx, secret, opts) +} + +func (s *secretClient) Update(ctx context.Context, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).Update(ctx, secret, opts) +} + +func (s *secretClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + if err := s.init(); err != nil { + return err + } + return s.client.CoreV1().Secrets(s.namespace).Delete(ctx, name, opts) +} + +func (s *secretClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + if err := s.init(); err != nil { + return err + } + return s.client.CoreV1().Secrets(s.namespace).DeleteCollection(ctx, opts, listOpts) +} + +func (s *secretClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Secret, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).Get(ctx, name, opts) +} + +func (s *secretClient) List(ctx context.Context, opts metav1.ListOptions) (*v1.SecretList, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).List(ctx, opts) +} + +func (s *secretClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).Watch(ctx, opts) +} + +func (s *secretClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*v1.Secret, error) { + if err := s.init(); err != nil { + return nil, err + } + return s.client.CoreV1().Secrets(s.namespace).Patch(ctx, name, pt, data, opts, subresources...) +} + +// configMapClient implements a corev1.ConfigMapInterface +type configMapClient struct{ *lazyClient } + +var _ corev1.ConfigMapInterface = (*configMapClient)(nil) + +func newConfigMapClient(lc *lazyClient) *configMapClient { + return &configMapClient{lazyClient: lc} +} + +func (c *configMapClient) Create(ctx context.Context, configMap *v1.ConfigMap, opts metav1.CreateOptions) (*v1.ConfigMap, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Create(ctx, configMap, opts) +} + +func (c *configMapClient) Update(ctx context.Context, configMap *v1.ConfigMap, opts metav1.UpdateOptions) (*v1.ConfigMap, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Update(ctx, configMap, opts) +} + +func (c *configMapClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + if err := c.init(); err != nil { + return err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Delete(ctx, name, opts) +} + +func (c *configMapClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error { + if err := c.init(); err != nil { + return err + } + return c.client.CoreV1().ConfigMaps(c.namespace).DeleteCollection(ctx, opts, listOpts) +} + +func (c *configMapClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.ConfigMap, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Get(ctx, name, opts) +} + +func (c *configMapClient) List(ctx context.Context, opts metav1.ListOptions) (*v1.ConfigMapList, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).List(ctx, opts) +} + +func (c *configMapClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Watch(ctx, opts) +} + +func (c *configMapClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*v1.ConfigMap, error) { + if err := c.init(); err != nil { + return nil, err + } + return c.client.CoreV1().ConfigMaps(c.namespace).Patch(ctx, name, pt, data, opts, subresources...) +} diff --git a/pkg/action/lint.go b/pkg/action/lint.go new file mode 100644 index 00000000000..2292c14bf3d --- /dev/null +++ b/pkg/action/lint.go @@ -0,0 +1,118 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/lint" + "helm.sh/helm/v3/pkg/lint/support" +) + +// Lint is the action for checking that the semantics of a chart are well-formed. +// +// It provides the implementation of 'helm lint'. +type Lint struct { + Strict bool + Namespace string + WithSubcharts bool +} + +// LintResult is the result of Lint +type LintResult struct { + TotalChartsLinted int + Messages []support.Message + Errors []error +} + +// NewLint creates a new Lint object with the given configuration. +func NewLint() *Lint { + return &Lint{} +} + +// Run executes 'helm Lint' against the given chart. +func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult { + lowestTolerance := support.ErrorSev + if l.Strict { + lowestTolerance = support.WarningSev + } + result := &LintResult{} + for _, path := range paths { + linter, err := lintChart(path, vals, l.Namespace, l.Strict) + if err != nil { + result.Errors = append(result.Errors, err) + continue + } + + result.Messages = append(result.Messages, linter.Messages...) + result.TotalChartsLinted++ + for _, msg := range linter.Messages { + if msg.Severity >= lowestTolerance { + result.Errors = append(result.Errors, msg.Err) + } + } + } + return result +} + +func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) { + var chartPath string + linter := support.Linter{} + + if strings.HasSuffix(path, ".tgz") || strings.HasSuffix(path, ".tar.gz") { + tempDir, err := ioutil.TempDir("", "helm-lint") + if err != nil { + return linter, errors.Wrap(err, "unable to create temp dir to extract tarball") + } + defer os.RemoveAll(tempDir) + + file, err := os.Open(path) + if err != nil { + return linter, errors.Wrap(err, "unable to open tarball") + } + defer file.Close() + + if err = chartutil.Expand(tempDir, file); err != nil { + return linter, errors.Wrap(err, "unable to extract tarball") + } + + files, err := ioutil.ReadDir(tempDir) + if err != nil { + return linter, errors.Wrapf(err, "unable to read temporary output directory %s", tempDir) + } + if !files[0].IsDir() { + return linter, errors.Errorf("unexpected file %s in temporary output directory %s", files[0].Name(), tempDir) + } + + chartPath = filepath.Join(tempDir, files[0].Name()) + } else { + chartPath = path + } + + // Guard: Error out if this is not a chart. + if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { + return linter, errors.Wrap(err, "unable to check Chart.yaml file in chart") + } + + return lint.All(chartPath, vals, namespace, strict), nil +} diff --git a/pkg/action/lint_test.go b/pkg/action/lint_test.go new file mode 100644 index 00000000000..1828461f30a --- /dev/null +++ b/pkg/action/lint_test.go @@ -0,0 +1,160 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "testing" +) + +var ( + values = make(map[string]interface{}) + namespace = "testNamespace" + strict = false + chart1MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-1" + chart2MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-2" + corruptedTgzChart = "testdata/charts/corrupted-compressed-chart.tgz" + chartWithNoTemplatesDir = "testdata/charts/chart-with-no-templates-dir" +) + +func TestLintChart(t *testing.T) { + tests := []struct { + name string + chartPath string + err bool + }{ + { + name: "decompressed-chart", + chartPath: "testdata/charts/decompressedchart/", + }, + { + name: "archived-chart-path", + chartPath: "testdata/charts/compressedchart-0.1.0.tgz", + }, + { + name: "archived-chart-path-with-hyphens", + chartPath: "testdata/charts/compressedchart-with-hyphens-0.1.0.tgz", + }, + { + name: "archived-tar-gz-chart-path", + chartPath: "testdata/charts/compressedchart-0.1.0.tar.gz", + }, + { + name: "invalid-archived-chart-path", + chartPath: "testdata/charts/invalidcompressedchart0.1.0.tgz", + err: true, + }, + { + name: "chart-missing-manifest", + chartPath: "testdata/charts/chart-missing-manifest", + err: true, + }, + { + name: "chart-with-schema", + chartPath: "testdata/charts/chart-with-schema", + }, + { + name: "chart-with-schema-negative", + chartPath: "testdata/charts/chart-with-schema-negative", + }, + { + name: "pre-release-chart", + chartPath: "testdata/charts/pre-release-chart-0.1.0-alpha.tgz", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := lintChart(tt.chartPath, map[string]interface{}{}, namespace, strict) + switch { + case err != nil && !tt.err: + t.Errorf("%s", err) + case err == nil && tt.err: + t.Errorf("Expected a chart parsing error") + } + }) + } +} + +func TestNonExistentChart(t *testing.T) { + t.Run("should error out for non existent tgz chart", func(t *testing.T) { + testCharts := []string{"non-existent-chart.tgz"} + expectedError := "unable to open tarball: open non-existent-chart.tgz: no such file or directory" + testLint := NewLint() + + result := testLint.Run(testCharts, values) + if len(result.Errors) != 1 { + t.Error("expected one error, but got", len(result.Errors)) + } + + actual := result.Errors[0].Error() + if actual != expectedError { + t.Errorf("expected '%s', but got '%s'", expectedError, actual) + } + }) + + t.Run("should error out for corrupted tgz chart", func(t *testing.T) { + testCharts := []string{corruptedTgzChart} + expectedEOFError := "unable to extract tarball: EOF" + testLint := NewLint() + + result := testLint.Run(testCharts, values) + if len(result.Errors) != 1 { + t.Error("expected one error, but got", len(result.Errors)) + } + + actual := result.Errors[0].Error() + if actual != expectedEOFError { + t.Errorf("expected '%s', but got '%s'", expectedEOFError, actual) + } + }) +} + +func TestLint_MultipleCharts(t *testing.T) { + testCharts := []string{chart2MultipleChartLint, chart1MultipleChartLint} + testLint := NewLint() + if result := testLint.Run(testCharts, values); len(result.Errors) > 0 { + t.Error(result.Errors) + } +} + +func TestLint_EmptyResultErrors(t *testing.T) { + testCharts := []string{chart2MultipleChartLint} + testLint := NewLint() + if result := testLint.Run(testCharts, values); len(result.Errors) > 0 { + t.Error("Expected no error, got more") + } +} + +func TestLint_ChartWithWarnings(t *testing.T) { + t.Run("should pass when not strict", func(t *testing.T) { + testCharts := []string{chartWithNoTemplatesDir} + testLint := NewLint() + testLint.Strict = false + if result := testLint.Run(testCharts, values); len(result.Errors) > 0 { + t.Error("Expected no error, got more") + } + }) + + t.Run("should fail with errors when strict", func(t *testing.T) { + testCharts := []string{chartWithNoTemplatesDir} + testLint := NewLint() + testLint.Strict = true + if result := testLint.Run(testCharts, values); len(result.Errors) != 1 { + t.Error("expected one error, but got", len(result.Errors)) + } + }) +} diff --git a/pkg/action/list.go b/pkg/action/list.go new file mode 100644 index 00000000000..c9e6e364ab0 --- /dev/null +++ b/pkg/action/list.go @@ -0,0 +1,323 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "path" + "regexp" + + "k8s.io/apimachinery/pkg/labels" + + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/releaseutil" +) + +// ListStates represents zero or more status codes that a list item may have set +// +// Because this is used as a bitmask filter, more than one bit can be flipped +// in the ListStates. +type ListStates uint + +const ( + // ListDeployed filters on status "deployed" + ListDeployed ListStates = 1 << iota + // ListUninstalled filters on status "uninstalled" + ListUninstalled + // ListUninstalling filters on status "uninstalling" (uninstall in progress) + ListUninstalling + // ListPendingInstall filters on status "pending" (deployment in progress) + ListPendingInstall + // ListPendingUpgrade filters on status "pending_upgrade" (upgrade in progress) + ListPendingUpgrade + // ListPendingRollback filters on status "pending_rollback" (rollback in progress) + ListPendingRollback + // ListSuperseded filters on status "superseded" (historical release version that is no longer deployed) + ListSuperseded + // ListFailed filters on status "failed" (release version not deployed because of error) + ListFailed + // ListUnknown filters on an unknown status + ListUnknown +) + +// FromName takes a state name and returns a ListStates representation. +// +// Currently, there are only names for individual flipped bits, so the returned +// ListStates will only match one of the constants. However, it is possible that +// this behavior could change in the future. +func (s ListStates) FromName(str string) ListStates { + switch str { + case "deployed": + return ListDeployed + case "uninstalled": + return ListUninstalled + case "superseded": + return ListSuperseded + case "failed": + return ListFailed + case "uninstalling": + return ListUninstalling + case "pending-install": + return ListPendingInstall + case "pending-upgrade": + return ListPendingUpgrade + case "pending-rollback": + return ListPendingRollback + } + return ListUnknown +} + +// ListAll is a convenience for enabling all list filters +const ListAll = ListDeployed | ListUninstalled | ListUninstalling | ListPendingInstall | ListPendingRollback | ListPendingUpgrade | ListSuperseded | ListFailed + +// Sorter is a top-level sort +type Sorter uint + +const ( + // ByNameDesc sorts by descending lexicographic order + ByNameDesc Sorter = iota + 1 + // ByDateAsc sorts by ascending dates (oldest updated release first) + ByDateAsc + // ByDateDesc sorts by descending dates (latest updated release first) + ByDateDesc +) + +// List is the action for listing releases. +// +// It provides, for example, the implementation of 'helm list'. +// It returns no more than one revision of every release in one specific, or in +// all, namespaces. +// To list all the revisions of a specific release, see the History action. +type List struct { + cfg *Configuration + + // All ignores the limit/offset + All bool + // AllNamespaces searches across namespaces + AllNamespaces bool + // Sort indicates the sort to use + // + // see pkg/releaseutil for several useful sorters + Sort Sorter + // Overrides the default lexicographic sorting + ByDate bool + SortReverse bool + // StateMask accepts a bitmask of states for items to show. + // The default is ListDeployed + StateMask ListStates + // Limit is the number of items to return per Run() + Limit int + // Offset is the starting index for the Run() call + Offset int + // Filter is a filter that is applied to the results + Filter string + Short bool + TimeFormat string + Uninstalled bool + Superseded bool + Uninstalling bool + Deployed bool + Failed bool + Pending bool + Selector string +} + +// NewList constructs a new *List +func NewList(cfg *Configuration) *List { + return &List{ + StateMask: ListDeployed | ListFailed, + cfg: cfg, + } +} + +// Run executes the list command, returning a set of matches. +func (l *List) Run() ([]*release.Release, error) { + if err := l.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + + var filter *regexp.Regexp + if l.Filter != "" { + var err error + filter, err = regexp.Compile(l.Filter) + if err != nil { + return nil, err + } + } + + results, err := l.cfg.Releases.List(func(rel *release.Release) bool { + // Skip anything that doesn't match the filter. + if filter != nil && !filter.MatchString(rel.Name) { + return false + } + + return true + }) + + if err != nil { + return nil, err + } + + if results == nil { + return results, nil + } + + // by definition, superseded releases are never shown if + // only the latest releases are returned. so if requested statemask + // is _only_ ListSuperseded, skip the latest release filter + if l.StateMask != ListSuperseded { + results = filterLatestReleases(results) + } + + // State mask application must occur after filtering to + // latest releases, otherwise outdated entries can be returned + results = l.filterStateMask(results) + + // Skip anything that doesn't match the selector + selectorObj, err := labels.Parse(l.Selector) + if err != nil { + return nil, err + } + results = l.filterSelector(results, selectorObj) + + // Unfortunately, we have to sort before truncating, which can incur substantial overhead + l.sort(results) + + // Guard on offset + if l.Offset >= len(results) { + return []*release.Release{}, nil + } + + // Calculate the limit and offset, and then truncate results if necessary. + limit := len(results) + if l.Limit > 0 && l.Limit < limit { + limit = l.Limit + } + last := l.Offset + limit + if l := len(results); l < last { + last = l + } + results = results[l.Offset:last] + + return results, err +} + +// sort is an in-place sort where order is based on the value of a.Sort +func (l *List) sort(rels []*release.Release) { + if l.SortReverse { + l.Sort = ByNameDesc + } + + if l.ByDate { + l.Sort = ByDateDesc + if l.SortReverse { + l.Sort = ByDateAsc + } + } + + switch l.Sort { + case ByDateDesc: + releaseutil.SortByDate(rels) + case ByDateAsc: + releaseutil.Reverse(rels, releaseutil.SortByDate) + case ByNameDesc: + releaseutil.Reverse(rels, releaseutil.SortByName) + default: + releaseutil.SortByName(rels) + } +} + +// filterLatestReleases returns a list scrubbed of old releases. +func filterLatestReleases(releases []*release.Release) []*release.Release { + latestReleases := make(map[string]*release.Release) + + for _, rls := range releases { + name, namespace := rls.Name, rls.Namespace + key := path.Join(namespace, name) + if latestRelease, exists := latestReleases[key]; exists && latestRelease.Version > rls.Version { + continue + } + latestReleases[key] = rls + } + + var list = make([]*release.Release, 0, len(latestReleases)) + for _, rls := range latestReleases { + list = append(list, rls) + } + return list +} + +func (l *List) filterStateMask(releases []*release.Release) []*release.Release { + desiredStateReleases := make([]*release.Release, 0) + + for _, rls := range releases { + currentStatus := l.StateMask.FromName(rls.Info.Status.String()) + mask := l.StateMask & currentStatus + if mask == 0 { + continue + } + desiredStateReleases = append(desiredStateReleases, rls) + } + + return desiredStateReleases +} + +func (l *List) filterSelector(releases []*release.Release, selector labels.Selector) []*release.Release { + desiredStateReleases := make([]*release.Release, 0) + + for _, rls := range releases { + if selector.Matches(labels.Set(rls.Labels)) { + desiredStateReleases = append(desiredStateReleases, rls) + } + } + + return desiredStateReleases +} + +// SetStateMask calculates the state mask based on parameters. +func (l *List) SetStateMask() { + if l.All { + l.StateMask = ListAll + return + } + + state := ListStates(0) + if l.Deployed { + state |= ListDeployed + } + if l.Uninstalled { + state |= ListUninstalled + } + if l.Uninstalling { + state |= ListUninstalling + } + if l.Pending { + state |= ListPendingInstall | ListPendingRollback | ListPendingUpgrade + } + if l.Failed { + state |= ListFailed + } + if l.Superseded { + state |= ListSuperseded + } + + // Apply a default + if state == 0 { + state = ListDeployed | ListFailed + } + + l.StateMask = state +} diff --git a/pkg/action/list_test.go b/pkg/action/list_test.go new file mode 100644 index 00000000000..73009d52341 --- /dev/null +++ b/pkg/action/list_test.go @@ -0,0 +1,368 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage" +) + +func TestListStates(t *testing.T) { + for input, expect := range map[string]ListStates{ + "deployed": ListDeployed, + "uninstalled": ListUninstalled, + "uninstalling": ListUninstalling, + "superseded": ListSuperseded, + "failed": ListFailed, + "pending-install": ListPendingInstall, + "pending-rollback": ListPendingRollback, + "pending-upgrade": ListPendingUpgrade, + "unknown": ListUnknown, + "totally made up key": ListUnknown, + } { + if expect != expect.FromName(input) { + t.Errorf("Expected %d for %s", expect, input) + } + // This is a cheap way to verify that ListAll actually allows everything but Unknown + if got := expect.FromName(input); got != ListUnknown && got&ListAll == 0 { + t.Errorf("Expected %s to match the ListAll filter", input) + } + } + + filter := ListDeployed | ListPendingRollback + if status := filter.FromName("deployed"); filter&status == 0 { + t.Errorf("Expected %d to match mask %d", status, filter) + } + if status := filter.FromName("failed"); filter&status != 0 { + t.Errorf("Expected %d to fail to match mask %d", status, filter) + } +} + +func TestList_Empty(t *testing.T) { + lister := NewList(actionConfigFixture(t)) + list, err := lister.Run() + assert.NoError(t, err) + assert.Len(t, list, 0) +} + +func newListFixture(t *testing.T) *List { + return NewList(actionConfigFixture(t)) +} + +func TestList_OneNamespace(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + makeMeSomeReleases(lister.cfg.Releases, t) + list, err := lister.Run() + is.NoError(err) + is.Len(list, 3) +} + +func TestList_AllNamespaces(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + makeMeSomeReleases(lister.cfg.Releases, t) + lister.AllNamespaces = true + lister.SetStateMask() + list, err := lister.Run() + is.NoError(err) + is.Len(list, 3) +} + +func TestList_Sort(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + lister.Sort = ByNameDesc // Other sorts are tested elsewhere + makeMeSomeReleases(lister.cfg.Releases, t) + list, err := lister.Run() + is.NoError(err) + is.Len(list, 3) + is.Equal("two", list[0].Name) + is.Equal("three", list[1].Name) + is.Equal("one", list[2].Name) +} + +func TestList_Limit(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + lister.Limit = 2 + makeMeSomeReleases(lister.cfg.Releases, t) + list, err := lister.Run() + is.NoError(err) + is.Len(list, 2) + // Lex order means one, three, two + is.Equal("one", list[0].Name) + is.Equal("three", list[1].Name) +} + +func TestList_BigLimit(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + lister.Limit = 20 + makeMeSomeReleases(lister.cfg.Releases, t) + list, err := lister.Run() + is.NoError(err) + is.Len(list, 3) + + // Lex order means one, three, two + is.Equal("one", list[0].Name) + is.Equal("three", list[1].Name) + is.Equal("two", list[2].Name) +} + +func TestList_LimitOffset(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + lister.Limit = 2 + lister.Offset = 1 + makeMeSomeReleases(lister.cfg.Releases, t) + list, err := lister.Run() + is.NoError(err) + is.Len(list, 2) + + // Lex order means one, three, two + is.Equal("three", list[0].Name) + is.Equal("two", list[1].Name) +} + +func TestList_LimitOffsetOutOfBounds(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + lister.Limit = 2 + lister.Offset = 3 // Last item is index 2 + makeMeSomeReleases(lister.cfg.Releases, t) + list, err := lister.Run() + is.NoError(err) + is.Len(list, 0) + + lister.Limit = 10 + lister.Offset = 1 + list, err = lister.Run() + is.NoError(err) + is.Len(list, 2) +} + +func TestList_StateMask(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + makeMeSomeReleases(lister.cfg.Releases, t) + one, err := lister.cfg.Releases.Get("one", 1) + is.NoError(err) + one.SetStatus(release.StatusUninstalled, "uninstalled") + err = lister.cfg.Releases.Update(one) + is.NoError(err) + + res, err := lister.Run() + is.NoError(err) + is.Len(res, 2) + is.Equal("three", res[0].Name) + is.Equal("two", res[1].Name) + + lister.StateMask = ListUninstalled + res, err = lister.Run() + is.NoError(err) + is.Len(res, 1) + is.Equal("one", res[0].Name) + + lister.StateMask |= ListDeployed + res, err = lister.Run() + is.NoError(err) + is.Len(res, 3) +} + +func TestList_StateMaskWithStaleRevisions(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + lister.StateMask = ListFailed + + makeMeSomeReleasesWithStaleFailure(lister.cfg.Releases, t) + + res, err := lister.Run() + + is.NoError(err) + is.Len(res, 1) + + // "dirty" release should _not_ be present as most recent + // release is deployed despite failed release in past + is.Equal("failed", res[0].Name) +} + +func makeMeSomeReleasesWithStaleFailure(store *storage.Storage, t *testing.T) { + t.Helper() + one := namedReleaseStub("clean", release.StatusDeployed) + one.Namespace = "default" + one.Version = 1 + + two := namedReleaseStub("dirty", release.StatusDeployed) + two.Namespace = "default" + two.Version = 1 + + three := namedReleaseStub("dirty", release.StatusFailed) + three.Namespace = "default" + three.Version = 2 + + four := namedReleaseStub("dirty", release.StatusDeployed) + four.Namespace = "default" + four.Version = 3 + + five := namedReleaseStub("failed", release.StatusFailed) + five.Namespace = "default" + five.Version = 1 + + for _, rel := range []*release.Release{one, two, three, four, five} { + if err := store.Create(rel); err != nil { + t.Fatal(err) + } + } + + all, err := store.ListReleases() + assert.NoError(t, err) + assert.Len(t, all, 5, "sanity test: five items added") +} + +func TestList_Filter(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + lister.Filter = "th." + makeMeSomeReleases(lister.cfg.Releases, t) + + res, err := lister.Run() + is.NoError(err) + is.Len(res, 1) + is.Equal("three", res[0].Name) +} + +func TestList_FilterFailsCompile(t *testing.T) { + is := assert.New(t) + lister := newListFixture(t) + lister.Filter = "t[h.{{{" + makeMeSomeReleases(lister.cfg.Releases, t) + + _, err := lister.Run() + is.Error(err) +} + +func makeMeSomeReleases(store *storage.Storage, t *testing.T) { + t.Helper() + one := releaseStub() + one.Name = "one" + one.Namespace = "default" + one.Version = 1 + two := releaseStub() + two.Name = "two" + two.Namespace = "default" + two.Version = 2 + three := releaseStub() + three.Name = "three" + three.Namespace = "default" + three.Version = 3 + + for _, rel := range []*release.Release{one, two, three} { + if err := store.Create(rel); err != nil { + t.Fatal(err) + } + } + + all, err := store.ListReleases() + assert.NoError(t, err) + assert.Len(t, all, 3, "sanity test: three items added") +} + +func TestFilterLatestReleases(t *testing.T) { + t.Run("should filter old versions of the same release", func(t *testing.T) { + r1 := releaseStub() + r1.Name = "r" + r1.Version = 1 + r2 := releaseStub() + r2.Name = "r" + r2.Version = 2 + another := releaseStub() + another.Name = "another" + another.Version = 1 + + filteredList := filterLatestReleases([]*release.Release{r1, r2, another}) + expectedFilteredList := []*release.Release{r2, another} + + assert.ElementsMatch(t, expectedFilteredList, filteredList) + }) + + t.Run("should not filter out any version across namespaces", func(t *testing.T) { + r1 := releaseStub() + r1.Name = "r" + r1.Namespace = "default" + r1.Version = 1 + r2 := releaseStub() + r2.Name = "r" + r2.Namespace = "testing" + r2.Version = 2 + + filteredList := filterLatestReleases([]*release.Release{r1, r2}) + expectedFilteredList := []*release.Release{r1, r2} + + assert.ElementsMatch(t, expectedFilteredList, filteredList) + }) +} + +func TestSelectorList(t *testing.T) { + r1 := releaseStub() + r1.Name = "r1" + r1.Version = 1 + r1.Labels = map[string]string{"key": "value1"} + r2 := releaseStub() + r2.Name = "r2" + r2.Version = 1 + r2.Labels = map[string]string{"key": "value2"} + r3 := releaseStub() + r3.Name = "r3" + r3.Version = 1 + r3.Labels = map[string]string{} + + lister := newListFixture(t) + for _, rel := range []*release.Release{r1, r2, r3} { + if err := lister.cfg.Releases.Create(rel); err != nil { + t.Fatal(err) + } + } + + t.Run("should fail selector parsing", func(t *testing.T) { + is := assert.New(t) + lister.Selector = "a?=b" + + _, err := lister.Run() + is.Error(err) + }) + + t.Run("should select one release with matching label", func(t *testing.T) { + lister.Selector = "key==value1" + res, _ := lister.Run() + + expectedFilteredList := []*release.Release{r1} + assert.ElementsMatch(t, expectedFilteredList, res) + }) + + t.Run("should select two releases with non matching label", func(t *testing.T) { + lister.Selector = "key!=value1" + res, _ := lister.Run() + + expectedFilteredList := []*release.Release{r2, r3} + assert.ElementsMatch(t, expectedFilteredList, res) + }) +} diff --git a/pkg/action/package.go b/pkg/action/package.go new file mode 100644 index 00000000000..52920956fb8 --- /dev/null +++ b/pkg/action/package.go @@ -0,0 +1,182 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "syscall" + + "github.com/Masterminds/semver/v3" + "github.com/pkg/errors" + "golang.org/x/term" + + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/provenance" +) + +// Package is the action for packaging a chart. +// +// It provides the implementation of 'helm package'. +type Package struct { + Sign bool + Key string + Keyring string + PassphraseFile string + Version string + AppVersion string + Destination string + DependencyUpdate bool + + RepositoryConfig string + RepositoryCache string +} + +// NewPackage creates a new Package object with the given configuration. +func NewPackage() *Package { + return &Package{} +} + +// Run executes 'helm package' against the given chart and returns the path to the packaged chart. +func (p *Package) Run(path string, vals map[string]interface{}) (string, error) { + ch, err := loader.LoadDir(path) + if err != nil { + return "", err + } + + // If version is set, modify the version. + if p.Version != "" { + ch.Metadata.Version = p.Version + } + + if err := validateVersion(ch.Metadata.Version); err != nil { + return "", err + } + + if p.AppVersion != "" { + ch.Metadata.AppVersion = p.AppVersion + } + + if reqs := ch.Metadata.Dependencies; reqs != nil { + if err := CheckDependencies(ch, reqs); err != nil { + return "", err + } + } + + var dest string + if p.Destination == "." { + // Save to the current working directory. + dest, err = os.Getwd() + if err != nil { + return "", err + } + } else { + // Otherwise save to set destination + dest = p.Destination + } + + name, err := chartutil.Save(ch, dest) + if err != nil { + return "", errors.Wrap(err, "failed to save") + } + + if p.Sign { + err = p.Clearsign(name) + } + + return name, err +} + +// validateVersion Verify that version is a Version, and error out if it is not. +func validateVersion(ver string) error { + if _, err := semver.NewVersion(ver); err != nil { + return err + } + return nil +} + +// Clearsign signs a chart +func (p *Package) Clearsign(filename string) error { + // Load keyring + signer, err := provenance.NewFromKeyring(p.Keyring, p.Key) + if err != nil { + return err + } + + passphraseFetcher := promptUser + if p.PassphraseFile != "" { + passphraseFetcher, err = passphraseFileFetcher(p.PassphraseFile, os.Stdin) + if err != nil { + return err + } + } + + if err := signer.DecryptKey(passphraseFetcher); err != nil { + return err + } + + sig, err := signer.ClearSign(filename) + if err != nil { + return err + } + + return ioutil.WriteFile(filename+".prov", []byte(sig), 0644) +} + +// promptUser implements provenance.PassphraseFetcher +func promptUser(name string) ([]byte, error) { + fmt.Printf("Password for key %q > ", name) + // syscall.Stdin is not an int in all environments and needs to be coerced + // into one there (e.g., Windows) + pw, err := term.ReadPassword(int(syscall.Stdin)) + fmt.Println() + return pw, err +} + +func passphraseFileFetcher(passphraseFile string, stdin *os.File) (provenance.PassphraseFetcher, error) { + file, err := openPassphraseFile(passphraseFile, stdin) + if err != nil { + return nil, err + } + defer file.Close() + + reader := bufio.NewReader(file) + passphrase, _, err := reader.ReadLine() + if err != nil { + return nil, err + } + return func(name string) ([]byte, error) { + return passphrase, nil + }, nil +} + +func openPassphraseFile(passphraseFile string, stdin *os.File) (*os.File, error) { + if passphraseFile == "-" { + stat, err := stdin.Stat() + if err != nil { + return nil, err + } + if (stat.Mode() & os.ModeNamedPipe) == 0 { + return nil, errors.New("specified reading passphrase from stdin, without input on stdin") + } + return stdin, nil + } + return os.Open(passphraseFile) +} diff --git a/pkg/action/package_test.go b/pkg/action/package_test.go new file mode 100644 index 00000000000..5c5fed571c1 --- /dev/null +++ b/pkg/action/package_test.go @@ -0,0 +1,125 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "io/ioutil" + "os" + "path" + "testing" + + "github.com/Masterminds/semver/v3" + + "helm.sh/helm/v3/internal/test/ensure" +) + +func TestPassphraseFileFetcher(t *testing.T) { + secret := "secret" + directory := ensure.TempFile(t, "passphrase-file", []byte(secret)) + defer os.RemoveAll(directory) + + fetcher, err := passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil) + if err != nil { + t.Fatal("Unable to create passphraseFileFetcher", err) + } + + passphrase, err := fetcher("key") + if err != nil { + t.Fatal("Unable to fetch passphrase") + } + + if string(passphrase) != secret { + t.Errorf("Expected %s got %s", secret, string(passphrase)) + } +} + +func TestPassphraseFileFetcher_WithLineBreak(t *testing.T) { + secret := "secret" + directory := ensure.TempFile(t, "passphrase-file", []byte(secret+"\n\n.")) + defer os.RemoveAll(directory) + + fetcher, err := passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil) + if err != nil { + t.Fatal("Unable to create passphraseFileFetcher", err) + } + + passphrase, err := fetcher("key") + if err != nil { + t.Fatal("Unable to fetch passphrase") + } + + if string(passphrase) != secret { + t.Errorf("Expected %s got %s", secret, string(passphrase)) + } +} + +func TestPassphraseFileFetcher_WithInvalidStdin(t *testing.T) { + directory := ensure.TempDir(t) + defer os.RemoveAll(directory) + + stdin, err := ioutil.TempFile(directory, "non-existing") + if err != nil { + t.Fatal("Unable to create test file", err) + } + + if _, err := passphraseFileFetcher("-", stdin); err == nil { + t.Error("Expected passphraseFileFetcher returning an error") + } +} + +func TestValidateVersion(t *testing.T) { + type args struct { + ver string + } + tests := []struct { + name string + args args + wantErr error + }{ + { + "normal semver version", + args{ + ver: "1.1.3-23658", + }, + nil, + }, + { + "Pre version number starting with 0", + args{ + ver: "1.1.3-023658", + }, + semver.ErrSegmentStartsZero, + }, + { + "Invalid version number", + args{ + ver: "1.1.3.sd.023658", + }, + semver.ErrInvalidSemVer, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateVersion(tt.args.ver); err != nil { + if err != tt.wantErr { + t.Errorf("Expected {%v}, got {%v}", tt.wantErr, err) + } + + } + }) + } +} diff --git a/pkg/action/pull.go b/pkg/action/pull.go new file mode 100644 index 00000000000..04faa3b6bbd --- /dev/null +++ b/pkg/action/pull.go @@ -0,0 +1,168 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/downloader" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" +) + +// Pull is the action for checking a given release's information. +// +// It provides the implementation of 'helm pull'. +type Pull struct { + ChartPathOptions + + Settings *cli.EnvSettings // TODO: refactor this out of pkg/action + + Devel bool + Untar bool + VerifyLater bool + UntarDir string + DestDir string + cfg *Configuration +} + +type PullOpt func(*Pull) + +func WithConfig(cfg *Configuration) PullOpt { + return func(p *Pull) { + p.cfg = cfg + } +} + +// NewPull creates a new Pull object. +func NewPull() *Pull { + return NewPullWithOpts() +} + +// NewPullWithOpts creates a new pull, with configuration options. +func NewPullWithOpts(opts ...PullOpt) *Pull { + p := &Pull{} + for _, fn := range opts { + fn(p) + } + + return p +} + +// Run executes 'helm pull' against the given release. +func (p *Pull) Run(chartRef string) (string, error) { + var out strings.Builder + + c := downloader.ChartDownloader{ + Out: &out, + Keyring: p.Keyring, + Verify: downloader.VerifyNever, + Getters: getter.All(p.Settings), + Options: []getter.Option{ + getter.WithBasicAuth(p.Username, p.Password), + getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile), + getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify), + }, + RepositoryConfig: p.Settings.RepositoryConfig, + RepositoryCache: p.Settings.RepositoryCache, + } + + if strings.HasPrefix(chartRef, "oci://") { + if p.Version == "" { + return out.String(), errors.Errorf("--version flag is explicitly required for OCI registries") + } + + c.Options = append(c.Options, + getter.WithRegistryClient(p.cfg.RegistryClient), + getter.WithTagName(p.Version)) + } + + if p.Verify { + c.Verify = downloader.VerifyAlways + } else if p.VerifyLater { + c.Verify = downloader.VerifyLater + } + + // If untar is set, we fetch to a tempdir, then untar and copy after + // verification. + dest := p.DestDir + if p.Untar { + var err error + dest, err = ioutil.TempDir("", "helm-") + if err != nil { + return out.String(), errors.Wrap(err, "failed to untar") + } + defer os.RemoveAll(dest) + } + + if p.RepoURL != "" { + chartURL, err := repo.FindChartInAuthAndTLSRepoURL(p.RepoURL, p.Username, p.Password, chartRef, p.Version, p.CertFile, p.KeyFile, p.CaFile, p.InsecureSkipTLSverify, getter.All(p.Settings)) + if err != nil { + return out.String(), err + } + chartRef = chartURL + } + + saved, v, err := c.DownloadTo(chartRef, p.Version, dest) + if err != nil { + return out.String(), err + } + + if p.Verify { + for name := range v.SignedBy.Identities { + fmt.Fprintf(&out, "Signed by: %v\n", name) + } + fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", v.SignedBy.PrimaryKey.Fingerprint) + fmt.Fprintf(&out, "Chart Hash Verified: %s\n", v.FileHash) + } + + // After verification, untar the chart into the requested directory. + if p.Untar { + ud := p.UntarDir + if !filepath.IsAbs(ud) { + ud = filepath.Join(p.DestDir, ud) + } + // Let udCheck to check conflict file/dir without replacing ud when untarDir is the current directory(.). + udCheck := ud + if udCheck == "." { + _, udCheck = filepath.Split(chartRef) + } else { + _, chartName := filepath.Split(chartRef) + udCheck = filepath.Join(udCheck, chartName) + } + + if _, err := os.Stat(udCheck); err != nil { + if err := os.MkdirAll(udCheck, 0755); err != nil { + return out.String(), errors.Wrap(err, "failed to untar (mkdir)") + } + + } else { + return out.String(), errors.Errorf("failed to untar: a file or directory with the name %s already exists", udCheck) + } + + return out.String(), chartutil.ExpandFile(ud, saved) + } + return out.String(), nil +} diff --git a/pkg/action/registry_login.go b/pkg/action/registry_login.go new file mode 100644 index 00000000000..00f6e2644ce --- /dev/null +++ b/pkg/action/registry_login.go @@ -0,0 +1,38 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "io" +) + +// RegistryLogin performs a registry login operation. +type RegistryLogin struct { + cfg *Configuration +} + +// NewRegistryLogin creates a new RegistryLogin object with the given configuration. +func NewRegistryLogin(cfg *Configuration) *RegistryLogin { + return &RegistryLogin{ + cfg: cfg, + } +} + +// Run executes the registry login operation +func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string, insecure bool) error { + return a.cfg.RegistryClient.Login(hostname, username, password, insecure) +} diff --git a/pkg/action/registry_logout.go b/pkg/action/registry_logout.go new file mode 100644 index 00000000000..69add4163de --- /dev/null +++ b/pkg/action/registry_logout.go @@ -0,0 +1,38 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "io" +) + +// RegistryLogout performs a registry login operation. +type RegistryLogout struct { + cfg *Configuration +} + +// NewRegistryLogout creates a new RegistryLogout object with the given configuration. +func NewRegistryLogout(cfg *Configuration) *RegistryLogout { + return &RegistryLogout{ + cfg: cfg, + } +} + +// Run executes the registry logout operation +func (a *RegistryLogout) Run(out io.Writer, hostname string) error { + return a.cfg.RegistryClient.Logout(hostname) +} diff --git a/pkg/action/release_testing.go b/pkg/action/release_testing.go new file mode 100644 index 00000000000..ecaeaf59f57 --- /dev/null +++ b/pkg/action/release_testing.go @@ -0,0 +1,138 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/release" +) + +// ReleaseTesting is the action for testing a release. +// +// It provides the implementation of 'helm test'. +type ReleaseTesting struct { + cfg *Configuration + Timeout time.Duration + // Used for fetching logs from test pods + Namespace string + Filters map[string][]string +} + +// NewReleaseTesting creates a new ReleaseTesting object with the given configuration. +func NewReleaseTesting(cfg *Configuration) *ReleaseTesting { + return &ReleaseTesting{ + cfg: cfg, + Filters: map[string][]string{}, + } +} + +// Run executes 'helm test' against the given release. +func (r *ReleaseTesting) Run(name string) (*release.Release, error) { + if err := r.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + + if err := chartutil.ValidateReleaseName(name); err != nil { + return nil, errors.Errorf("releaseTest: Release name is invalid: %s", name) + } + + // finds the non-deleted release with the given name + rel, err := r.cfg.Releases.Last(name) + if err != nil { + return rel, err + } + + skippedHooks := []*release.Hook{} + executingHooks := []*release.Hook{} + if len(r.Filters["!name"]) != 0 { + for _, h := range rel.Hooks { + if contains(r.Filters["!name"], h.Name) { + skippedHooks = append(skippedHooks, h) + } else { + executingHooks = append(executingHooks, h) + } + } + rel.Hooks = executingHooks + } + if len(r.Filters["name"]) != 0 { + executingHooks = nil + for _, h := range rel.Hooks { + if contains(r.Filters["name"], h.Name) { + executingHooks = append(executingHooks, h) + } else { + skippedHooks = append(skippedHooks, h) + } + } + rel.Hooks = executingHooks + } + + if err := r.cfg.execHook(rel, release.HookTest, r.Timeout); err != nil { + rel.Hooks = append(skippedHooks, rel.Hooks...) + r.cfg.Releases.Update(rel) + return rel, err + } + + rel.Hooks = append(skippedHooks, rel.Hooks...) + return rel, r.cfg.Releases.Update(rel) +} + +// GetPodLogs will write the logs for all test pods in the given release into +// the given writer. These can be immediately output to the user or captured for +// other uses +func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error { + client, err := r.cfg.KubernetesClientSet() + if err != nil { + return errors.Wrap(err, "unable to get kubernetes client to fetch pod logs") + } + + for _, h := range rel.Hooks { + for _, e := range h.Events { + if e == release.HookTest { + req := client.CoreV1().Pods(r.Namespace).GetLogs(h.Name, &v1.PodLogOptions{}) + logReader, err := req.Stream(context.Background()) + if err != nil { + return errors.Wrapf(err, "unable to get pod logs for %s", h.Name) + } + + fmt.Fprintf(out, "POD LOGS: %s\n", h.Name) + _, err = io.Copy(out, logReader) + fmt.Fprintln(out) + if err != nil { + return errors.Wrapf(err, "unable to write pod logs for %s", h.Name) + } + } + } + } + return nil +} + +func contains(arr []string, value string) bool { + for _, item := range arr { + if item == value { + return true + } + } + return false +} diff --git a/pkg/action/resource_policy.go b/pkg/action/resource_policy.go new file mode 100644 index 00000000000..63e83f3d940 --- /dev/null +++ b/pkg/action/resource_policy.go @@ -0,0 +1,46 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "strings" + + "helm.sh/helm/v3/pkg/kube" + "helm.sh/helm/v3/pkg/releaseutil" +) + +func filterManifestsToKeep(manifests []releaseutil.Manifest) (keep, remaining []releaseutil.Manifest) { + for _, m := range manifests { + 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[kube.ResourcePolicyAnno] + if !ok { + remaining = append(remaining, m) + continue + } + + resourcePolicyType = strings.ToLower(strings.TrimSpace(resourcePolicyType)) + if resourcePolicyType == kube.KeepPolicy { + keep = append(keep, m) + } + + } + return keep, remaining +} diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go new file mode 100644 index 00000000000..f3f958f3d46 --- /dev/null +++ b/pkg/action/rollback.go @@ -0,0 +1,241 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "bytes" + "fmt" + "strings" + "time" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" +) + +// Rollback is the action for rolling back to a given release. +// +// It provides the implementation of 'helm rollback'. +type Rollback struct { + cfg *Configuration + + Version int + Timeout time.Duration + Wait bool + WaitForJobs bool + DisableHooks bool + DryRun bool + Recreate bool // will (if true) recreate pods after a rollback. + Force bool // will (if true) force resource upgrade through uninstall/recreate if needed + CleanupOnFail bool + MaxHistory int // MaxHistory limits the maximum number of revisions saved per release +} + +// NewRollback creates a new Rollback object with the given configuration. +func NewRollback(cfg *Configuration) *Rollback { + return &Rollback{ + cfg: cfg, + } +} + +// Run executes 'helm rollback' against the given release. +func (r *Rollback) Run(name string) error { + if err := r.cfg.KubeClient.IsReachable(); err != nil { + return err + } + + r.cfg.Releases.MaxHistory = r.MaxHistory + + r.cfg.Log("preparing rollback of %s", name) + currentRelease, targetRelease, err := r.prepareRollback(name) + if err != nil { + return err + } + + if !r.DryRun { + r.cfg.Log("creating rolled back release for %s", name) + if err := r.cfg.Releases.Create(targetRelease); err != nil { + return err + } + } + + r.cfg.Log("performing rollback of %s", name) + if _, err := r.performRollback(currentRelease, targetRelease); err != nil { + return err + } + + if !r.DryRun { + r.cfg.Log("updating status for rolled back release for %s", name) + if err := r.cfg.Releases.Update(targetRelease); err != nil { + return err + } + } + return nil +} + +// prepareRollback finds the previous release and prepares a new release object with +// the previous release's configuration +func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Release, error) { + if err := chartutil.ValidateReleaseName(name); err != nil { + return nil, nil, errors.Errorf("prepareRollback: Release name is invalid: %s", name) + } + + if r.Version < 0 { + return nil, nil, errInvalidRevision + } + + currentRelease, err := r.cfg.Releases.Last(name) + if err != nil { + return nil, nil, err + } + + previousVersion := r.Version + if r.Version == 0 { + previousVersion = currentRelease.Version - 1 + } + + r.cfg.Log("rolling back %s (current: v%d, target: v%d)", name, currentRelease.Version, previousVersion) + + previousRelease, err := r.cfg.Releases.Get(name, previousVersion) + if err != nil { + return nil, nil, err + } + + // Store a new release object with previous release's configuration + targetRelease := &release.Release{ + Name: name, + Namespace: currentRelease.Namespace, + Chart: previousRelease.Chart, + Config: previousRelease.Config, + Info: &release.Info{ + FirstDeployed: currentRelease.Info.FirstDeployed, + LastDeployed: helmtime.Now(), + Status: release.StatusPendingRollback, + Notes: previousRelease.Info.Notes, + // Because we lose the reference to previous version elsewhere, we set the + // message here, and only override it later if we experience failure. + Description: fmt.Sprintf("Rollback to %d", previousVersion), + }, + Version: currentRelease.Version + 1, + Manifest: previousRelease.Manifest, + Hooks: previousRelease.Hooks, + } + + return currentRelease, targetRelease, nil +} + +func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release) (*release.Release, error) { + if r.DryRun { + r.cfg.Log("dry run for %s", targetRelease.Name) + return targetRelease, nil + } + + current, err := r.cfg.KubeClient.Build(bytes.NewBufferString(currentRelease.Manifest), false) + if err != nil { + return targetRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest") + } + target, err := r.cfg.KubeClient.Build(bytes.NewBufferString(targetRelease.Manifest), false) + if err != nil { + return targetRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest") + } + + // pre-rollback hooks + if !r.DisableHooks { + if err := r.cfg.execHook(targetRelease, release.HookPreRollback, r.Timeout); err != nil { + return targetRelease, err + } + } else { + r.cfg.Log("rollback hooks disabled for %s", targetRelease.Name) + } + + results, err := r.cfg.KubeClient.Update(current, target, r.Force) + + if err != nil { + msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err) + r.cfg.Log("warning: %s", msg) + currentRelease.Info.Status = release.StatusSuperseded + targetRelease.Info.Status = release.StatusFailed + targetRelease.Info.Description = msg + r.cfg.recordRelease(currentRelease) + r.cfg.recordRelease(targetRelease) + if r.CleanupOnFail { + r.cfg.Log("Cleanup on fail set, cleaning up %d resources", len(results.Created)) + _, errs := r.cfg.KubeClient.Delete(results.Created) + if errs != nil { + var errorList []string + for _, e := range errs { + errorList = append(errorList, e.Error()) + } + return targetRelease, errors.Wrapf(fmt.Errorf("unable to cleanup resources: %s", strings.Join(errorList, ", ")), "an error occurred while cleaning up resources. original rollback error: %s", err) + } + r.cfg.Log("Resource cleanup complete") + } + return targetRelease, err + } + + if r.Recreate { + // NOTE: Because this is not critical for a release to succeed, we just + // log if an error occurs and continue onward. If we ever introduce log + // levels, we should make these error level logs so users are notified + // that they'll need to go do the cleanup on their own + if err := recreate(r.cfg, results.Updated); err != nil { + r.cfg.Log(err.Error()) + } + } + + if r.Wait { + if r.WaitForJobs { + if err := r.cfg.KubeClient.WaitWithJobs(target, r.Timeout); err != nil { + targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error())) + r.cfg.recordRelease(currentRelease) + r.cfg.recordRelease(targetRelease) + return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name) + } + } else { + if err := r.cfg.KubeClient.Wait(target, r.Timeout); err != nil { + targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error())) + r.cfg.recordRelease(currentRelease) + r.cfg.recordRelease(targetRelease) + return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name) + } + } + } + + // post-rollback hooks + if !r.DisableHooks { + if err := r.cfg.execHook(targetRelease, release.HookPostRollback, r.Timeout); err != nil { + return targetRelease, err + } + } + + deployed, err := r.cfg.Releases.DeployedAll(currentRelease.Name) + if err != nil && !strings.Contains(err.Error(), "has no deployed releases") { + return nil, err + } + // Supersede all previous deployments, see issue #2941. + for _, rel := range deployed { + r.cfg.Log("superseding previous deployment %d", rel.Version) + rel.Info.Status = release.StatusSuperseded + r.cfg.recordRelease(rel) + } + + targetRelease.Info.Status = release.StatusDeployed + + return targetRelease, nil +} diff --git a/pkg/action/show.go b/pkg/action/show.go new file mode 100644 index 00000000000..4eab2b4fd0b --- /dev/null +++ b/pkg/action/show.go @@ -0,0 +1,130 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + "k8s.io/cli-runtime/pkg/printers" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" +) + +// ShowOutputFormat is the format of the output of `helm show` +type ShowOutputFormat string + +const ( + // ShowAll is the format which shows all the information of a chart + ShowAll ShowOutputFormat = "all" + // ShowChart is the format which only shows the chart's definition + ShowChart ShowOutputFormat = "chart" + // ShowValues is the format which only shows the chart's values + ShowValues ShowOutputFormat = "values" + // ShowReadme is the format which only shows the chart's README + ShowReadme ShowOutputFormat = "readme" +) + +var readmeFileNames = []string{"readme.md", "readme.txt", "readme"} + +func (o ShowOutputFormat) String() string { + return string(o) +} + +// Show is the action for checking a given release's information. +// +// It provides the implementation of 'helm show' and its respective subcommands. +type Show struct { + ChartPathOptions + Devel bool + OutputFormat ShowOutputFormat + JSONPathTemplate string + chart *chart.Chart // for testing +} + +// NewShow creates a new Show object with the given configuration. +func NewShow(output ShowOutputFormat) *Show { + return &Show{ + OutputFormat: output, + } +} + +// Run executes 'helm show' against the given release. +func (s *Show) Run(chartpath string) (string, error) { + if s.chart == nil { + chrt, err := loader.Load(chartpath) + if err != nil { + return "", err + } + s.chart = chrt + } + cf, err := yaml.Marshal(s.chart.Metadata) + if err != nil { + return "", err + } + + var out strings.Builder + if s.OutputFormat == ShowChart || s.OutputFormat == ShowAll { + fmt.Fprintf(&out, "%s\n", cf) + } + + if (s.OutputFormat == ShowValues || s.OutputFormat == ShowAll) && s.chart.Values != nil { + if s.OutputFormat == ShowAll { + fmt.Fprintln(&out, "---") + } + if s.JSONPathTemplate != "" { + printer, err := printers.NewJSONPathPrinter(s.JSONPathTemplate) + if err != nil { + return "", errors.Wrapf(err, "error parsing jsonpath %s", s.JSONPathTemplate) + } + printer.Execute(&out, s.chart.Values) + } else { + for _, f := range s.chart.Raw { + if f.Name == chartutil.ValuesfileName { + fmt.Fprintln(&out, string(f.Data)) + } + } + } + } + + if s.OutputFormat == ShowReadme || s.OutputFormat == ShowAll { + if s.OutputFormat == ShowAll { + fmt.Fprintln(&out, "---") + } + readme := findReadme(s.chart.Files) + if readme == nil { + return out.String(), nil + } + fmt.Fprintf(&out, "%s\n", readme.Data) + } + return out.String(), nil +} + +func findReadme(files []*chart.File) (file *chart.File) { + for _, file := range files { + for _, n := range readmeFileNames { + if strings.EqualFold(file.Name, n) { + return file + } + } + } + return nil +} diff --git a/pkg/action/show_test.go b/pkg/action/show_test.go new file mode 100644 index 00000000000..a2efdc8ace5 --- /dev/null +++ b/pkg/action/show_test.go @@ -0,0 +1,85 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "testing" + + "helm.sh/helm/v3/pkg/chart" +) + +func TestShow(t *testing.T) { + client := NewShow(ShowAll) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "alpine"}, + Files: []*chart.File{ + {Name: "README.md", Data: []byte("README\n")}, + }, + Raw: []*chart.File{ + {Name: "values.yaml", Data: []byte("VALUES\n")}, + }, + Values: map[string]interface{}{}, + } + + output, err := client.Run("") + if err != nil { + t.Fatal(err) + } + + expect := `name: alpine + +--- +VALUES + +--- +README + +` + if output != expect { + t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) + } +} + +func TestShowNoValues(t *testing.T) { + client := NewShow(ShowAll) + client.chart = new(chart.Chart) + + // Regression tests for missing values. See issue #1024. + client.OutputFormat = ShowValues + output, err := client.Run("") + if err != nil { + t.Fatal(err) + } + + if len(output) != 0 { + t.Errorf("expected empty values buffer, got %s", output) + } +} + +func TestShowValuesByJsonPathFormat(t *testing.T) { + client := NewShow(ShowValues) + client.JSONPathTemplate = "{$.nestedKey.simpleKey}" + client.chart = buildChart(withSampleValues()) + output, err := client.Run("") + if err != nil { + t.Fatal(err) + } + expect := "simpleValue" + if output != expect { + t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) + } +} diff --git a/pkg/action/status.go b/pkg/action/status.go new file mode 100644 index 00000000000..1c556e28dc2 --- /dev/null +++ b/pkg/action/status.go @@ -0,0 +1,51 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "helm.sh/helm/v3/pkg/release" +) + +// Status is the action for checking the deployment status of releases. +// +// It provides the implementation of 'helm status'. +type Status struct { + cfg *Configuration + + Version int + + // If true, display description to output format, + // only affect print type table. + // TODO Helm 4: Remove this flag and output the description by default. + ShowDescription bool +} + +// NewStatus creates a new Status object with the given configuration. +func NewStatus(cfg *Configuration) *Status { + return &Status{ + cfg: cfg, + } +} + +// Run executes 'helm status' against the given release. +func (s *Status) Run(name string) (*release.Release, error) { + if err := s.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + + return s.cfg.releaseContent(name, s.Version) +} diff --git a/pkg/action/testdata/charts/chart-missing-deps/Chart.yaml b/pkg/action/testdata/charts/chart-missing-deps/Chart.yaml new file mode 100755 index 00000000000..ba10ee80363 --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/Chart.yaml @@ -0,0 +1,2 @@ +name: chart-with-missing-deps +version: 2.1.8 diff --git a/pkg/action/testdata/charts/chart-missing-deps/requirements.lock b/pkg/action/testdata/charts/chart-missing-deps/requirements.lock new file mode 100755 index 00000000000..dcda2b1420f --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: mariadb + repository: https://charts.helm.sh/stable/ + version: 4.3.1 +digest: sha256:82a0e5374376169d2ecf7d452c18a2ed93507f5d17c3393a1457f9ffad7e9b26 +generated: 2018-08-02T22:07:51.905271776Z diff --git a/pkg/action/testdata/charts/chart-missing-deps/requirements.yaml b/pkg/action/testdata/charts/chart-missing-deps/requirements.yaml new file mode 100755 index 00000000000..fef7d0b7f22 --- /dev/null +++ b/pkg/action/testdata/charts/chart-missing-deps/requirements.yaml @@ -0,0 +1,7 @@ +dependencies: +- name: mariadb + version: 4.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - wordpress-database diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies-2.1.8.tgz b/pkg/action/testdata/charts/chart-with-compressed-dependencies-2.1.8.tgz new file mode 100644 index 00000000000..7a22b1d827f Binary files /dev/null and b/pkg/action/testdata/charts/chart-with-compressed-dependencies-2.1.8.tgz differ diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/Chart.yaml b/pkg/action/testdata/charts/chart-with-compressed-dependencies/Chart.yaml new file mode 100755 index 00000000000..1d16590b687 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-compressed-dependencies/Chart.yaml @@ -0,0 +1,2 @@ +name: chart-with-compressed-dependencies +version: 2.1.8 diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/charts/mariadb-4.3.1.tgz b/pkg/action/testdata/charts/chart-with-compressed-dependencies/charts/mariadb-4.3.1.tgz new file mode 100644 index 00000000000..5b38fa1c399 Binary files /dev/null and b/pkg/action/testdata/charts/chart-with-compressed-dependencies/charts/mariadb-4.3.1.tgz differ diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.lock b/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.lock new file mode 100755 index 00000000000..dcda2b1420f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: mariadb + repository: https://charts.helm.sh/stable/ + version: 4.3.1 +digest: sha256:82a0e5374376169d2ecf7d452c18a2ed93507f5d17c3393a1457f9ffad7e9b26 +generated: 2018-08-02T22:07:51.905271776Z diff --git a/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.yaml b/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.yaml new file mode 100755 index 00000000000..fef7d0b7f22 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-compressed-dependencies/requirements.yaml @@ -0,0 +1,7 @@ +dependencies: +- name: mariadb + version: 4.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - wordpress-database diff --git a/pkg/action/testdata/charts/chart-with-no-templates-dir/Chart.yaml b/pkg/action/testdata/charts/chart-with-no-templates-dir/Chart.yaml new file mode 100644 index 00000000000..d3458f6a22c --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-no-templates-dir/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: chart-with-no-templates-dir +description: an example chart +version: 199.44.12345-Alpha.1+cafe009 +icon: http://riverrun.io diff --git a/pkg/action/testdata/charts/chart-with-schema-negative/Chart.yaml b/pkg/action/testdata/charts/chart-with-schema-negative/Chart.yaml new file mode 100644 index 00000000000..395d24f6aa0 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema-negative/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +description: Empty testing chart +home: https://k8s.io/helm +name: empty +sources: +- https://github.com/kubernetes/helm +version: 0.1.0 diff --git a/pkg/action/testdata/charts/chart-with-schema-negative/templates/empty.yaml b/pkg/action/testdata/charts/chart-with-schema-negative/templates/empty.yaml new file mode 100644 index 00000000000..c80812f6e0a --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema-negative/templates/empty.yaml @@ -0,0 +1 @@ +# This file is intentionally blank diff --git a/pkg/action/testdata/charts/chart-with-schema-negative/values.schema.json b/pkg/action/testdata/charts/chart-with-schema-negative/values.schema.json new file mode 100644 index 00000000000..4df89bbe89f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema-negative/values.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "addresses": { + "description": "List of addresses", + "items": { + "properties": { + "city": { + "type": "string" + }, + "number": { + "type": "number" + }, + "street": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "age": { + "description": "Age", + "minimum": 0, + "type": "integer" + }, + "employmentInfo": { + "properties": { + "salary": { + "minimum": 0, + "type": "number" + }, + "title": { + "type": "string" + } + }, + "required": [ + "salary" + ], + "type": "object" + }, + "firstname": { + "description": "First name", + "type": "string" + }, + "lastname": { + "type": "string" + }, + "likesCoffee": { + "type": "boolean" + }, + "phoneNumbers": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "firstname", + "lastname", + "addresses", + "employmentInfo" + ], + "title": "Values", + "type": "object" +} diff --git a/pkg/action/testdata/charts/chart-with-schema-negative/values.yaml b/pkg/action/testdata/charts/chart-with-schema-negative/values.yaml new file mode 100644 index 00000000000..5a1250bff36 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema-negative/values.yaml @@ -0,0 +1,14 @@ +firstname: John +lastname: Doe +age: -5 +likesCoffee: true +addresses: + - city: Springfield + street: Main + number: 12345 + - city: New York + street: Broadway + number: 67890 +phoneNumbers: + - "(888) 888-8888" + - "(555) 555-5555" diff --git a/pkg/action/testdata/charts/chart-with-schema/Chart.yaml b/pkg/action/testdata/charts/chart-with-schema/Chart.yaml new file mode 100644 index 00000000000..395d24f6aa0 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +description: Empty testing chart +home: https://k8s.io/helm +name: empty +sources: +- https://github.com/kubernetes/helm +version: 0.1.0 diff --git a/pkg/action/testdata/charts/chart-with-schema/extra-values.yaml b/pkg/action/testdata/charts/chart-with-schema/extra-values.yaml new file mode 100644 index 00000000000..76c290c4f4c --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema/extra-values.yaml @@ -0,0 +1,2 @@ +age: -5 +employmentInfo: null diff --git a/pkg/action/testdata/charts/chart-with-schema/templates/empty.yaml b/pkg/action/testdata/charts/chart-with-schema/templates/empty.yaml new file mode 100644 index 00000000000..c80812f6e0a --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema/templates/empty.yaml @@ -0,0 +1 @@ +# This file is intentionally blank diff --git a/pkg/action/testdata/charts/chart-with-schema/values.schema.json b/pkg/action/testdata/charts/chart-with-schema/values.schema.json new file mode 100644 index 00000000000..4df89bbe89f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema/values.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "addresses": { + "description": "List of addresses", + "items": { + "properties": { + "city": { + "type": "string" + }, + "number": { + "type": "number" + }, + "street": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "age": { + "description": "Age", + "minimum": 0, + "type": "integer" + }, + "employmentInfo": { + "properties": { + "salary": { + "minimum": 0, + "type": "number" + }, + "title": { + "type": "string" + } + }, + "required": [ + "salary" + ], + "type": "object" + }, + "firstname": { + "description": "First name", + "type": "string" + }, + "lastname": { + "type": "string" + }, + "likesCoffee": { + "type": "boolean" + }, + "phoneNumbers": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "firstname", + "lastname", + "addresses", + "employmentInfo" + ], + "title": "Values", + "type": "object" +} diff --git a/pkg/action/testdata/charts/chart-with-schema/values.yaml b/pkg/action/testdata/charts/chart-with-schema/values.yaml new file mode 100644 index 00000000000..042dea664b8 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-schema/values.yaml @@ -0,0 +1,17 @@ +firstname: John +lastname: Doe +age: 25 +likesCoffee: true +employmentInfo: + title: Software Developer + salary: 100000 +addresses: + - city: Springfield + street: Main + number: 12345 + - city: New York + street: Broadway + number: 67890 +phoneNumbers: + - "(888) 888-8888" + - "(555) 555-5555" diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies-2.1.8.tgz b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies-2.1.8.tgz new file mode 100644 index 00000000000..ad9e681795c Binary files /dev/null and b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies-2.1.8.tgz differ diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/.helmignore b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/.helmignore new file mode 100755 index 00000000000..e2cf7941fa1 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/.helmignore @@ -0,0 +1,5 @@ +.git +# OWNERS file for Kubernetes +OWNERS +# example production yaml +values-production.yaml \ No newline at end of file diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/Chart.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/Chart.yaml new file mode 100755 index 00000000000..4d8569c8908 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/Chart.yaml @@ -0,0 +1,20 @@ +appVersion: 4.9.8 +description: Web publishing platform for building blogs and websites. +engine: gotpl +home: http://www.wordpress.com/ +icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png +keywords: +- wordpress +- cms +- blog +- http +- web +- application +- php +maintainers: +- email: containers@bitnami.com + name: bitnami-bot +name: chart-with-uncompressed-dependencies +sources: +- https://github.com/bitnami/bitnami-docker-wordpress +version: 2.1.8 diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/README.md b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/README.md new file mode 100755 index 00000000000..341a1ad9369 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/README.md @@ -0,0 +1,3 @@ +# WordPress + +This is a testing mock, and is not operational. diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/.helmignore b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/.helmignore new file mode 100755 index 00000000000..6b8710a711f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/.helmignore @@ -0,0 +1 @@ +.git diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/Chart.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/Chart.yaml new file mode 100755 index 00000000000..cefc1583697 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/Chart.yaml @@ -0,0 +1,21 @@ +appVersion: 10.1.34 +description: Fast, reliable, scalable, and easy to use open-source relational database + system. MariaDB Server is intended for mission-critical, heavy-load production systems + as well as for embedding into mass-deployed software. Highly available MariaDB cluster. +engine: gotpl +home: https://mariadb.org +icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png +keywords: +- mariadb +- mysql +- database +- sql +- prometheus +maintainers: +- email: containers@bitnami.com + name: bitnami-bot +name: mariadb +sources: +- https://github.com/bitnami/bitnami-docker-mariadb +- https://github.com/prometheus/mysqld_exporter +version: 4.3.1 diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/README.md b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/README.md new file mode 100755 index 00000000000..3463b8b6dbd --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/README.md @@ -0,0 +1,143 @@ +# MariaDB + +[MariaDB](https://mariadb.org) is one of the most popular database servers in the world. It’s made by the original developers of MySQL and guaranteed to stay open source. Notable users include Wikipedia, Facebook and Google. + +MariaDB is developed as open source software and as a relational database it provides an SQL interface for accessing data. The latest versions of MariaDB also include GIS and JSON features. + +## TL;DR + +```bash +$ helm install stable/mariadb +``` + +## Introduction + +This chart bootstraps a [MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) replication cluster deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. + +## Prerequisites + +- Kubernetes 1.4+ with Beta APIs enabled +- PV provisioner support in the underlying infrastructure + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +$ helm install --name my-release stable/mariadb +``` + +The command deploys MariaDB on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation. + +> **Tip**: List all releases using `helm list` + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: + +```bash +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Configuration + +The following table lists the configurable parameters of the MariaDB chart and their default values. + +| Parameter | Description | Default | +|-------------------------------------------|-----------------------------------------------------|-------------------------------------------------------------------| +| `image.registry` | MariaDB image registry | `docker.io` | +| `image.repository` | MariaDB Image name | `bitnami/mariadb` | +| `image.tag` | MariaDB Image tag | `{VERSION}` | +| `image.pullPolicy` | MariaDB image pull policy | `Always` if `imageTag` is `latest`, else `IfNotPresent` | +| `image.pullSecrets` | Specify image pull secrets | `nil` (does not add image pull secrets to deployed pods) | +| `service.type` | Kubernetes service type | `ClusterIP` | +| `service.port` | MySQL service port | `3306` | +| `rootUser.password` | Password for the `root` user | _random 10 character alphanumeric string_ | +| `rootUser.forcePassword` | Force users to specify a password | `false` | +| `db.user` | Username of new user to create | `nil` | +| `db.password` | Password for the new user | _random 10 character alphanumeric string if `db.user` is defined_ | +| `db.name` | Name for new database to create | `my_database` | +| `replication.enabled` | MariaDB replication enabled | `true` | +| `replication.user` | MariaDB replication user | `replicator` | +| `replication.password` | MariaDB replication user password | _random 10 character alphanumeric string_ | +| `master.antiAffinity` | Master pod anti-affinity policy | `soft` | +| `master.persistence.enabled` | Enable persistence using a `PersistentVolumeClaim` | `true` | +| `master.persistence.annotations` | Persistent Volume Claim annotations | `{}` | +| `master.persistence.storageClass` | Persistent Volume Storage Class | `` | +| `master.persistence.accessModes` | Persistent Volume Access Modes | `[ReadWriteOnce]` | +| `master.persistence.size` | Persistent Volume Size | `8Gi` | +| `master.config` | Config file for the MariaDB Master server | `_default values in the values.yaml file_` | +| `master.resources` | CPU/Memory resource requests/limits for master node | `{}` | +| `master.livenessProbe.enabled` | Turn on and off liveness probe (master) | `true` | +| `master.livenessProbe.initialDelaySeconds`| Delay before liveness probe is initiated (master) | `120` | +| `master.livenessProbe.periodSeconds` | How often to perform the probe (master) | `10` | +| `master.livenessProbe.timeoutSeconds` | When the probe times out (master) | `1` | +| `master.livenessProbe.successThreshold` | Minimum consecutive successes for the probe (master)| `1` | +| `master.livenessProbe.failureThreshold` | Minimum consecutive failures for the probe (master) | `3` | +| `master.readinessProbe.enabled` | Turn on and off readiness probe (master) | `true` | +| `master.readinessProbe.initialDelaySeconds`| Delay before readiness probe is initiated (master) | `15` | +| `master.readinessProbe.periodSeconds` | How often to perform the probe (master) | `10` | +| `master.readinessProbe.timeoutSeconds` | When the probe times out (master) | `1` | +| `master.readinessProbe.successThreshold` | Minimum consecutive successes for the probe (master)| `1` | +| `master.readinessProbe.failureThreshold` | Minimum consecutive failures for the probe (master) | `3` | +| `slave.replicas` | Desired number of slave replicas | `1` | +| `slave.antiAffinity` | Slave pod anti-affinity policy | `soft` | +| `slave.persistence.enabled` | Enable persistence using a `PersistentVolumeClaim` | `true` | +| `slave.persistence.annotations` | Persistent Volume Claim annotations | `{}` | +| `slave.persistence.storageClass` | Persistent Volume Storage Class | `` | +| `slave.persistence.accessModes` | Persistent Volume Access Modes | `[ReadWriteOnce]` | +| `slave.persistence.size` | Persistent Volume Size | `8Gi` | +| `slave.config` | Config file for the MariaDB Slave replicas | `_default values in the values.yaml file_` | +| `slave.resources` | CPU/Memory resource requests/limits for slave node | `{}` | +| `slave.livenessProbe.enabled` | Turn on and off liveness probe (slave) | `true` | +| `slave.livenessProbe.initialDelaySeconds` | Delay before liveness probe is initiated (slave) | `120` | +| `slave.livenessProbe.periodSeconds` | How often to perform the probe (slave) | `10` | +| `slave.livenessProbe.timeoutSeconds` | When the probe times out (slave) | `1` | +| `slave.livenessProbe.successThreshold` | Minimum consecutive successes for the probe (slave) | `1` | +| `slave.livenessProbe.failureThreshold` | Minimum consecutive failures for the probe (slave) | `3` | +| `slave.readinessProbe.enabled` | Turn on and off readiness probe (slave) | `true` | +| `slave.readinessProbe.initialDelaySeconds`| Delay before readiness probe is initiated (slave) | `15` | +| `slave.readinessProbe.periodSeconds` | How often to perform the probe (slave) | `10` | +| `slave.readinessProbe.timeoutSeconds` | When the probe times out (slave) | `1` | +| `slave.readinessProbe.successThreshold` | Minimum consecutive successes for the probe (slave) | `1` | +| `slave.readinessProbe.failureThreshold` | Minimum consecutive failures for the probe (slave) | `3` | +| `metrics.enabled` | Start a side-car prometheus exporter | `false` | +| `metrics.image.registry` | Exporter image registry | `docker.io` | +`metrics.image.repository` | Exporter image name | `prom/mysqld-exporter` | +| `metrics.image.tag` | Exporter image tag | `v0.10.0` | +| `metrics.image.pullPolicy` | Exporter image pull policy | `IfNotPresent` | +| `metrics.resources` | Exporter resource requests/limit | `nil` | + +The above parameters map to the env variables defined in [bitnami/mariadb](http://github.com/bitnami/bitnami-docker-mariadb). For more information please refer to the [bitnami/mariadb](http://github.com/bitnami/bitnami-docker-mariadb) image documentation. + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, + +```bash +$ helm install --name my-release \ + --set root.password=secretpassword,user.database=app_database \ + stable/mariadb +``` + +The above command sets the MariaDB `root` account password to `secretpassword`. Additionally it creates a database named `my_database`. + +Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, + +```bash +$ helm install --name my-release -f values.yaml stable/mariadb +``` + +> **Tip**: You can use the default [values.yaml](values.yaml) + +## Initialize a fresh instance + +The [Bitnami MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) image allows you to use your custom scripts to initialize a fresh instance. In order to execute the scripts, they must be located inside the chart folder `files/docker-entrypoint-initdb.d` so they can be consumed as a ConfigMap. + +The allowed extensions are `.sh`, `.sql` and `.sql.gz`. + +## Persistence + +The [Bitnami MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) image stores the MariaDB data and configurations at the `/bitnami/mariadb` path of the container. + +The chart mounts a [Persistent Volume](kubernetes.io/docs/user-guide/persistent-volumes/) volume at this location. The volume is created using dynamic volume provisioning, by default. An existing PersistentVolumeClaim can be defined. diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/files/docker-entrypoint-initdb.d/README.md b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/files/docker-entrypoint-initdb.d/README.md new file mode 100755 index 00000000000..aaddde3030f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/files/docker-entrypoint-initdb.d/README.md @@ -0,0 +1,3 @@ +You can copy here your custom .sh, .sql or .sql.gz file so they are executed during the first boot of the image. + +More info in the [bitnami-docker-mariadb](https://github.com/bitnami/bitnami-docker-mariadb#initializing-a-new-instance) repository. \ No newline at end of file diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/NOTES.txt b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/NOTES.txt new file mode 100755 index 00000000000..4ba3b668ad2 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/NOTES.txt @@ -0,0 +1,35 @@ + +Please be patient while the chart is being deployed + +Tip: + + Watch the deployment status using the command: kubectl get pods -w --namespace {{ .Release.Namespace }} -l release={{ .Release.Name }} + +Services: + + echo Master: {{ template "mariadb.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }} +{{- if .Values.replication.enabled }} + echo Slave: {{ template "slave.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }} +{{- end }} + +Administrator credentials: + + Username: root + Password : $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "mariadb.fullname" . }} -o jsonpath="{.data.mariadb-root-password}" | base64 --decode) + +To connect to your database + + 1. Run a pod that you can use as a client: + + kubectl run {{ template "mariadb.fullname" . }}-client --rm --tty -i --image {{ template "mariadb.image" . }} --namespace {{ .Release.Namespace }} --command -- bash + + 2. To connect to master service (read/write): + + mysql -h {{ template "mariadb.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local -uroot -p {{ .Values.db.name }} + +{{- if .Values.replication.enabled }} + + 3. To connect to slave service (read-only): + + mysql -h {{ template "slave.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local -uroot -p {{ .Values.db.name }} +{{- end }} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/_helpers.tpl b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/_helpers.tpl new file mode 100755 index 00000000000..5afe380ffe1 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/_helpers.tpl @@ -0,0 +1,53 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "mariadb.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "mariadb.fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "master.fullname" -}} +{{- if .Values.replication.enabled -}} +{{- printf "%s-%s" .Release.Name "mariadb-master" | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name "mariadb" | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + + +{{- define "slave.fullname" -}} +{{- printf "%s-%s" .Release.Name "mariadb-slave" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "mariadb.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Return the proper image name +*/}} +{{- define "mariadb.image" -}} +{{- $registryName := .Values.image.registry -}} +{{- $repositoryName := .Values.image.repository -}} +{{- $tag := .Values.image.tag | toString -}} +{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} +{{- end -}} + +{{/* +Return the proper image name +*/}} +{{- define "metrics.image" -}} +{{- $registryName := .Values.metrics.image.registry -}} +{{- $repositoryName := .Values.metrics.image.repository -}} +{{- $tag := .Values.metrics.image.tag | toString -}} +{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} +{{- end -}} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/initialization-configmap.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/initialization-configmap.yaml new file mode 100755 index 00000000000..7bb969627ee --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/initialization-configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "master.fullname" . }}-init-scripts + labels: + app: {{ template "mariadb.name" . }} + component: "master" + chart: {{ template "mariadb.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +data: +{{ (.Files.Glob "files/docker-entrypoint-initdb.d/*").AsConfig | indent 2 }} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-configmap.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-configmap.yaml new file mode 100755 index 00000000000..880a10198da --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-configmap.yaml @@ -0,0 +1,15 @@ +{{- if .Values.master.config }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "master.fullname" . }} + labels: + app: {{ template "mariadb.name" . }} + component: "master" + chart: {{ template "mariadb.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +data: + my.cnf: |- +{{ .Values.master.config | indent 4 }} +{{- end -}} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-statefulset.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-statefulset.yaml new file mode 100755 index 00000000000..0d74f01ff91 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-statefulset.yaml @@ -0,0 +1,187 @@ +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: {{ template "master.fullname" . }} + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + component: "master" + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +spec: + serviceName: "{{ template "master.fullname" . }}" + replicas: 1 + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + app: "{{ template "mariadb.name" . }}" + component: "master" + release: "{{ .Release.Name }}" + chart: {{ template "mariadb.chart" . }} + spec: + securityContext: + runAsUser: 1001 + fsGroup: 1001 + {{- if eq .Values.master.antiAffinity "hard" }} + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: "kubernetes.io/hostname" + labelSelector: + matchLabels: + app: "{{ template "mariadb.name" . }}" + release: "{{ .Release.Name }}" + {{- else if eq .Values.master.antiAffinity "soft" }} + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app: "{{ template "mariadb.name" . }}" + release: "{{ .Release.Name }}" + {{- end }} + {{- if .Values.image.pullSecrets }} + imagePullSecrets: + {{- range .Values.image.pullSecrets }} + - name: {{ . }} + {{- end}} + {{- end }} + containers: + - name: "mariadb" + image: {{ template "mariadb.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + {{- if .Values.db.user }} + - name: MARIADB_USER + value: "{{ .Values.db.user }}" + - name: MARIADB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-password + {{- end }} + - name: MARIADB_DATABASE + value: "{{ .Values.db.name }}" + {{- if .Values.replication.enabled }} + - name: MARIADB_REPLICATION_MODE + value: "master" + - name: MARIADB_REPLICATION_USER + value: "{{ .Values.replication.user }}" + - name: MARIADB_REPLICATION_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-replication-password + {{- end }} + ports: + - name: mysql + containerPort: 3306 + {{- if .Values.master.livenessProbe.enabled }} + livenessProbe: + exec: + command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"] + initialDelaySeconds: {{ .Values.master.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.master.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.master.livenessProbe.timeoutSeconds }} + successThreshold: {{ .Values.master.livenessProbe.successThreshold }} + failureThreshold: {{ .Values.master.livenessProbe.failureThreshold }} + {{- end }} + {{- if .Values.master.readinessProbe.enabled }} + readinessProbe: + exec: + command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"] + initialDelaySeconds: {{ .Values.master.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.master.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.master.readinessProbe.timeoutSeconds }} + successThreshold: {{ .Values.master.readinessProbe.successThreshold }} + failureThreshold: {{ .Values.master.readinessProbe.failureThreshold }} + {{- end }} + resources: +{{ toYaml .Values.master.resources | indent 10 }} + volumeMounts: + - name: data + mountPath: /bitnami/mariadb + - name: custom-init-scripts + mountPath: /docker-entrypoint-initdb.d +{{- if .Values.master.config }} + - name: config + mountPath: /opt/bitnami/mariadb/conf/my.cnf + subPath: my.cnf +{{- end }} +{{- if .Values.metrics.enabled }} + - name: metrics + image: {{ template "metrics.image" . }} + imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }} + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + command: [ 'sh', '-c', 'DATA_SOURCE_NAME="root:$MARIADB_ROOT_PASSWORD@(localhost:3306)/" /bin/mysqld_exporter' ] + ports: + - name: metrics + containerPort: 9104 + livenessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 15 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 5 + timeoutSeconds: 1 + resources: +{{ toYaml .Values.metrics.resources | indent 10 }} +{{- end }} + volumes: + {{- if .Values.master.config }} + - name: config + configMap: + name: {{ template "master.fullname" . }} + {{- end }} + - name: custom-init-scripts + configMap: + name: {{ template "master.fullname" . }}-init-scripts +{{- if .Values.master.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + component: "master" + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} + spec: + accessModes: + {{- range .Values.master.persistence.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.master.persistence.size | quote }} + {{- if .Values.master.persistence.storageClass }} + {{- if (eq "-" .Values.master.persistence.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: {{ .Values.master.persistence.storageClass | quote }} + {{- end }} + {{- end }} +{{- else }} + - name: "data" + emptyDir: {} +{{- end }} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-svc.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-svc.yaml new file mode 100755 index 00000000000..460ec328eda --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/master-svc.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "mariadb.fullname" . }} + labels: + app: "{{ template "mariadb.name" . }}" + component: "master" + chart: {{ template "mariadb.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +{{- if .Values.metrics.enabled }} + annotations: +{{ toYaml .Values.metrics.annotations | indent 4 }} +{{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - name: mysql + port: {{ .Values.service.port }} + targetPort: mysql +{{- if .Values.metrics.enabled }} + - name: metrics + port: 9104 + targetPort: metrics +{{- end }} + selector: + app: "{{ template "mariadb.name" . }}" + component: "master" + release: "{{ .Release.Name }}" diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/secrets.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/secrets.yaml new file mode 100755 index 00000000000..17999d609b4 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/secrets.yaml @@ -0,0 +1,38 @@ +{{- if (not .Values.rootUser.existingSecret) -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "mariadb.fullname" . }} + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +type: Opaque +data: + {{- if .Values.rootUser.password }} + mariadb-root-password: "{{ .Values.rootUser.password | b64enc }}" + {{- else if (not .Values.rootUser.forcePassword) }} + mariadb-root-password: "{{ randAlphaNum 10 | b64enc }}" + {{ else }} + mariadb-root-password: {{ required "A MariaDB Root Password is required!" .Values.rootUser.password }} + {{- end }} + {{- if .Values.db.user }} + {{- if .Values.db.password }} + mariadb-password: "{{ .Values.db.password | b64enc }}" + {{- else if (not .Values.db.forcePassword) }} + mariadb-password: "{{ randAlphaNum 10 | b64enc }}" + {{- else }} + mariadb-password: {{ required "A MariaDB Database Password is required!" .Values.db.password }} + {{- end }} + {{- end }} + {{- if .Values.replication.enabled }} + {{- if .Values.replication.password }} + mariadb-replication-password: "{{ .Values.replication.password | b64enc }}" + {{- else if (not .Values.replication.forcePassword) }} + mariadb-replication-password: "{{ randAlphaNum 10 | b64enc }}" + {{- else }} + mariadb-replication-password: {{ required "A MariaDB Replication Password is required!" .Values.replication.password }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-configmap.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-configmap.yaml new file mode 100755 index 00000000000..056cf5c0700 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-configmap.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.replication.enabled .Values.slave.config }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "slave.fullname" . }} + labels: + app: {{ template "mariadb.name" . }} + component: "slave" + chart: {{ template "mariadb.chart" . }} + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +data: + my.cnf: |- +{{ .Values.slave.config | indent 4 }} +{{- end }} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-statefulset.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-statefulset.yaml new file mode 100755 index 00000000000..aa67d4a7099 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-statefulset.yaml @@ -0,0 +1,193 @@ +{{- if .Values.replication.enabled }} +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: {{ template "slave.fullname" . }} + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + component: "slave" + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +spec: + serviceName: "{{ template "slave.fullname" . }}" + replicas: {{ .Values.slave.replicas }} + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + app: "{{ template "mariadb.name" . }}" + component: "slave" + release: "{{ .Release.Name }}" + chart: {{ template "mariadb.chart" . }} + spec: + securityContext: + runAsUser: 1001 + fsGroup: 1001 + {{- if eq .Values.slave.antiAffinity "hard" }} + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - topologyKey: "kubernetes.io/hostname" + labelSelector: + matchLabels: + app: "{{ template "mariadb.name" . }}" + release: "{{ .Release.Name }}" + {{- else if eq .Values.slave.antiAffinity "soft" }} + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 1 + podAffinityTerm: + topologyKey: kubernetes.io/hostname + labelSelector: + matchLabels: + app: "{{ template "mariadb.name" . }}" + release: "{{ .Release.Name }}" + {{- end }} + {{- if .Values.image.pullSecrets }} + imagePullSecrets: + {{- range .Values.image.pullSecrets }} + - name: {{ . }} + {{- end}} + {{- end }} + containers: + - name: "mariadb" + image: {{ template "mariadb.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + {{- if .Values.db.user }} + - name: MARIADB_USER + value: "{{ .Values.db.user }}" + - name: MARIADB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-password + {{- end }} + - name: MARIADB_DATABASE + value: "{{ .Values.db.name }}" + - name: MARIADB_REPLICATION_MODE + value: "slave" + - name: MARIADB_MASTER_HOST + value: {{ template "mariadb.fullname" . }} + - name: MARIADB_MASTER_PORT + value: "3306" + - name: MARIADB_MASTER_USER + value: "root" + - name: MARIADB_MASTER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + - name: MARIADB_REPLICATION_USER + value: "{{ .Values.replication.user }}" + - name: MARIADB_REPLICATION_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-replication-password + ports: + - name: mysql + containerPort: 3306 + {{- if .Values.slave.livenessProbe.enabled }} + livenessProbe: + exec: + command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"] + initialDelaySeconds: {{ .Values.slave.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.slave.livenessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.slave.livenessProbe.timeoutSeconds }} + successThreshold: {{ .Values.slave.livenessProbe.successThreshold }} + failureThreshold: {{ .Values.slave.livenessProbe.failureThreshold }} + {{- end }} + {{- if .Values.slave.readinessProbe.enabled }} + readinessProbe: + exec: + command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"] + initialDelaySeconds: {{ .Values.slave.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.slave.readinessProbe.periodSeconds }} + timeoutSeconds: {{ .Values.slave.readinessProbe.timeoutSeconds }} + successThreshold: {{ .Values.slave.readinessProbe.successThreshold }} + failureThreshold: {{ .Values.slave.readinessProbe.failureThreshold }} + {{- end }} + resources: +{{ toYaml .Values.slave.resources | indent 10 }} + volumeMounts: + - name: data + mountPath: /bitnami/mariadb +{{- if .Values.slave.config }} + - name: config + mountPath: /opt/bitnami/mariadb/conf/my.cnf + subPath: my.cnf +{{- end }} +{{- if .Values.metrics.enabled }} + - name: metrics + image: {{ template "metrics.image" . }} + imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }} + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + command: [ 'sh', '-c', 'DATA_SOURCE_NAME="root:$MARIADB_ROOT_PASSWORD@(localhost:3306)/" /bin/mysqld_exporter' ] + ports: + - name: metrics + containerPort: 9104 + livenessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 15 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /metrics + port: metrics + initialDelaySeconds: 5 + timeoutSeconds: 1 + resources: +{{ toYaml .Values.metrics.resources | indent 10 }} +{{- end }} + volumes: + {{- if .Values.slave.config }} + - name: config + configMap: + name: {{ template "slave.fullname" . }} + {{- end }} +{{- if .Values.slave.persistence.enabled }} + volumeClaimTemplates: + - metadata: + name: data + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + component: "slave" + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} + spec: + accessModes: + {{- range .Values.slave.persistence.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.slave.persistence.size | quote }} + {{- if .Values.slave.persistence.storageClass }} + {{- if (eq "-" .Values.slave.persistence.storageClass) }} + storageClassName: "" + {{- else }} + storageClassName: {{ .Values.slave.persistence.storageClass | quote }} + {{- end }} + {{- end }} +{{- else }} + - name: "data" + emptyDir: {} +{{- end }} +{{- end }} diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-svc.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-svc.yaml new file mode 100755 index 00000000000..fa551371f85 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/slave-svc.yaml @@ -0,0 +1,31 @@ +{{- if .Values.replication.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ template "slave.fullname" . }} + labels: + app: "{{ template "mariadb.name" . }}" + chart: {{ template "mariadb.chart" . }} + component: "slave" + release: {{ .Release.Name | quote }} + heritage: {{ .Release.Service | quote }} +{{- if .Values.metrics.enabled }} + annotations: +{{ toYaml .Values.metrics.annotations | indent 4 }} +{{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - name: mysql + port: {{ .Values.service.port }} + targetPort: mysql +{{- if .Values.metrics.enabled }} + - name: metrics + port: 9104 + targetPort: metrics +{{- end }} + selector: + app: "{{ template "mariadb.name" . }}" + component: "slave" + release: "{{ .Release.Name }}" +{{- end }} \ No newline at end of file diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/test-runner.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/test-runner.yaml new file mode 100755 index 00000000000..99a85d4aaa0 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/test-runner.yaml @@ -0,0 +1,44 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ template "mariadb.fullname" . }}-test-{{ randAlphaNum 5 | lower }}" + annotations: + "helm.sh/hook": test-success +spec: + initContainers: + - name: "test-framework" + image: "dduportal/bats:0.4.0" + command: + - "bash" + - "-c" + - | + set -ex + # copy bats to tools dir + cp -R /usr/local/libexec/ /tools/bats/ + volumeMounts: + - mountPath: /tools + name: tools + containers: + - name: mariadb-test + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + command: ["/tools/bats/bats", "-t", "/tests/run.sh"] + env: + - name: MARIADB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mariadb.fullname" . }} + key: mariadb-root-password + volumeMounts: + - mountPath: /tests + name: tests + readOnly: true + - mountPath: /tools + name: tools + volumes: + - name: tests + configMap: + name: {{ template "mariadb.fullname" . }}-tests + - name: tools + emptyDir: {} + restartPolicy: Never diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/tests.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/tests.yaml new file mode 100755 index 00000000000..957f3fd1e03 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/templates/tests.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "mariadb.fullname" . }}-tests +data: + run.sh: |- + @test "Testing MariaDB is accessible" { + mysql -h {{ template "mariadb.fullname" . }} -uroot -p$MARIADB_ROOT_PASSWORD -e 'show databases;' + } diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/values.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/values.yaml new file mode 100755 index 00000000000..ce2414e9f70 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/charts/mariadb/values.yaml @@ -0,0 +1,233 @@ +## Bitnami MariaDB image +## ref: https://hub.docker.com/r/bitnami/mariadb/tags/ +## +image: + registry: docker.io + repository: bitnami/mariadb + tag: 10.1.34-debian-9 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistrKeySecretName + +service: + ## Kubernetes service type + type: ClusterIP + port: 3306 + +rootUser: + ## MariaDB admin password + ## ref: https://github.com/bitnami/bitnami-docker-mariadb#setting-the-root-password-on-first-run + ## + password: + ## Use existing secret (ignores root, db and replication passwords) + # existingSecret: + ## + ## Option to force users to specify a password. That is required for 'helm upgrade' to work properly. + ## If it is not force, a random password will be generated. + forcePassword: false + +db: + ## MariaDB username and password + ## ref: https://github.com/bitnami/bitnami-docker-mariadb#creating-a-database-user-on-first-run + ## + user: + password: + ## Password is ignored if existingSecret is specified. + ## Database to create + ## ref: https://github.com/bitnami/bitnami-docker-mariadb#creating-a-database-on-first-run + ## + name: my_database + ## Option to force users to specify a password. That is required for 'helm upgrade' to work properly. + ## If it is not force, a random password will be generated. + forcePassword: false + +replication: + ## Enable replication. This enables the creation of replicas of MariaDB. If false, only a + ## master deployment would be created + enabled: true + ## + ## MariaDB replication user + ## ref: https://github.com/bitnami/bitnami-docker-mariadb#setting-up-a-replication-cluster + ## + user: replicator + ## MariaDB replication user password + ## ref: https://github.com/bitnami/bitnami-docker-mariadb#setting-up-a-replication-cluster + ## + password: + ## Password is ignored if existingSecret is specified. + ## + ## Option to force users to specify a password. That is required for 'helm upgrade' to work properly. + ## If it is not force, a random password will be generated. + forcePassword: false + +master: + antiAffinity: soft + ## Enable persistence using Persistent Volume Claims + ## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ + ## + persistence: + ## If true, use a Persistent Volume Claim, If false, use emptyDir + ## + enabled: true + ## Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## Persistent Volume Claim annotations + ## + annotations: + ## Persistent Volume Access Mode + ## + accessModes: + - ReadWriteOnce + ## Persistent Volume size + ## + size: 8Gi + ## + + ## Configure MySQL with a custom my.cnf file + ## ref: https://mysql.com/kb/en/mysql/configuring-mysql-with-mycnf/#example-of-configuration-file + ## + config: |- + [mysqld] + skip-name-resolve + explicit_defaults_for_timestamp + basedir=/opt/bitnami/mariadb + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + tmpdir=/opt/bitnami/mariadb/tmp + max_allowed_packet=16M + bind-address=0.0.0.0 + pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid + log-error=/opt/bitnami/mariadb/logs/mysqld.log + character-set-server=UTF8 + collation-server=utf8_general_ci + + [client] + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + default-character-set=UTF8 + + [manager] + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid + + ## Configure master resource requests and limits + ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ + ## + resources: {} + livenessProbe: + enabled: true + ## + ## Initializing the database could take some time + initialDelaySeconds: 120 + ## + ## Default Kubernetes values + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + enabled: true + initialDelaySeconds: 15 + ## + ## Default Kubernetes values + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + +slave: + replicas: 1 + antiAffinity: soft + persistence: + ## If true, use a Persistent Volume Claim, If false, use emptyDir + ## + enabled: true + # storageClass: "-" + annotations: + accessModes: + - ReadWriteOnce + ## Persistent Volume size + ## + size: 8Gi + ## + + ## Configure MySQL slave with a custom my.cnf file + ## ref: https://mysql.com/kb/en/mysql/configuring-mysql-with-mycnf/#example-of-configuration-file + ## + config: |- + [mysqld] + skip-name-resolve + explicit_defaults_for_timestamp + basedir=/opt/bitnami/mariadb + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + tmpdir=/opt/bitnami/mariadb/tmp + max_allowed_packet=16M + bind-address=0.0.0.0 + pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid + log-error=/opt/bitnami/mariadb/logs/mysqld.log + character-set-server=UTF8 + collation-server=utf8_general_ci + + [client] + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + default-character-set=UTF8 + + [manager] + port=3306 + socket=/opt/bitnami/mariadb/tmp/mysql.sock + pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid + + ## + ## Configure slave resource requests and limits + ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ + ## + resources: {} + livenessProbe: + enabled: true + ## + ## Initializing the database could take some time + initialDelaySeconds: 120 + ## + ## Default Kubernetes values + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + enabled: true + initialDelaySeconds: 15 + ## + ## Default Kubernetes values + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + +metrics: + enabled: false + image: + registry: docker.io + repository: prom/mysqld-exporter + tag: v0.10.0 + pullPolicy: IfNotPresent + resources: {} + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9104" diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.lock b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.lock new file mode 100755 index 00000000000..dcda2b1420f --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: mariadb + repository: https://charts.helm.sh/stable/ + version: 4.3.1 +digest: sha256:82a0e5374376169d2ecf7d452c18a2ed93507f5d17c3393a1457f9ffad7e9b26 +generated: 2018-08-02T22:07:51.905271776Z diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.yaml new file mode 100755 index 00000000000..fef7d0b7f22 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/requirements.yaml @@ -0,0 +1,7 @@ +dependencies: +- name: mariadb + version: 4.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - wordpress-database diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/templates/NOTES.txt b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/templates/NOTES.txt new file mode 100755 index 00000000000..75ed9b64ff3 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/templates/NOTES.txt @@ -0,0 +1 @@ +Placeholder. diff --git a/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/values.yaml b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/values.yaml new file mode 100755 index 00000000000..3cb66dafdac --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/values.yaml @@ -0,0 +1,254 @@ +## Bitnami WordPress image version +## ref: https://hub.docker.com/r/bitnami/wordpress/tags/ +## +image: + registry: docker.io + repository: bitnami/wordpress + tag: 4.9.8-debian-9 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistrKeySecretName + +## User of the application +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressUsername: user + +## Application password +## Defaults to a random 10-character alphanumeric string if not set +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +# wordpressPassword: + +## Admin email +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressEmail: user@example.com + +## First name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressFirstName: FirstName + +## Last name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressLastName: LastName + +## Blog name +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressBlogName: User's Blog! + +## Table prefix +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +## +wordpressTablePrefix: wp_ + +## Set to `yes` to allow the container to be started with blank passwords +## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables +allowEmptyPassword: yes + +## SMTP mail delivery configuration +## ref: https://github.com/bitnami/bitnami-docker-wordpress/#smtp-configuration +## +# smtpHost: +# smtpPort: +# smtpUser: +# smtpPassword: +# smtpUsername: +# smtpProtocol: + +replicaCount: 1 + +externalDatabase: +## All of these values are only used when mariadb.enabled is set to false + ## Database host + host: localhost + + ## non-root Username for Wordpress Database + user: bn_wordpress + + ## Database password + password: "" + + ## Database name + database: bitnami_wordpress + + ## Database port number + port: 3306 + +## +## MariaDB chart configuration +## +mariadb: + ## Whether to deploy a mariadb server to satisfy the applications database requirements. To use an external database set this to false and configure the externalDatabase parameters + enabled: true + ## Disable MariaDB replication + replication: + enabled: false + + ## Create a database and a database user + ## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-user-on-first-run + ## + db: + name: bitnami_wordpress + user: bn_wordpress + ## If the password is not specified, mariadb will generates a random password + ## + # password: + + ## MariaDB admin password + ## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#setting-the-root-password-on-first-run + ## + # rootUser: + # password: + + ## Enable persistence using Persistent Volume Claims + ## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ + ## + master: + persistence: + enabled: true + ## mariadb data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + accessMode: ReadWriteOnce + size: 8Gi + +## Kubernetes configuration +## For minikube, set this to NodePort, elsewhere use LoadBalancer or ClusterIP +## +serviceType: LoadBalancer +## +## serviceType: NodePort +## nodePorts: +## http: +## https: +nodePorts: + http: "" + https: "" +## Enable client source IP preservation +## ref http://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip +## +serviceExternalTrafficPolicy: Cluster + +## Allow health checks to be pointed at the https port +healthcheckHttps: false + +## Configure extra options for liveness and readiness probes +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes) +livenessProbe: + initialDelaySeconds: 120 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 +readinessProbe: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + successThreshold: 1 + +## Configure the ingress resource that allows you to access the +## Wordpress installation. Set up the URL +## ref: http://kubernetes.io/docs/user-guide/ingress/ +## +ingress: + ## Set to true to enable ingress record generation + enabled: false + + ## The list of hostnames to be covered with this ingress record. + ## Most likely this will be just one host, but in the event more hosts are needed, this is an array + hosts: + - name: wordpress.local + + ## Set this to true in order to enable TLS on the ingress record + ## A side effect of this will be that the backend wordpress service will be connected at port 443 + tls: false + + ## If TLS is set to true, you must declare what secret will store the key/certificate for TLS + tlsSecret: wordpress.local-tls + + ## Ingress annotations done as key:value pairs + ## If you're using kube-lego, you will want to add: + ## kubernetes.io/tls-acme: true + ## + ## For a full list of possible ingress annotations, please see + ## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/annotations.md + ## + ## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set + annotations: + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: true + + secrets: + ## If you're providing your own certificates, please use this to add the certificates as secrets + ## key and certificate should start with -----BEGIN CERTIFICATE----- or + ## -----BEGIN RSA PRIVATE KEY----- + ## + ## name should line up with a tlsSecret set further up + ## If you're using kube-lego, this is unneeded, as it will create the secret for you if it is not set + ## + ## It is also possible to create and manage the certificates outside of this helm chart + ## Please see README.md for more information + # - name: wordpress.local-tls + # key: + # certificate: + +## Enable persistence using Persistent Volume Claims +## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ +## +persistence: + enabled: true + ## wordpress data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + ## + ## If you want to reuse an existing claim, you can pass the name of the PVC using + ## the existingClaim variable + # existingClaim: your-claim + accessMode: ReadWriteOnce + size: 10Gi + +## Configure resource requests and limits +## ref: http://kubernetes.io/docs/user-guide/compute-resources/ +## +resources: + requests: + memory: 512Mi + cpu: 300m + +## Node labels for pod assignment +## Ref: https://kubernetes.io/docs/user-guide/node-selection/ +## +nodeSelector: {} + +## Tolerations for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## +tolerations: [] + +## Affinity for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +## +affinity: {} diff --git a/pkg/action/testdata/charts/compressedchart-0.1.0.tar.gz b/pkg/action/testdata/charts/compressedchart-0.1.0.tar.gz new file mode 100644 index 00000000000..3c9c24d7606 Binary files /dev/null and b/pkg/action/testdata/charts/compressedchart-0.1.0.tar.gz differ diff --git a/pkg/action/testdata/charts/compressedchart-0.1.0.tgz b/pkg/action/testdata/charts/compressedchart-0.1.0.tgz new file mode 100644 index 00000000000..3c9c24d7606 Binary files /dev/null and b/pkg/action/testdata/charts/compressedchart-0.1.0.tgz differ diff --git a/pkg/action/testdata/charts/compressedchart-0.2.0.tgz b/pkg/action/testdata/charts/compressedchart-0.2.0.tgz new file mode 100644 index 00000000000..16a644a796f Binary files /dev/null and b/pkg/action/testdata/charts/compressedchart-0.2.0.tgz differ diff --git a/pkg/action/testdata/charts/compressedchart-0.3.0.tgz b/pkg/action/testdata/charts/compressedchart-0.3.0.tgz new file mode 100644 index 00000000000..051bd6fd9a0 Binary files /dev/null and b/pkg/action/testdata/charts/compressedchart-0.3.0.tgz differ diff --git a/pkg/action/testdata/charts/compressedchart-with-hyphens-0.1.0.tgz b/pkg/action/testdata/charts/compressedchart-with-hyphens-0.1.0.tgz new file mode 100644 index 00000000000..379210a92c1 Binary files /dev/null and b/pkg/action/testdata/charts/compressedchart-with-hyphens-0.1.0.tgz differ diff --git a/pkg/action/testdata/charts/corrupted-compressed-chart.tgz b/pkg/action/testdata/charts/corrupted-compressed-chart.tgz new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/action/testdata/charts/decompressedchart/Chart.yaml b/pkg/action/testdata/charts/decompressedchart/Chart.yaml new file mode 100644 index 00000000000..92ba4d88fee --- /dev/null +++ b/pkg/action/testdata/charts/decompressedchart/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: A Helm chart for Kubernetes +name: decompressedchart +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/decompressedchart/values.yaml b/pkg/action/testdata/charts/decompressedchart/values.yaml similarity index 100% rename from cmd/helm/testdata/testcharts/decompressedchart/values.yaml rename to pkg/action/testdata/charts/decompressedchart/values.yaml diff --git a/pkg/action/testdata/charts/multiplecharts-lint-chart-1/Chart.yaml b/pkg/action/testdata/charts/multiplecharts-lint-chart-1/Chart.yaml new file mode 100644 index 00000000000..e33c97e8c36 --- /dev/null +++ b/pkg/action/testdata/charts/multiplecharts-lint-chart-1/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +name: multiplecharts-lint-chart-1 +version: "1" +icon: "" \ No newline at end of file diff --git a/pkg/action/testdata/charts/multiplecharts-lint-chart-1/templates/configmap.yaml b/pkg/action/testdata/charts/multiplecharts-lint-chart-1/templates/configmap.yaml new file mode 100644 index 00000000000..88ebf246886 --- /dev/null +++ b/pkg/action/testdata/charts/multiplecharts-lint-chart-1/templates/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +metadata: + name: multicharttest-chart1-configmap +data: + dat: | + {{ .Values.config | indent 4 }} diff --git a/pkg/action/testdata/charts/multiplecharts-lint-chart-1/values.yaml b/pkg/action/testdata/charts/multiplecharts-lint-chart-1/values.yaml new file mode 100644 index 00000000000..aafb09e4b3a --- /dev/null +++ b/pkg/action/testdata/charts/multiplecharts-lint-chart-1/values.yaml @@ -0,0 +1 @@ +config: "Test" \ No newline at end of file diff --git a/pkg/action/testdata/charts/multiplecharts-lint-chart-2/Chart.yaml b/pkg/action/testdata/charts/multiplecharts-lint-chart-2/Chart.yaml new file mode 100644 index 00000000000..b27de275429 --- /dev/null +++ b/pkg/action/testdata/charts/multiplecharts-lint-chart-2/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +name: multiplecharts-lint-chart-2 +version: "1" +icon: "" \ No newline at end of file diff --git a/pkg/action/testdata/charts/multiplecharts-lint-chart-2/templates/configmap.yaml b/pkg/action/testdata/charts/multiplecharts-lint-chart-2/templates/configmap.yaml new file mode 100644 index 00000000000..8484bfe6a7a --- /dev/null +++ b/pkg/action/testdata/charts/multiplecharts-lint-chart-2/templates/configmap.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +metadata: + name: multicharttest-chart2-configmap +data: + {{ toYaml .Values.config | indent 4 }} diff --git a/pkg/action/testdata/charts/multiplecharts-lint-chart-2/values.yaml b/pkg/action/testdata/charts/multiplecharts-lint-chart-2/values.yaml new file mode 100644 index 00000000000..9139f486e27 --- /dev/null +++ b/pkg/action/testdata/charts/multiplecharts-lint-chart-2/values.yaml @@ -0,0 +1,2 @@ +config: + test: "Test" \ No newline at end of file diff --git a/pkg/action/testdata/charts/pre-release-chart-0.1.0-alpha.tgz b/pkg/action/testdata/charts/pre-release-chart-0.1.0-alpha.tgz new file mode 100644 index 00000000000..5d5770fed22 Binary files /dev/null and b/pkg/action/testdata/charts/pre-release-chart-0.1.0-alpha.tgz differ diff --git a/pkg/action/testdata/output/list-compressed-deps-tgz.txt b/pkg/action/testdata/output/list-compressed-deps-tgz.txt new file mode 100644 index 00000000000..6cc526b70c0 --- /dev/null +++ b/pkg/action/testdata/output/list-compressed-deps-tgz.txt @@ -0,0 +1,3 @@ +NAME VERSION REPOSITORY STATUS +mariadb 4.x.x https://kubernetes-charts.storage.googleapis.com/ unpacked + diff --git a/pkg/action/testdata/output/list-compressed-deps.txt b/pkg/action/testdata/output/list-compressed-deps.txt new file mode 100644 index 00000000000..08597f31eca --- /dev/null +++ b/pkg/action/testdata/output/list-compressed-deps.txt @@ -0,0 +1,3 @@ +NAME VERSION REPOSITORY STATUS +mariadb 4.x.x https://charts.helm.sh/stable/ ok + diff --git a/pkg/action/testdata/output/list-missing-deps.txt b/pkg/action/testdata/output/list-missing-deps.txt new file mode 100644 index 00000000000..03051251e30 --- /dev/null +++ b/pkg/action/testdata/output/list-missing-deps.txt @@ -0,0 +1,3 @@ +NAME VERSION REPOSITORY STATUS +mariadb 4.x.x https://charts.helm.sh/stable/ missing + diff --git a/pkg/action/testdata/output/list-uncompressed-deps-tgz.txt b/pkg/action/testdata/output/list-uncompressed-deps-tgz.txt new file mode 100644 index 00000000000..6cc526b70c0 --- /dev/null +++ b/pkg/action/testdata/output/list-uncompressed-deps-tgz.txt @@ -0,0 +1,3 @@ +NAME VERSION REPOSITORY STATUS +mariadb 4.x.x https://kubernetes-charts.storage.googleapis.com/ unpacked + diff --git a/pkg/action/testdata/output/list-uncompressed-deps.txt b/pkg/action/testdata/output/list-uncompressed-deps.txt new file mode 100644 index 00000000000..bc59e825c9f --- /dev/null +++ b/pkg/action/testdata/output/list-uncompressed-deps.txt @@ -0,0 +1,3 @@ +NAME VERSION REPOSITORY STATUS +mariadb 4.x.x https://charts.helm.sh/stable/ unpacked + diff --git a/pkg/action/testdata/rbac.txt b/pkg/action/testdata/rbac.txt new file mode 100644 index 00000000000..0cb15b868e2 --- /dev/null +++ b/pkg/action/testdata/rbac.txt @@ -0,0 +1,25 @@ +--- +# Source: hello/templates/rbac +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: schedule-agents +rules: +- apiGroups: [""] + resources: ["pods", "pods/exec", "pods/log"] + verbs: ["*"] +--- +# Source: hello/templates/rbac +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: schedule-agents + namespace: spaced +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: schedule-agents +subjects: +- kind: ServiceAccount + name: schedule-agents + namespace: spaced diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go new file mode 100644 index 00000000000..c762159cbb2 --- /dev/null +++ b/pkg/action/uninstall.go @@ -0,0 +1,212 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "strings" + "time" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/releaseutil" + helmtime "helm.sh/helm/v3/pkg/time" +) + +// Uninstall is the action for uninstalling releases. +// +// It provides the implementation of 'helm uninstall'. +type Uninstall struct { + cfg *Configuration + + DisableHooks bool + DryRun bool + KeepHistory bool + Timeout time.Duration + Description string +} + +// NewUninstall creates a new Uninstall object with the given configuration. +func NewUninstall(cfg *Configuration) *Uninstall { + return &Uninstall{ + cfg: cfg, + } +} + +// Run uninstalls the given release. +func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) { + if err := u.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + + if u.DryRun { + // In the dry run case, just see if the release exists + r, err := u.cfg.releaseContent(name, 0) + if err != nil { + return &release.UninstallReleaseResponse{}, err + } + return &release.UninstallReleaseResponse{Release: r}, nil + } + + if err := chartutil.ValidateReleaseName(name); err != nil { + return nil, errors.Errorf("uninstall: Release name is invalid: %s", name) + } + + rels, err := u.cfg.Releases.History(name) + if err != nil { + return nil, errors.Wrapf(err, "uninstall: Release not loaded: %s", name) + } + if len(rels) < 1 { + return nil, errMissingRelease + } + + releaseutil.SortByRevision(rels) + rel := rels[len(rels)-1] + + // TODO: Are there any cases where we want to force a delete even if it's + // already marked deleted? + if rel.Info.Status == release.StatusUninstalled { + if !u.KeepHistory { + if err := u.purgeReleases(rels...); err != nil { + return nil, errors.Wrap(err, "uninstall: Failed to purge the release") + } + return &release.UninstallReleaseResponse{Release: rel}, nil + } + return nil, errors.Errorf("the release named %q is already deleted", name) + } + + u.cfg.Log("uninstall: Deleting %s", name) + rel.Info.Status = release.StatusUninstalling + rel.Info.Deleted = helmtime.Now() + rel.Info.Description = "Deletion in progress (or silently failed)" + res := &release.UninstallReleaseResponse{Release: rel} + + if !u.DisableHooks { + if err := u.cfg.execHook(rel, release.HookPreDelete, u.Timeout); err != nil { + return res, err + } + } else { + u.cfg.Log("delete hooks disabled for %s", name) + } + + // From here on out, the release is currently considered to be in StatusUninstalling + // state. + if err := u.cfg.Releases.Update(rel); err != nil { + u.cfg.Log("uninstall: Failed to store updated release: %s", err) + } + + kept, errs := u.deleteRelease(rel) + + if kept != "" { + kept = "These resources were kept due to the resource policy:\n" + kept + } + res.Info = kept + + if !u.DisableHooks { + if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil { + errs = append(errs, err) + } + } + + rel.Info.Status = release.StatusUninstalled + if len(u.Description) > 0 { + rel.Info.Description = u.Description + } else { + rel.Info.Description = "Uninstallation complete" + } + + if !u.KeepHistory { + u.cfg.Log("purge requested for %s", name) + err := u.purgeReleases(rels...) + if err != nil { + errs = append(errs, errors.Wrap(err, "uninstall: Failed to purge the release")) + } + + // Return the errors that occurred while deleting the release, if any + if len(errs) > 0 { + return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs)) + } + + return res, nil + } + + if err := u.cfg.Releases.Update(rel); err != nil { + u.cfg.Log("uninstall: Failed to store updated release: %s", err) + } + + if len(errs) > 0 { + return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs)) + } + return res, nil +} + +func (u *Uninstall) purgeReleases(rels ...*release.Release) error { + for _, rel := range rels { + if _, err := u.cfg.Releases.Delete(rel.Name, rel.Version); err != nil { + return err + } + } + return nil +} + +func joinErrors(errs []error) string { + es := make([]string, 0, len(errs)) + for _, e := range errs { + es = append(es, e.Error()) + } + return strings.Join(es, "; ") +} + +// deleteRelease deletes the release and returns manifests that were kept in the deletion process +func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) { + var errs []error + caps, err := u.cfg.getCapabilities() + if err != nil { + return rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")} + } + + manifests := releaseutil.SplitManifests(rel.Manifest) + _, files, err := releaseutil.SortManifests(manifests, caps.APIVersions, releaseutil.UninstallOrder) + if err != nil { + // We could instead just delete everything in no particular order. + // FIXME: One way to delete at this point would be to try a label-based + // deletion. The problem with this is that we could get a false positive + // and delete something that was not legitimately part of this release. + return rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")} + } + + filesToKeep, filesToDelete := filterManifestsToKeep(files) + var kept string + for _, f := range filesToKeep { + kept += "[" + f.Head.Kind + "] " + f.Head.Metadata.Name + "\n" + } + + var builder strings.Builder + for _, file := range filesToDelete { + builder.WriteString("\n---\n" + file.Content) + } + + resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false) + if err != nil { + return "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")} + } + if len(resources) > 0 { + _, errs = u.cfg.KubeClient.Delete(resources) + } + return kept, errs +} diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go new file mode 100644 index 00000000000..53c3bf8f9bf --- /dev/null +++ b/pkg/action/uninstall_test.go @@ -0,0 +1,62 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func uninstallAction(t *testing.T) *Uninstall { + config := actionConfigFixture(t) + unAction := NewUninstall(config) + return unAction +} + +func TestUninstallRelease_deleteRelease(t *testing.T) { + is := assert.New(t) + + unAction := uninstallAction(t) + unAction.DisableHooks = true + unAction.DryRun = false + unAction.KeepHistory = true + + rel := releaseStub() + rel.Name = "keep-secret" + rel.Manifest = `{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "secret", + "annotations": { + "helm.sh/resource-policy": "keep" + } + }, + "type": "Opaque", + "data": { + "password": "password" + } + }` + unAction.cfg.Releases.Create(rel) + res, err := unAction.Run(rel.Name) + is.NoError(err) + expected := `These resources were kept due to the resource policy: +[Secret] secret +` + is.Contains(res.Info, expected) +} diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go new file mode 100644 index 00000000000..3b3dd3f1c47 --- /dev/null +++ b/pkg/action/upgrade.go @@ -0,0 +1,509 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "bytes" + "context" + "fmt" + "strings" + "time" + + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/resource" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/kube" + "helm.sh/helm/v3/pkg/postrender" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/releaseutil" + "helm.sh/helm/v3/pkg/storage/driver" +) + +// Upgrade is the action for upgrading releases. +// +// It provides the implementation of 'helm upgrade'. +type Upgrade struct { + cfg *Configuration + + ChartPathOptions + + // Install is a purely informative flag that indicates whether this upgrade was done in "install" mode. + // + // Applications may use this to determine whether this Upgrade operation was done as part of a + // pure upgrade (Upgrade.Install == false) or as part of an install-or-upgrade operation + // (Upgrade.Install == true). + // + // Setting this to `true` will NOT cause `Upgrade` to perform an install if the release does not exist. + // That process must be handled by creating an Install action directly. See cmd/upgrade.go for an + // example of how this flag is used. + Install bool + // Devel indicates that the operation is done in devel mode. + Devel bool + // Namespace is the namespace in which this operation should be performed. + Namespace string + // SkipCRDs skips installing CRDs when install flag is enabled during upgrade + SkipCRDs bool + // Timeout is the timeout for this operation + Timeout time.Duration + // Wait determines whether the wait operation should be performed after the upgrade is requested. + Wait bool + // WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested. + WaitForJobs bool + // DisableHooks disables hook processing if set to true. + DisableHooks bool + // DryRun controls whether the operation is prepared, but not executed. + // If `true`, the upgrade is prepared but not performed. + DryRun bool + // Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. + // + // This should be used with caution. + Force bool + // ResetValues will reset the values to the chart's built-ins rather than merging with existing. + ResetValues bool + // ReuseValues will re-use the user's last supplied values. + ReuseValues bool + // Recreate will (if true) recreate pods after a rollback. + Recreate bool + // MaxHistory limits the maximum number of revisions saved per release + MaxHistory int + // Atomic, if true, will roll back on failure. + Atomic bool + // CleanupOnFail will, if true, cause the upgrade to delete newly-created resources on a failed update. + CleanupOnFail bool + // SubNotes determines whether sub-notes are rendered in the chart. + SubNotes bool + // Description is the description of this operation + Description string + // PostRender is an optional post-renderer + // + // If this is non-nil, then after templates are rendered, they will be sent to the + // post renderer before sending to the Kubernetes API server. + PostRenderer postrender.PostRenderer + // DisableOpenAPIValidation controls whether OpenAPI validation is enforced. + DisableOpenAPIValidation bool +} + +// NewUpgrade creates a new Upgrade object with the given configuration. +func NewUpgrade(cfg *Configuration) *Upgrade { + return &Upgrade{ + cfg: cfg, + } +} + +// Run executes the upgrade on the given release. +func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + if err := u.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + + // Make sure if Atomic is set, that wait is set as well. This makes it so + // the user doesn't have to specify both + u.Wait = u.Wait || u.Atomic + + if err := chartutil.ValidateReleaseName(name); err != nil { + return nil, errors.Errorf("release name is invalid: %s", name) + } + u.cfg.Log("preparing upgrade for %s", name) + currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals) + if err != nil { + return nil, err + } + + u.cfg.Releases.MaxHistory = u.MaxHistory + + u.cfg.Log("performing update for %s", name) + res, err := u.performUpgrade(currentRelease, upgradedRelease) + if err != nil { + return res, err + } + + if !u.DryRun { + u.cfg.Log("updating status for upgraded release for %s", name) + if err := u.cfg.Releases.Update(upgradedRelease); err != nil { + return res, err + } + } + + return res, nil +} + +// prepareUpgrade builds an upgraded release for an upgrade operation. +func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) { + if chart == nil { + return nil, nil, errMissingChart + } + + // finds the last non-deleted release with the given name + lastRelease, err := u.cfg.Releases.Last(name) + if err != nil { + // to keep existing behavior of returning the "%q has no deployed releases" error when an existing release does not exist + if errors.Is(err, driver.ErrReleaseNotFound) { + return nil, nil, driver.NewErrNoDeployedReleases(name) + } + return nil, nil, err + } + + // Concurrent `helm upgrade`s will either fail here with `errPending` or when creating the release with "already exists". This should act as a pessimistic lock. + if lastRelease.Info.Status.IsPending() { + return nil, nil, errPending + } + + var currentRelease *release.Release + if lastRelease.Info.Status == release.StatusDeployed { + // no need to retrieve the last deployed release from storage as the last release is deployed + currentRelease = lastRelease + } else { + // finds the deployed release with the given name + currentRelease, err = u.cfg.Releases.Deployed(name) + if err != nil { + if errors.Is(err, driver.ErrNoDeployedReleases) && + (lastRelease.Info.Status == release.StatusFailed || lastRelease.Info.Status == release.StatusSuperseded) { + currentRelease = lastRelease + } else { + return nil, nil, err + } + } + } + + // determine if values will be reused + vals, err = u.reuseValues(chart, currentRelease, vals) + if err != nil { + return nil, nil, err + } + + if err := chartutil.ProcessDependencies(chart, vals); err != nil { + return nil, nil, err + } + + // Increment revision count. This is passed to templates, and also stored on + // the release object. + revision := lastRelease.Version + 1 + + options := chartutil.ReleaseOptions{ + Name: name, + Namespace: currentRelease.Namespace, + Revision: revision, + IsUpgrade: true, + } + + caps, err := u.cfg.getCapabilities() + if err != nil { + return nil, nil, err + } + valuesToRender, err := chartutil.ToRenderValues(chart, vals, options, caps) + if err != nil { + return nil, nil, err + } + + hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun) + if err != nil { + return nil, nil, err + } + + // Store an upgraded release. + upgradedRelease := &release.Release{ + Name: name, + Namespace: currentRelease.Namespace, + Chart: chart, + Config: vals, + Info: &release.Info{ + FirstDeployed: currentRelease.Info.FirstDeployed, + LastDeployed: Timestamper(), + Status: release.StatusPendingUpgrade, + Description: "Preparing upgrade", // This should be overwritten later. + }, + Version: revision, + Manifest: manifestDoc.String(), + Hooks: hooks, + } + + if len(notesTxt) > 0 { + upgradedRelease.Info.Notes = notesTxt + } + err = validateManifest(u.cfg.KubeClient, manifestDoc.Bytes(), !u.DisableOpenAPIValidation) + return currentRelease, upgradedRelease, err +} + +func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Release) (*release.Release, error) { + current, err := u.cfg.KubeClient.Build(bytes.NewBufferString(originalRelease.Manifest), false) + if err != nil { + // Checking for removed Kubernetes API error so can provide a more informative error message to the user + // Ref: https://github.com/helm/helm/issues/7219 + if strings.Contains(err.Error(), "unable to recognize \"\": no matches for kind") { + return upgradedRelease, errors.Wrap(err, "current release manifest contains removed kubernetes api(s) for this "+ + "kubernetes version and it is therefore unable to build the kubernetes "+ + "objects for performing the diff. error from kubernetes") + } + return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest") + } + target, err := u.cfg.KubeClient.Build(bytes.NewBufferString(upgradedRelease.Manifest), !u.DisableOpenAPIValidation) + if err != nil { + return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest") + } + + // It is safe to use force only on target because these are resources currently rendered by the chart. + err = target.Visit(setMetadataVisitor(upgradedRelease.Name, upgradedRelease.Namespace, true)) + if err != nil { + return upgradedRelease, err + } + + // Do a basic diff using gvk + name to figure out what new resources are being created so we can validate they don't already exist + existingResources := make(map[string]bool) + for _, r := range current { + existingResources[objectKey(r)] = true + } + + var toBeCreated kube.ResourceList + for _, r := range target { + if !existingResources[objectKey(r)] { + toBeCreated = append(toBeCreated, r) + } + } + + toBeUpdated, err := existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace) + if err != nil { + return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with update") + } + + toBeUpdated.Visit(func(r *resource.Info, err error) error { + if err != nil { + return err + } + current.Append(r) + return nil + }) + + if u.DryRun { + u.cfg.Log("dry run for %s", upgradedRelease.Name) + if len(u.Description) > 0 { + upgradedRelease.Info.Description = u.Description + } else { + upgradedRelease.Info.Description = "Dry run complete" + } + return upgradedRelease, nil + } + + u.cfg.Log("creating upgraded release for %s", upgradedRelease.Name) + if err := u.cfg.Releases.Create(upgradedRelease); err != nil { + return nil, err + } + + // pre-upgrade hooks + if !u.DisableHooks { + if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil { + return u.failRelease(upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err)) + } + } else { + u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name) + } + + results, err := u.cfg.KubeClient.Update(current, target, u.Force) + if err != nil { + u.cfg.recordRelease(originalRelease) + return u.failRelease(upgradedRelease, results.Created, err) + } + + if u.Recreate { + // NOTE: Because this is not critical for a release to succeed, we just + // log if an error occurs and continue onward. If we ever introduce log + // levels, we should make these error level logs so users are notified + // that they'll need to go do the cleanup on their own + if err := recreate(u.cfg, results.Updated); err != nil { + u.cfg.Log(err.Error()) + } + } + + if u.Wait { + if u.WaitForJobs { + if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil { + u.cfg.recordRelease(originalRelease) + return u.failRelease(upgradedRelease, results.Created, err) + } + } else { + if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil { + u.cfg.recordRelease(originalRelease) + return u.failRelease(upgradedRelease, results.Created, err) + } + } + } + + // post-upgrade hooks + if !u.DisableHooks { + if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil { + return u.failRelease(upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err)) + } + } + + originalRelease.Info.Status = release.StatusSuperseded + u.cfg.recordRelease(originalRelease) + + upgradedRelease.Info.Status = release.StatusDeployed + if len(u.Description) > 0 { + upgradedRelease.Info.Description = u.Description + } else { + upgradedRelease.Info.Description = "Upgrade complete" + } + + return upgradedRelease, nil +} + +func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) { + msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err) + u.cfg.Log("warning: %s", msg) + + rel.Info.Status = release.StatusFailed + rel.Info.Description = msg + u.cfg.recordRelease(rel) + if u.CleanupOnFail && len(created) > 0 { + u.cfg.Log("Cleanup on fail set, cleaning up %d resources", len(created)) + _, errs := u.cfg.KubeClient.Delete(created) + if errs != nil { + var errorList []string + for _, e := range errs { + errorList = append(errorList, e.Error()) + } + return rel, errors.Wrapf(fmt.Errorf("unable to cleanup resources: %s", strings.Join(errorList, ", ")), "an error occurred while cleaning up resources. original upgrade error: %s", err) + } + u.cfg.Log("Resource cleanup complete") + } + if u.Atomic { + u.cfg.Log("Upgrade failed and atomic is set, rolling back to last successful release") + + // As a protection, get the last successful release before rollback. + // If there are no successful releases, bail out + hist := NewHistory(u.cfg) + fullHistory, herr := hist.Run(rel.Name) + if herr != nil { + return rel, errors.Wrapf(herr, "an error occurred while finding last successful release. original upgrade error: %s", err) + } + + // There isn't a way to tell if a previous release was successful, but + // generally failed releases do not get superseded unless the next + // release is successful, so this should be relatively safe + filteredHistory := releaseutil.FilterFunc(func(r *release.Release) bool { + return r.Info.Status == release.StatusSuperseded || r.Info.Status == release.StatusDeployed + }).Filter(fullHistory) + if len(filteredHistory) == 0 { + return rel, errors.Wrap(err, "unable to find a previously successful release when attempting to rollback. original upgrade error") + } + + releaseutil.Reverse(filteredHistory, releaseutil.SortByRevision) + + rollin := NewRollback(u.cfg) + rollin.Version = filteredHistory[0].Version + rollin.Wait = true + rollin.WaitForJobs = u.WaitForJobs + rollin.DisableHooks = u.DisableHooks + rollin.Recreate = u.Recreate + rollin.Force = u.Force + rollin.Timeout = u.Timeout + if rollErr := rollin.Run(rel.Name); rollErr != nil { + return rel, errors.Wrapf(rollErr, "an error occurred while rolling back the release. original upgrade error: %s", err) + } + return rel, errors.Wrapf(err, "release %s failed, and has been rolled back due to atomic being set", rel.Name) + } + + return rel, err +} + +// reuseValues copies values from the current release to a new release if the +// new release does not have any values. +// +// If the request already has values, or if there are no values in the current +// release, this does nothing. +// +// This is skipped if the u.ResetValues flag is set, in which case the +// request values are not altered. +func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) { + if u.ResetValues { + // If ResetValues is set, we completely ignore current.Config. + u.cfg.Log("resetting values to the chart's original version") + return newVals, nil + } + + // If the ReuseValues flag is set, we always copy the old values over the new config's values. + if u.ReuseValues { + u.cfg.Log("reusing the old release's values") + + // We have to regenerate the old coalesced values: + oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config) + if err != nil { + return nil, errors.Wrap(err, "failed to rebuild old values") + } + + newVals = chartutil.CoalesceTables(newVals, current.Config) + + chart.Values = oldVals + + return newVals, nil + } + + if len(newVals) == 0 && len(current.Config) > 0 { + u.cfg.Log("copying values from %s (v%d) to new release.", current.Name, current.Version) + newVals = current.Config + } + return newVals, nil +} + +func validateManifest(c kube.Interface, manifest []byte, openAPIValidation bool) error { + _, err := c.Build(bytes.NewReader(manifest), openAPIValidation) + return err +} + +// recreate captures all the logic for recreating pods for both upgrade and +// rollback. If we end up refactoring rollback to use upgrade, this can just be +// made an unexported method on the upgrade action. +func recreate(cfg *Configuration, resources kube.ResourceList) error { + for _, res := range resources { + versioned := kube.AsVersioned(res) + selector, err := kube.SelectorsForObject(versioned) + if err != nil { + // If no selector is returned, it means this object is + // definitely not a pod, so continue onward + continue + } + + client, err := cfg.KubernetesClientSet() + if err != nil { + return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name) + } + + pods, err := client.CoreV1().Pods(res.Namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: selector.String(), + }) + if err != nil { + return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name) + } + + // Restart pods + for _, pod := range pods.Items { + // Delete each pod for get them restarted with changed spec. + if err := client.CoreV1().Pods(pod.Namespace).Delete(context.Background(), pod.Name, *metav1.NewPreconditionDeleteOptions(string(pod.UID))); err != nil { + return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name) + } + } + } + return nil +} + +func objectKey(r *resource.Info) string { + gvk := r.Object.GetObjectKind().GroupVersionKind() + return fmt.Sprintf("%s/%s/%s/%s", gvk.GroupVersion().String(), gvk.Kind, r.Namespace, r.Name) +} diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go new file mode 100644 index 00000000000..5cca7ca1a9b --- /dev/null +++ b/pkg/action/upgrade_test.go @@ -0,0 +1,298 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "fmt" + "testing" + + "helm.sh/helm/v3/pkg/chart" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/time" +) + +func upgradeAction(t *testing.T) *Upgrade { + config := actionConfigFixture(t) + upAction := NewUpgrade(config) + upAction.Namespace = "spaced" + + return upAction +} + +func TestUpgradeRelease_Wait(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "come-fail-away" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + upAction.cfg.KubeClient = failer + upAction.Wait = true + vals := map[string]interface{}{} + + res, err := upAction.Run(rel.Name, buildChart(), vals) + req.Error(err) + is.Contains(res.Info.Description, "I timed out") + is.Equal(res.Info.Status, release.StatusFailed) +} + +func TestUpgradeRelease_WaitForJobs(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "come-fail-away" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + upAction.cfg.KubeClient = failer + upAction.Wait = true + upAction.WaitForJobs = true + vals := map[string]interface{}{} + + res, err := upAction.Run(rel.Name, buildChart(), vals) + req.Error(err) + is.Contains(res.Info.Description, "I timed out") + is.Equal(res.Info.Status, release.StatusFailed) +} + +func TestUpgradeRelease_CleanupOnFail(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "come-fail-away" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("I timed out") + failer.DeleteError = fmt.Errorf("I tried to delete nil") + upAction.cfg.KubeClient = failer + upAction.Wait = true + upAction.CleanupOnFail = true + vals := map[string]interface{}{} + + res, err := upAction.Run(rel.Name, buildChart(), vals) + req.Error(err) + is.NotContains(err.Error(), "unable to cleanup resources") + is.Contains(res.Info.Description, "I timed out") + is.Equal(res.Info.Status, release.StatusFailed) +} + +func TestUpgradeRelease_Atomic(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + t.Run("atomic rollback succeeds", func(t *testing.T) { + upAction := upgradeAction(t) + + rel := releaseStub() + rel.Name = "nuketown" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + // We can't make Update error because then the rollback won't work + failer.WatchUntilReadyError = fmt.Errorf("arming key removed") + upAction.cfg.KubeClient = failer + upAction.Atomic = true + vals := map[string]interface{}{} + + res, err := upAction.Run(rel.Name, buildChart(), vals) + req.Error(err) + is.Contains(err.Error(), "arming key removed") + is.Contains(err.Error(), "atomic") + + // Now make sure it is actually upgraded + updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3) + is.NoError(err) + // Should have rolled back to the previous + is.Equal(updatedRes.Info.Status, release.StatusDeployed) + }) + + t.Run("atomic uninstall fails", func(t *testing.T) { + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "fallout" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.UpdateError = fmt.Errorf("update fail") + upAction.cfg.KubeClient = failer + upAction.Atomic = true + vals := map[string]interface{}{} + + _, err := upAction.Run(rel.Name, buildChart(), vals) + req.Error(err) + is.Contains(err.Error(), "update fail") + is.Contains(err.Error(), "an error occurred while rolling back the release") + }) +} + +func TestUpgradeRelease_ReuseValues(t *testing.T) { + is := assert.New(t) + + t.Run("reuse values should work with values", func(t *testing.T) { + upAction := upgradeAction(t) + + existingValues := map[string]interface{}{ + "name": "value", + "maxHeapSize": "128m", + "replicas": 2, + } + newValues := map[string]interface{}{ + "name": "newValue", + "maxHeapSize": "512m", + "cpu": "12m", + } + expectedValues := map[string]interface{}{ + "name": "newValue", + "maxHeapSize": "512m", + "cpu": "12m", + "replicas": 2, + } + + rel := releaseStub() + rel.Name = "nuketown" + rel.Info.Status = release.StatusDeployed + rel.Config = existingValues + + err := upAction.cfg.Releases.Create(rel) + is.NoError(err) + + upAction.ReuseValues = true + // setting newValues and upgrading + res, err := upAction.Run(rel.Name, buildChart(), newValues) + is.NoError(err) + + // Now make sure it is actually upgraded + updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2) + is.NoError(err) + + if updatedRes == nil { + is.Fail("Updated Release is nil") + return + } + is.Equal(release.StatusDeployed, updatedRes.Info.Status) + is.Equal(expectedValues, updatedRes.Config) + }) + + t.Run("reuse values should not install disabled charts", func(t *testing.T) { + upAction := upgradeAction(t) + chartDefaultValues := map[string]interface{}{ + "subchart": map[string]interface{}{ + "enabled": true, + }, + } + dependency := chart.Dependency{ + Name: "subchart", + Version: "0.1.0", + Repository: "http://some-repo.com", + Condition: "subchart.enabled", + } + sampleChart := buildChart( + withName("sample"), + withValues(chartDefaultValues), + withMetadataDependency(dependency), + ) + now := time.Now() + existingValues := map[string]interface{}{ + "subchart": map[string]interface{}{ + "enabled": false, + }, + } + rel := &release.Release{ + Name: "nuketown", + Info: &release.Info{ + FirstDeployed: now, + LastDeployed: now, + Status: release.StatusDeployed, + Description: "Named Release Stub", + }, + Chart: sampleChart, + Config: existingValues, + Version: 1, + } + err := upAction.cfg.Releases.Create(rel) + is.NoError(err) + + upAction.ReuseValues = true + sampleChartWithSubChart := buildChart( + withName(sampleChart.Name()), + withValues(sampleChart.Values), + withDependency(withName("subchart")), + withMetadataDependency(dependency), + ) + // reusing values and upgrading + res, err := upAction.Run(rel.Name, sampleChartWithSubChart, map[string]interface{}{}) + is.NoError(err) + + // Now get the upgraded release + updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2) + is.NoError(err) + + if updatedRes == nil { + is.Fail("Updated Release is nil") + return + } + is.Equal(release.StatusDeployed, updatedRes.Info.Status) + is.Equal(0, len(updatedRes.Chart.Dependencies()), "expected 0 dependencies") + + expectedValues := map[string]interface{}{ + "subchart": map[string]interface{}{ + "enabled": false, + }, + } + is.Equal(expectedValues, updatedRes.Config) + }) +} + +func TestUpgradeRelease_Pending(t *testing.T) { + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "come-fail-away" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + rel2 := releaseStub() + rel2.Name = "come-fail-away" + rel2.Info.Status = release.StatusPendingUpgrade + rel2.Version = 2 + upAction.cfg.Releases.Create(rel2) + + vals := map[string]interface{}{} + + _, err := upAction.Run(rel.Name, buildChart(), vals) + req.Contains(err.Error(), "progress", err) +} diff --git a/pkg/action/validate.go b/pkg/action/validate.go new file mode 100644 index 00000000000..6e074f78b3b --- /dev/null +++ b/pkg/action/validate.go @@ -0,0 +1,184 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "fmt" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/resource" + + "helm.sh/helm/v3/pkg/kube" +) + +var accessor = meta.NewAccessor() + +const ( + appManagedByLabel = "app.kubernetes.io/managed-by" + appManagedByHelm = "Helm" + helmReleaseNameAnnotation = "meta.helm.sh/release-name" + helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace" +) + +func existingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, error) { + var requireUpdate kube.ResourceList + + err := resources.Visit(func(info *resource.Info, err error) error { + if err != nil { + return err + } + + helper := resource.NewHelper(info.Client, info.Mapping) + existing, err := helper.Get(info.Namespace, info.Name) + if err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return errors.Wrap(err, "could not get information about the resource") + } + + // Allow adoption of the resource if it is managed by Helm and is annotated with correct release name and namespace. + if err := checkOwnership(existing, releaseName, releaseNamespace); err != nil { + return fmt.Errorf("%s exists and cannot be imported into the current release: %s", resourceString(info), err) + } + + requireUpdate.Append(info) + return nil + }) + + return requireUpdate, err +} + +func checkOwnership(obj runtime.Object, releaseName, releaseNamespace string) error { + lbls, err := accessor.Labels(obj) + if err != nil { + return err + } + annos, err := accessor.Annotations(obj) + if err != nil { + return err + } + + var errs []error + if err := requireValue(lbls, appManagedByLabel, appManagedByHelm); err != nil { + errs = append(errs, fmt.Errorf("label validation error: %s", err)) + } + if err := requireValue(annos, helmReleaseNameAnnotation, releaseName); err != nil { + errs = append(errs, fmt.Errorf("annotation validation error: %s", err)) + } + if err := requireValue(annos, helmReleaseNamespaceAnnotation, releaseNamespace); err != nil { + errs = append(errs, fmt.Errorf("annotation validation error: %s", err)) + } + + if len(errs) > 0 { + err := errors.New("invalid ownership metadata") + for _, e := range errs { + err = fmt.Errorf("%w; %s", err, e) + } + return err + } + + return nil +} + +func requireValue(meta map[string]string, k, v string) error { + actual, ok := meta[k] + if !ok { + return fmt.Errorf("missing key %q: must be set to %q", k, v) + } + if actual != v { + return fmt.Errorf("key %q must equal %q: current value is %q", k, v, actual) + } + return nil +} + +// setMetadataVisitor adds release tracking metadata to all resources. If force is enabled, existing +// ownership metadata will be overwritten. Otherwise an error will be returned if any resource has an +// existing and conflicting value for the managed by label or Helm release/namespace annotations. +func setMetadataVisitor(releaseName, releaseNamespace string, force bool) resource.VisitorFunc { + return func(info *resource.Info, err error) error { + if err != nil { + return err + } + + if !force { + if err := checkOwnership(info.Object, releaseName, releaseNamespace); err != nil { + return fmt.Errorf("%s cannot be owned: %s", resourceString(info), err) + } + } + + if err := mergeLabels(info.Object, map[string]string{ + appManagedByLabel: appManagedByHelm, + }); err != nil { + return fmt.Errorf( + "%s labels could not be updated: %s", + resourceString(info), err, + ) + } + + if err := mergeAnnotations(info.Object, map[string]string{ + helmReleaseNameAnnotation: releaseName, + helmReleaseNamespaceAnnotation: releaseNamespace, + }); err != nil { + return fmt.Errorf( + "%s annotations could not be updated: %s", + resourceString(info), err, + ) + } + + return nil + } +} + +func resourceString(info *resource.Info) string { + _, k := info.Mapping.GroupVersionKind.ToAPIVersionAndKind() + return fmt.Sprintf( + "%s %q in namespace %q", + k, info.Name, info.Namespace, + ) +} + +func mergeLabels(obj runtime.Object, labels map[string]string) error { + current, err := accessor.Labels(obj) + if err != nil { + return err + } + return accessor.SetLabels(obj, mergeStrStrMaps(current, labels)) +} + +func mergeAnnotations(obj runtime.Object, annotations map[string]string) error { + current, err := accessor.Annotations(obj) + if err != nil { + return err + } + return accessor.SetAnnotations(obj, mergeStrStrMaps(current, annotations)) +} + +// merge two maps, always taking the value on the right +func mergeStrStrMaps(current, desired map[string]string) map[string]string { + result := make(map[string]string) + for k, v := range current { + result[k] = v + } + for k, desiredVal := range desired { + result[k] = desiredVal + } + return result +} diff --git a/pkg/action/validate_test.go b/pkg/action/validate_test.go new file mode 100644 index 00000000000..a9c1cb49c9c --- /dev/null +++ b/pkg/action/validate_test.go @@ -0,0 +1,123 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "testing" + + "helm.sh/helm/v3/pkg/kube" + + appsv1 "k8s.io/api/apps/v1" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/meta" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/resource" +) + +func newDeploymentResource(name, namespace string) *resource.Info { + return &resource.Info{ + Name: name, + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"}, + GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + }, + Object: &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + } +} + +func TestCheckOwnership(t *testing.T) { + deployFoo := newDeploymentResource("foo", "ns-a") + + // Verify that a resource that lacks labels/annotations is not owned + err := checkOwnership(deployFoo.Object, "rel-a", "ns-a") + assert.EqualError(t, err, `invalid ownership metadata; label validation error: missing key "app.kubernetes.io/managed-by": must be set to "Helm"; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) + + // Set managed by label and verify annotation error message + _ = accessor.SetLabels(deployFoo.Object, map[string]string{ + appManagedByLabel: appManagedByHelm, + }) + err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") + assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) + + // Set only the release name annotation and verify missing release namespace error message + _ = accessor.SetAnnotations(deployFoo.Object, map[string]string{ + helmReleaseNameAnnotation: "rel-a", + }) + err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") + assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) + + // Set both release name and namespace annotations and verify no ownership errors + _ = accessor.SetAnnotations(deployFoo.Object, map[string]string{ + helmReleaseNameAnnotation: "rel-a", + helmReleaseNamespaceAnnotation: "ns-a", + }) + err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") + assert.NoError(t, err) + + // Verify ownership error for wrong release name + err = checkOwnership(deployFoo.Object, "rel-b", "ns-a") + assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-name" must equal "rel-b": current value is "rel-a"`) + + // Verify ownership error for wrong release namespace + err = checkOwnership(deployFoo.Object, "rel-a", "ns-b") + assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-namespace" must equal "ns-b": current value is "ns-a"`) + + // Verify ownership error for wrong manager label + _ = accessor.SetLabels(deployFoo.Object, map[string]string{ + appManagedByLabel: "helm", + }) + err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") + assert.EqualError(t, err, `invalid ownership metadata; label validation error: key "app.kubernetes.io/managed-by" must equal "Helm": current value is "helm"`) +} + +func TestSetMetadataVisitor(t *testing.T) { + var ( + err error + deployFoo = newDeploymentResource("foo", "ns-a") + deployBar = newDeploymentResource("bar", "ns-a-system") + resources = kube.ResourceList{deployFoo, deployBar} + ) + + // Set release tracking metadata and verify no error + err = resources.Visit(setMetadataVisitor("rel-a", "ns-a", true)) + assert.NoError(t, err) + + // Verify that release "b" cannot take ownership of "a" + err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false)) + assert.Error(t, err) + + // Force release "b" to take ownership + err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", true)) + assert.NoError(t, err) + + // Check that there is now no ownership error when setting metadata without force + err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false)) + assert.NoError(t, err) + + // Add a new resource that is missing ownership metadata and verify error + resources.Append(newDeploymentResource("baz", "default")) + err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false)) + assert.Error(t, err) + assert.Contains(t, err.Error(), `Deployment "baz" in namespace "" cannot be owned`) +} diff --git a/pkg/action/verify.go b/pkg/action/verify.go new file mode 100644 index 00000000000..f3623949620 --- /dev/null +++ b/pkg/action/verify.go @@ -0,0 +1,59 @@ +/* +Copyright The Helm Authors. + +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 action + +import ( + "fmt" + "strings" + + "helm.sh/helm/v3/pkg/downloader" +) + +// Verify is the action for building a given chart's Verify tree. +// +// It provides the implementation of 'helm verify'. +type Verify struct { + Keyring string + Out string +} + +// NewVerify creates a new Verify object with the given configuration. +func NewVerify() *Verify { + return &Verify{} +} + +// Run executes 'helm verify'. +func (v *Verify) Run(chartfile string) error { + var out strings.Builder + p, err := downloader.VerifyChart(chartfile, v.Keyring) + if err != nil { + return err + } + + for name := range p.SignedBy.Identities { + fmt.Fprintf(&out, "Signed by: %v\n", name) + } + fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", p.SignedBy.PrimaryKey.Fingerprint) + fmt.Fprintf(&out, "Chart Hash Verified: %s\n", p.FileHash) + + // TODO(mattfarina): The output is set as a property rather than returned + // to maintain the Go API. In Helm v4 this function should return the out + // and the property on the struct can be removed. + v.Out = out.String() + + return nil +} diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go new file mode 100644 index 00000000000..a3bed63a38a --- /dev/null +++ b/pkg/chart/chart.go @@ -0,0 +1,173 @@ +/* +Copyright The Helm Authors. +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 chart + +import ( + "path/filepath" + "regexp" + "strings" +) + +// APIVersionV1 is the API version number for version 1. +const APIVersionV1 = "v1" + +// APIVersionV2 is the API version number for version 2. +const APIVersionV2 = "v2" + +// aliasNameFormat defines the characters that are legal in an alias name. +var aliasNameFormat = regexp.MustCompile("^[a-zA-Z0-9_-]+$") + +// Chart is a helm package that contains metadata, a default config, zero or more +// optionally parameterizable templates, and zero or more charts (dependencies). +type Chart struct { + // Raw contains the raw contents of the files originally contained in the chart archive. + // + // This should not be used except in special cases like `helm show values`, + // where we want to display the raw values, comments and all. + Raw []*File `json:"-"` + // Metadata is the contents of the Chartfile. + Metadata *Metadata `json:"metadata"` + // Lock is the contents of Chart.lock. + Lock *Lock `json:"lock"` + // Templates for this chart. + Templates []*File `json:"templates"` + // Values are default config for this chart. + Values map[string]interface{} `json:"values"` + // Schema is an optional JSON schema for imposing structure on Values + Schema []byte `json:"schema"` + // Files are miscellaneous files in a chart archive, + // e.g. README, LICENSE, etc. + Files []*File `json:"files"` + + parent *Chart + dependencies []*Chart +} + +type CRD struct { + // Name is the File.Name for the crd file + Name string + // Filename is the File obj Name including (sub-)chart.ChartFullPath + Filename string + // File is the File obj for the crd + File *File +} + +// SetDependencies replaces the chart dependencies. +func (ch *Chart) SetDependencies(charts ...*Chart) { + ch.dependencies = nil + ch.AddDependency(charts...) +} + +// Name returns the name of the chart. +func (ch *Chart) Name() string { + if ch.Metadata == nil { + return "" + } + return ch.Metadata.Name +} + +// AddDependency determines if the chart is a subchart. +func (ch *Chart) AddDependency(charts ...*Chart) { + for i, x := range charts { + charts[i].parent = ch + ch.dependencies = append(ch.dependencies, x) + } +} + +// Root finds the root chart. +func (ch *Chart) Root() *Chart { + if ch.IsRoot() { + return ch + } + return ch.Parent().Root() +} + +// Dependencies are the charts that this chart depends on. +func (ch *Chart) Dependencies() []*Chart { return ch.dependencies } + +// IsRoot determines if the chart is the root chart. +func (ch *Chart) IsRoot() bool { return ch.parent == nil } + +// Parent returns a subchart's parent chart. +func (ch *Chart) Parent() *Chart { return ch.parent } + +// ChartPath returns the full path to this chart in dot notation. +func (ch *Chart) ChartPath() string { + if !ch.IsRoot() { + return ch.Parent().ChartPath() + "." + ch.Name() + } + return ch.Name() +} + +// ChartFullPath returns the full path to this chart. +func (ch *Chart) ChartFullPath() string { + if !ch.IsRoot() { + return ch.Parent().ChartFullPath() + "/charts/" + ch.Name() + } + return ch.Name() +} + +// Validate validates the metadata. +func (ch *Chart) Validate() error { + return ch.Metadata.Validate() +} + +// AppVersion returns the appversion of the chart. +func (ch *Chart) AppVersion() string { + if ch.Metadata == nil { + return "" + } + return ch.Metadata.AppVersion +} + +// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. +// Deprecated: use CRDObjects() +func (ch *Chart) CRDs() []*File { + files := []*File{} + // Find all resources in the crds/ directory + for _, f := range ch.Files { + if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { + files = append(files, f) + } + } + // Get CRDs from dependencies, too. + for _, dep := range ch.Dependencies() { + files = append(files, dep.CRDs()...) + } + return files +} + +// CRDObjects returns a list of CRD objects in the 'crds/' directory of a Helm chart & subcharts +func (ch *Chart) CRDObjects() []CRD { + crds := []CRD{} + // Find all resources in the crds/ directory + for _, f := range ch.Files { + if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { + mycrd := CRD{Name: f.Name, Filename: filepath.Join(ch.ChartFullPath(), f.Name), File: f} + crds = append(crds, mycrd) + } + } + // Get CRDs from dependencies, too. + for _, dep := range ch.Dependencies() { + crds = append(crds, dep.CRDObjects()...) + } + return crds +} + +func hasManifestExtension(fname string) bool { + ext := filepath.Ext(fname) + return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json") +} diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go new file mode 100644 index 00000000000..ef8cec3ad79 --- /dev/null +++ b/pkg/chart/chart_test.go @@ -0,0 +1,211 @@ +/* +Copyright The Helm Authors. + +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 chart + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCRDs(t *testing.T) { + chrt := Chart{ + Files: []*File{ + { + Name: "crds/foo.yaml", + Data: []byte("hello"), + }, + { + Name: "bar.yaml", + Data: []byte("hello"), + }, + { + Name: "crds/foo/bar/baz.yaml", + Data: []byte("hello"), + }, + { + Name: "crdsfoo/bar/baz.yaml", + Data: []byte("hello"), + }, + { + Name: "crds/README.md", + Data: []byte("# hello"), + }, + }, + } + + is := assert.New(t) + crds := chrt.CRDs() + is.Equal(2, len(crds)) + is.Equal("crds/foo.yaml", crds[0].Name) + is.Equal("crds/foo/bar/baz.yaml", crds[1].Name) +} + +func TestSaveChartNoRawData(t *testing.T) { + chrt := Chart{ + Raw: []*File{ + { + Name: "fhqwhgads.yaml", + Data: []byte("Everybody to the Limit"), + }, + }, + } + + is := assert.New(t) + data, err := json.Marshal(chrt) + if err != nil { + t.Fatal(err) + } + + res := &Chart{} + if err := json.Unmarshal(data, res); err != nil { + t.Fatal(err) + } + + is.Equal([]*File(nil), res.Raw) +} + +func TestMetadata(t *testing.T) { + chrt := Chart{ + Metadata: &Metadata{ + Name: "foo.yaml", + AppVersion: "1.0.0", + APIVersion: "v2", + Version: "1.0.0", + Type: "application", + }, + } + + is := assert.New(t) + + is.Equal("foo.yaml", chrt.Name()) + is.Equal("1.0.0", chrt.AppVersion()) + is.Equal(nil, chrt.Validate()) +} + +func TestIsRoot(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal(false, chrt1.IsRoot()) + is.Equal(true, chrt2.IsRoot()) +} + +func TestChartPath(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal("foo.", chrt1.ChartPath()) + is.Equal("foo", chrt2.ChartPath()) +} + +func TestChartFullPath(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal("foo/charts/", chrt1.ChartFullPath()) + is.Equal("foo", chrt2.ChartFullPath()) +} + +func TestCRDObjects(t *testing.T) { + chrt := Chart{ + Files: []*File{ + { + Name: "crds/foo.yaml", + Data: []byte("hello"), + }, + { + Name: "bar.yaml", + Data: []byte("hello"), + }, + { + Name: "crds/foo/bar/baz.yaml", + Data: []byte("hello"), + }, + { + Name: "crdsfoo/bar/baz.yaml", + Data: []byte("hello"), + }, + { + Name: "crds/README.md", + Data: []byte("# hello"), + }, + }, + } + + expected := []CRD{ + { + Name: "crds/foo.yaml", + Filename: "crds/foo.yaml", + File: &File{ + Name: "crds/foo.yaml", + Data: []byte("hello"), + }, + }, + { + Name: "crds/foo/bar/baz.yaml", + Filename: "crds/foo/bar/baz.yaml", + File: &File{ + Name: "crds/foo/bar/baz.yaml", + Data: []byte("hello"), + }, + }, + } + + is := assert.New(t) + crds := chrt.CRDObjects() + is.Equal(expected, crds) +} diff --git a/pkg/chart/dependency.go b/pkg/chart/dependency.go new file mode 100644 index 00000000000..b2819f373a3 --- /dev/null +++ b/pkg/chart/dependency.go @@ -0,0 +1,79 @@ +/* +Copyright The Helm Authors. +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 chart + +import "time" + +// Dependency describes a chart upon which another chart depends. +// +// Dependencies can be used to express developer intent, or to capture the state +// of a chart. +type Dependency struct { + // Name is the name of the dependency. + // + // This must mach the name in the dependency's Chart.yaml. + Name string `json:"name"` + // Version is the version (range) of this chart. + // + // A lock file will always produce a single version, while a dependency + // may contain a semantic version range. + Version string `json:"version,omitempty"` + // The URL to the repository. + // + // Appending `index.yaml` to this string should result in a URL that can be + // used to fetch the repository index. + Repository string `json:"repository"` + // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled ) + Condition string `json:"condition,omitempty"` + // Tags can be used to group charts for enabling/disabling together + Tags []string `json:"tags,omitempty"` + // Enabled bool determines if chart should be loaded + Enabled bool `json:"enabled,omitempty"` + // ImportValues holds the mapping of source values to parent key to be imported. Each item can be a + // string or pair of child/parent sublist items. + ImportValues []interface{} `json:"import-values,omitempty"` + // Alias usable alias to be used for the chart + Alias string `json:"alias,omitempty"` +} + +// Validate checks for common problems with the dependency datastructure in +// the chart. This check must be done at load time before the dependency's charts are +// loaded. +func (d *Dependency) Validate() error { + d.Name = sanitizeString(d.Name) + d.Version = sanitizeString(d.Version) + d.Repository = sanitizeString(d.Repository) + d.Condition = sanitizeString(d.Condition) + for i := range d.Tags { + d.Tags[i] = sanitizeString(d.Tags[i]) + } + if d.Alias != "" && !aliasNameFormat.MatchString(d.Alias) { + return ValidationErrorf("dependency %q has disallowed characters in the alias", d.Name) + } + return nil +} + +// Lock is a lock file for dependencies. +// +// It represents the state that the dependencies should be in. +type Lock struct { + // Generated is the date the lock file was last generated. + Generated time.Time `json:"generated"` + // Digest is a hash of the dependencies in Chart.yaml. + Digest string `json:"digest"` + // Dependencies is the list of dependencies that this lock file has locked. + Dependencies []*Dependency `json:"dependencies"` +} diff --git a/pkg/chart/dependency_test.go b/pkg/chart/dependency_test.go new file mode 100644 index 00000000000..99c45b4b582 --- /dev/null +++ b/pkg/chart/dependency_test.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. + +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 chart + +import ( + "testing" +) + +func TestValidateDependency(t *testing.T) { + dep := &Dependency{ + Name: "example", + } + for value, shouldFail := range map[string]bool{ + "abcdefghijklmenopQRSTUVWXYZ-0123456780_": false, + "-okay": false, + "_okay": false, + "- bad": true, + " bad": true, + "bad\nvalue": true, + "bad ": true, + "bad$": true, + } { + dep.Alias = value + res := dep.Validate() + if res != nil && !shouldFail { + t.Errorf("Failed on case %q", dep.Alias) + } else if res == nil && shouldFail { + t.Errorf("Expected failure for %q", dep.Alias) + } + } +} diff --git a/pkg/chart/errors.go b/pkg/chart/errors.go new file mode 100644 index 00000000000..2fad5f37081 --- /dev/null +++ b/pkg/chart/errors.go @@ -0,0 +1,30 @@ +/* +Copyright The Helm Authors. +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 chart + +import "fmt" + +// ValidationError represents a data validation error. +type ValidationError string + +func (v ValidationError) Error() string { + return "validation: " + string(v) +} + +// ValidationErrorf takes a message and formatting options and creates a ValidationError +func ValidationErrorf(msg string, args ...interface{}) ValidationError { + return ValidationError(fmt.Sprintf(msg, args...)) +} diff --git a/pkg/chart/file.go b/pkg/chart/file.go new file mode 100644 index 00000000000..9dd7c08d525 --- /dev/null +++ b/pkg/chart/file.go @@ -0,0 +1,27 @@ +/* +Copyright The Helm Authors. +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 chart + +// File represents a file as a name/value pair. +// +// By convention, name is a relative path within the scope of the chart's +// base directory. +type File struct { + // Name is the path-like name of the template. + Name string `json:"name"` + // Data is the template as byte data. + Data []byte `json:"data"` +} diff --git a/pkg/chart/loader/archive.go b/pkg/chart/loader/archive.go new file mode 100644 index 00000000000..8b38cb89f99 --- /dev/null +++ b/pkg/chart/loader/archive.go @@ -0,0 +1,196 @@ +/* +Copyright The Helm Authors. + +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 loader + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "io" + "net/http" + "os" + "path" + "regexp" + "strings" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chart" +) + +var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`) + +// FileLoader loads a chart from a file +type FileLoader string + +// Load loads a chart +func (l FileLoader) Load() (*chart.Chart, error) { + return LoadFile(string(l)) +} + +// LoadFile loads from an archive file. +func LoadFile(name string) (*chart.Chart, error) { + if fi, err := os.Stat(name); err != nil { + return nil, err + } else if fi.IsDir() { + return nil, errors.New("cannot load a directory") + } + + raw, err := os.Open(name) + if err != nil { + return nil, err + } + defer raw.Close() + + err = ensureArchive(name, raw) + if err != nil { + return nil, err + } + + c, err := LoadArchive(raw) + if err != nil { + if err == gzip.ErrHeader { + return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err) + } + } + return c, err +} + +// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive. +// +// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence +// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error +// if we didn't check for this. +func ensureArchive(name string, raw *os.File) error { + defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed. + + // Check the file format to give us a chance to provide the user with more actionable feedback. + buffer := make([]byte, 512) + _, err := raw.Read(buffer) + if err != nil && err != io.EOF { + return fmt.Errorf("file '%s' cannot be read: %s", name, err) + } + if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" { + // TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide + // variety of content (Makefile, .zshrc) as valid YAML without errors. + + // Wrong content type. Let's check if it's yaml and give an extra hint? + if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") { + return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name) + } + return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType) + } + return nil +} + +// LoadArchiveFiles reads in files out of an archive into memory. This function +// performs important path security checks and should always be used before +// expanding a tarball +func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { + unzipped, err := gzip.NewReader(in) + if err != nil { + return nil, err + } + defer unzipped.Close() + + files := []*BufferedFile{} + tr := tar.NewReader(unzipped) + for { + b := bytes.NewBuffer(nil) + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + if hd.FileInfo().IsDir() { + // Use this instead of hd.Typeflag because we don't have to do any + // inference chasing. + continue + } + + switch hd.Typeflag { + // We don't want to process these extension header files. + case tar.TypeXGlobalHeader, tar.TypeXHeader: + continue + } + + // Archive could contain \ if generated on Windows + delimiter := "/" + if strings.ContainsRune(hd.Name, '\\') { + delimiter = "\\" + } + + parts := strings.Split(hd.Name, delimiter) + n := strings.Join(parts[1:], delimiter) + + // Normalize the path to the / delimiter + n = strings.ReplaceAll(n, delimiter, "/") + + if path.IsAbs(n) { + return nil, errors.New("chart illegally contains absolute paths") + } + + n = path.Clean(n) + if n == "." { + // In this case, the original path was relative when it should have been absolute. + return nil, errors.Errorf("chart illegally contains content outside the base directory: %q", hd.Name) + } + if strings.HasPrefix(n, "..") { + return nil, errors.New("chart illegally references parent directory") + } + + // In some particularly arcane acts of path creativity, it is possible to intermix + // UNIX and Windows style paths in such a way that you produce a result of the form + // c:/foo even after all the built-in absolute path checks. So we explicitly check + // for this condition. + if drivePathPattern.MatchString(n) { + return nil, errors.New("chart contains illegally named files") + } + + if parts[0] == "Chart.yaml" { + return nil, errors.New("chart yaml not in base directory") + } + + if _, err := io.Copy(b, tr); err != nil { + return nil, err + } + + data := bytes.TrimPrefix(b.Bytes(), utf8bom) + + files = append(files, &BufferedFile{Name: n, Data: data}) + b.Reset() + } + + if len(files) == 0 { + return nil, errors.New("no files in chart archive") + } + return files, nil +} + +// LoadArchive loads from a reader containing a compressed tar archive. +func LoadArchive(in io.Reader) (*chart.Chart, error) { + files, err := LoadArchiveFiles(in) + if err != nil { + return nil, err + } + + return LoadFiles(files) +} diff --git a/pkg/chart/loader/archive_test.go b/pkg/chart/loader/archive_test.go new file mode 100644 index 00000000000..41b0af1aa4e --- /dev/null +++ b/pkg/chart/loader/archive_test.go @@ -0,0 +1,90 @@ +/* +Copyright The Helm Authors. + +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 loader + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "testing" +) + +func TestLoadArchiveFiles(t *testing.T) { + tcs := []struct { + name string + generate func(w *tar.Writer) + check func(t *testing.T, files []*BufferedFile, err error) + }{ + { + name: "empty input should return no files", + generate: func(w *tar.Writer) {}, + check: func(t *testing.T, files []*BufferedFile, err error) { + if err.Error() != "no files in chart archive" { + t.Fatalf(`expected "no files in chart archive", got [%#v]`, err) + } + }, + }, + { + name: "should ignore files with XGlobalHeader type", + generate: func(w *tar.Writer) { + // simulate the presence of a `pax_global_header` file like you would get when + // processing a GitHub release archive. + err := w.WriteHeader(&tar.Header{ + Typeflag: tar.TypeXGlobalHeader, + Name: "pax_global_header", + }) + if err != nil { + t.Fatal(err) + } + + // we need to have at least one file, otherwise we'll get the "no files in chart archive" error + err = w.WriteHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: "dir/empty", + }) + if err != nil { + t.Fatal(err) + } + }, + check: func(t *testing.T, files []*BufferedFile, err error) { + if err != nil { + t.Fatalf(`got unwanted error [%#v] for tar file with pax_global_header content`, err) + } + + if len(files) != 1 { + t.Fatalf(`expected to get one file but got [%v]`, files) + } + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + buf := &bytes.Buffer{} + gzw := gzip.NewWriter(buf) + tw := tar.NewWriter(gzw) + + tc.generate(tw) + + _ = tw.Close() + _ = gzw.Close() + + files, err := LoadArchiveFiles(buf) + tc.check(t, files, err) + }) + } +} diff --git a/pkg/chart/loader/directory.go b/pkg/chart/loader/directory.go new file mode 100644 index 00000000000..bbe543870d1 --- /dev/null +++ b/pkg/chart/loader/directory.go @@ -0,0 +1,120 @@ +/* +Copyright The Helm Authors. + +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 loader + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/internal/ignore" + "helm.sh/helm/v3/internal/sympath" + "helm.sh/helm/v3/pkg/chart" +) + +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + +// DirLoader loads a chart from a directory +type DirLoader string + +// Load loads the chart +func (l DirLoader) Load() (*chart.Chart, error) { + return LoadDir(string(l)) +} + +// LoadDir loads from a directory. +// +// This loads charts only from directories. +func LoadDir(dir string) (*chart.Chart, error) { + topdir, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + // Just used for errors. + c := &chart.Chart{} + + rules := ignore.Empty() + ifile := filepath.Join(topdir, ignore.HelmIgnore) + if _, err := os.Stat(ifile); err == nil { + r, err := ignore.ParseFile(ifile) + if err != nil { + return c, err + } + rules = r + } + rules.AddDefaults() + + files := []*BufferedFile{} + topdir += string(filepath.Separator) + + walk := func(name string, fi os.FileInfo, err error) error { + n := strings.TrimPrefix(name, topdir) + if n == "" { + // No need to process top level. Avoid bug with helmignore .* matching + // empty names. See issue 1779. + return nil + } + + // Normalize to / since it will also work on Windows + n = filepath.ToSlash(n) + + if err != nil { + return err + } + if fi.IsDir() { + // Directory-based ignore rules should involve skipping the entire + // contents of that directory. + if rules.Ignore(n, fi) { + return filepath.SkipDir + } + return nil + } + + // If a .helmignore file matches, skip this file. + if rules.Ignore(n, fi) { + return nil + } + + // Irregular files include devices, sockets, and other uses of files that + // are not regular files. In Go they have a file mode type bit set. + // See https://golang.org/pkg/os/#FileMode for examples. + if !fi.Mode().IsRegular() { + return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name) + } + + data, err := ioutil.ReadFile(name) + if err != nil { + return errors.Wrapf(err, "error reading %s", n) + } + + data = bytes.TrimPrefix(data, utf8bom) + + files = append(files, &BufferedFile{Name: n, Data: data}) + return nil + } + if err = sympath.Walk(topdir, walk); err != nil { + return c, err + } + + return LoadFiles(files) +} diff --git a/pkg/chart/loader/load.go b/pkg/chart/loader/load.go new file mode 100644 index 00000000000..7cc8878a8dd --- /dev/null +++ b/pkg/chart/loader/load.go @@ -0,0 +1,200 @@ +/* +Copyright The Helm Authors. + +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 loader + +import ( + "bytes" + "log" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/chart" +) + +// ChartLoader loads a chart. +type ChartLoader interface { + Load() (*chart.Chart, error) +} + +// Loader returns a new ChartLoader appropriate for the given chart name +func Loader(name string) (ChartLoader, error) { + fi, err := os.Stat(name) + if err != nil { + return nil, err + } + if fi.IsDir() { + return DirLoader(name), nil + } + return FileLoader(name), nil + +} + +// Load takes a string name, tries to resolve it to a file or directory, and then loads it. +// +// This is the preferred way to load a chart. It will discover the chart encoding +// and hand off to the appropriate chart reader. +// +// If a .helmignore file is present, the directory loader will skip loading any files +// matching it. But .helmignore is not evaluated when reading out of an archive. +func Load(name string) (*chart.Chart, error) { + l, err := Loader(name) + if err != nil { + return nil, err + } + return l.Load() +} + +// BufferedFile represents an archive file buffered for later processing. +type BufferedFile struct { + Name string + Data []byte +} + +// LoadFiles loads from in-memory files. +func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { + c := new(chart.Chart) + subcharts := make(map[string][]*BufferedFile) + + // do not rely on assumed ordering of files in the chart and crash + // if Chart.yaml was not coming early enough to initialize metadata + for _, f := range files { + c.Raw = append(c.Raw, &chart.File{Name: f.Name, Data: f.Data}) + if f.Name == "Chart.yaml" { + if c.Metadata == nil { + c.Metadata = new(chart.Metadata) + } + if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil { + return c, errors.Wrap(err, "cannot load Chart.yaml") + } + // NOTE(bacongobbler): while the chart specification says that APIVersion must be set, + // Helm 2 accepted charts that did not provide an APIVersion in their chart metadata. + // Because of that, if APIVersion is unset, we should assume we're loading a v1 chart. + if c.Metadata.APIVersion == "" { + c.Metadata.APIVersion = chart.APIVersionV1 + } + } + } + for _, f := range files { + switch { + case f.Name == "Chart.yaml": + // already processed + continue + case f.Name == "Chart.lock": + c.Lock = new(chart.Lock) + if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil { + return c, errors.Wrap(err, "cannot load Chart.lock") + } + case f.Name == "values.yaml": + c.Values = make(map[string]interface{}) + if err := yaml.Unmarshal(f.Data, &c.Values); err != nil { + return c, errors.Wrap(err, "cannot load values.yaml") + } + case f.Name == "values.schema.json": + c.Schema = f.Data + + // Deprecated: requirements.yaml is deprecated use Chart.yaml. + // We will handle it for you because we are nice people + case f.Name == "requirements.yaml": + if c.Metadata == nil { + c.Metadata = new(chart.Metadata) + } + if c.Metadata.APIVersion != chart.APIVersionV1 { + log.Printf("Warning: Dependencies are handled in Chart.yaml since apiVersion \"v2\". We recommend migrating dependencies to Chart.yaml.") + } + if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil { + return c, errors.Wrap(err, "cannot load requirements.yaml") + } + if c.Metadata.APIVersion == chart.APIVersionV1 { + c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + } + // Deprecated: requirements.lock is deprecated use Chart.lock. + case f.Name == "requirements.lock": + c.Lock = new(chart.Lock) + if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil { + return c, errors.Wrap(err, "cannot load requirements.lock") + } + if c.Metadata == nil { + c.Metadata = new(chart.Metadata) + } + if c.Metadata.APIVersion == chart.APIVersionV1 { + c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + } + + case strings.HasPrefix(f.Name, "templates/"): + c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data}) + case strings.HasPrefix(f.Name, "charts/"): + if filepath.Ext(f.Name) == ".prov" { + c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + continue + } + + fname := strings.TrimPrefix(f.Name, "charts/") + cname := strings.SplitN(fname, "/", 2)[0] + subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data}) + default: + c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + } + } + + if c.Metadata == nil { + return c, errors.New("Chart.yaml file is missing") + } + + if err := c.Validate(); err != nil { + return c, err + } + + for n, files := range subcharts { + var sc *chart.Chart + var err error + switch { + case strings.IndexAny(n, "_.") == 0: + continue + case filepath.Ext(n) == ".tgz": + file := files[0] + if file.Name != n { + return c, errors.Errorf("error unpacking tar in %s: expected %s, got %s", c.Name(), n, file.Name) + } + // Untar the chart and add to c.Dependencies + sc, err = LoadArchive(bytes.NewBuffer(file.Data)) + default: + // We have to trim the prefix off of every file, and ignore any file + // that is in charts/, but isn't actually a chart. + buff := make([]*BufferedFile, 0, len(files)) + for _, f := range files { + parts := strings.SplitN(f.Name, "/", 2) + if len(parts) < 2 { + continue + } + f.Name = parts[1] + buff = append(buff, f) + } + sc, err = LoadFiles(buff) + } + + if err != nil { + return c, errors.Wrapf(err, "error unpacking %s in %s", n, c.Name()) + } + c.AddDependency(sc) + } + + return c, nil +} diff --git a/pkg/chart/loader/load_test.go b/pkg/chart/loader/load_test.go new file mode 100644 index 00000000000..9605b3152d7 --- /dev/null +++ b/pkg/chart/loader/load_test.go @@ -0,0 +1,653 @@ +/* +Copyright The Helm Authors. + +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 loader + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "helm.sh/helm/v3/pkg/chart" +) + +func TestLoadDir(t *testing.T) { + l, err := Loader("testdata/frobnitz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load() + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestLoadDirWithDevNull(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test only works on unix systems with /dev/null present") + } + + l, err := Loader("testdata/frobnitz_with_dev_null") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + if _, err := l.Load(); err == nil { + t.Errorf("packages with an irregular file (/dev/null) should not load") + } +} + +func TestLoadDirWithSymlink(t *testing.T) { + sym := filepath.Join("..", "LICENSE") + link := filepath.Join("testdata", "frobnitz_with_symlink", "LICENSE") + + if err := os.Symlink(sym, link); err != nil { + t.Fatal(err) + } + + defer os.Remove(link) + + l, err := Loader("testdata/frobnitz_with_symlink") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + + c, err := l.Load() + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestBomTestData(t *testing.T) { + testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"} + for _, file := range testFiles { + data, err := ioutil.ReadFile("testdata/" + file) + if err != nil || !bytes.HasPrefix(data, utf8bom) { + t.Errorf("Test file has no BOM or is invalid: testdata/%s", file) + } + } + + archive, err := ioutil.ReadFile("testdata/frobnitz_with_bom.tgz") + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + unzipped, err := gzip.NewReader(bytes.NewReader(archive)) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + defer unzipped.Close() + for _, testFile := range testFiles { + data := make([]byte, 3) + err := unzipped.Reset(bytes.NewReader(archive)) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + tr := tar.NewReader(unzipped) + for { + file, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + if file != nil && strings.EqualFold(file.Name, testFile) { + _, err := tr.Read(data) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } else { + break + } + } + } + if !bytes.Equal(data, utf8bom) { + t.Fatalf("Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile) + } + } +} + +func TestLoadDirWithUTFBOM(t *testing.T) { + l, err := Loader("testdata/frobnitz_with_bom") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load() + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) + verifyBomStripped(t, c.Files) +} + +func TestLoadArchiveWithUTFBOM(t *testing.T) { + l, err := Loader("testdata/frobnitz_with_bom.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load() + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) + verifyBomStripped(t, c.Files) +} + +func TestLoadV1(t *testing.T) { + l, err := Loader("testdata/frobnitz.v1") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load() + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestLoadFileV1(t *testing.T) { + l, err := Loader("testdata/frobnitz.v1.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load() + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestLoadFile(t *testing.T) { + l, err := Loader("testdata/frobnitz-1.2.3.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load() + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) +} + +func TestLoadFiles_BadCases(t *testing.T) { + for _, tt := range []struct { + name string + bufferedFiles []*BufferedFile + expectError string + }{ + { + name: "These files contain only requirements.lock", + bufferedFiles: []*BufferedFile{ + { + Name: "requirements.lock", + Data: []byte(""), + }, + }, + expectError: "validation: chart.metadata.apiVersion is required"}, + } { + _, err := LoadFiles(tt.bufferedFiles) + if err == nil { + t.Fatal("expected error when load illegal files") + } + if !strings.Contains(err.Error(), tt.expectError) { + t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.name) + } + } +} + +func TestLoadFiles(t *testing.T) { + goodFiles := []*BufferedFile{ + { + Name: "Chart.yaml", + Data: []byte(`apiVersion: v1 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +`), + }, + { + Name: "values.yaml", + Data: []byte("var: some values"), + }, + { + Name: "values.schema.json", + Data: []byte("type: Values"), + }, + { + Name: "templates/deployment.yaml", + Data: []byte("some deployment"), + }, + { + Name: "templates/service.yaml", + Data: []byte("some service"), + }, + } + + c, err := LoadFiles(goodFiles) + if err != nil { + t.Errorf("Expected good files to be loaded, got %v", err) + } + + if c.Name() != "frobnitz" { + t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Name()) + } + + if c.Values["var"] != "some values" { + t.Error("Expected chart values to be populated with default values") + } + + if len(c.Raw) != 5 { + t.Errorf("Expected %d files, got %d", 5, len(c.Raw)) + } + + if !bytes.Equal(c.Schema, []byte("type: Values")) { + t.Error("Expected chart schema to be populated with default values") + } + + if len(c.Templates) != 2 { + t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) + } + + if _, err = LoadFiles([]*BufferedFile{}); err == nil { + t.Fatal("Expected err to be non-nil") + } + if err.Error() != "Chart.yaml file is missing" { + t.Errorf("Expected chart metadata missing error, got '%s'", err.Error()) + } +} + +// Test the order of file loading. The Chart.yaml file needs to come first for +// later comparison checks. See https://github.com/helm/helm/pull/8948 +func TestLoadFilesOrder(t *testing.T) { + goodFiles := []*BufferedFile{ + { + Name: "requirements.yaml", + Data: []byte("dependencies:"), + }, + { + Name: "values.yaml", + Data: []byte("var: some values"), + }, + + { + Name: "templates/deployment.yaml", + Data: []byte("some deployment"), + }, + { + Name: "templates/service.yaml", + Data: []byte("some service"), + }, + { + Name: "Chart.yaml", + Data: []byte(`apiVersion: v1 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +`), + }, + } + + // Capture stderr to make sure message about Chart.yaml handle dependencies + // is not present + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Unable to create pipe: %s", err) + } + stderr := log.Writer() + log.SetOutput(w) + defer func() { + log.SetOutput(stderr) + }() + + _, err = LoadFiles(goodFiles) + if err != nil { + t.Errorf("Expected good files to be loaded, got %v", err) + } + w.Close() + + var text bytes.Buffer + io.Copy(&text, r) + if text.String() != "" { + t.Errorf("Expected no message to Stderr, got %s", text.String()) + } + +} + +// Packaging the chart on a Windows machine will produce an +// archive that has \\ as delimiters. Test that we support these archives +func TestLoadFileBackslash(t *testing.T) { + c, err := Load("testdata/frobnitz_backslash-1.2.3.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyChartFileAndTemplate(t, c, "frobnitz_backslash") + verifyChart(t, c) + verifyDependencies(t, c) +} + +func TestLoadV2WithReqs(t *testing.T) { + l, err := Loader("testdata/frobnitz.v2.reqs") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load() + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestLoadInvalidArchive(t *testing.T) { + tmpdir, err := ioutil.TempDir("", "helm-test-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpdir) + + writeTar := func(filename, internalPath string, body []byte) { + dest, err := os.Create(filename) + if err != nil { + t.Fatal(err) + } + zipper := gzip.NewWriter(dest) + tw := tar.NewWriter(zipper) + + h := &tar.Header{ + Name: internalPath, + Mode: 0755, + Size: int64(len(body)), + ModTime: time.Now(), + } + if err := tw.WriteHeader(h); err != nil { + t.Fatal(err) + } + if _, err := tw.Write(body); err != nil { + t.Fatal(err) + } + tw.Close() + zipper.Close() + dest.Close() + } + + for _, tt := range []struct { + chartname string + internal string + expectError string + }{ + {"illegal-dots.tgz", "../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-dots2.tgz", "/foo/../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-dots3.tgz", "/../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-dots4.tgz", "./../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-name.tgz", "./.", "chart illegally contains content outside the base directory"}, + {"illegal-name2.tgz", "/./.", "chart illegally contains content outside the base directory"}, + {"illegal-name3.tgz", "missing-leading-slash", "chart illegally contains content outside the base directory"}, + {"illegal-name4.tgz", "/missing-leading-slash", "Chart.yaml file is missing"}, + {"illegal-abspath.tgz", "//foo", "chart illegally contains absolute paths"}, + {"illegal-abspath2.tgz", "///foo", "chart illegally contains absolute paths"}, + {"illegal-abspath3.tgz", "\\\\foo", "chart illegally contains absolute paths"}, + {"illegal-abspath3.tgz", "\\..\\..\\foo", "chart illegally references parent directory"}, + + // Under special circumstances, this can get normalized to things that look like absolute Windows paths + {"illegal-abspath4.tgz", "\\.\\c:\\\\foo", "chart contains illegally named files"}, + {"illegal-abspath5.tgz", "/./c://foo", "chart contains illegally named files"}, + {"illegal-abspath6.tgz", "\\\\?\\Some\\windows\\magic", "chart illegally contains absolute paths"}, + } { + illegalChart := filepath.Join(tmpdir, tt.chartname) + writeTar(illegalChart, tt.internal, []byte("hello: world")) + _, err = Load(illegalChart) + if err == nil { + t.Fatal("expected error when unpacking illegal files") + } + if !strings.Contains(err.Error(), tt.expectError) { + t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname) + } + } + + // Make sure that absolute path gets interpreted as relative + illegalChart := filepath.Join(tmpdir, "abs-path.tgz") + writeTar(illegalChart, "/Chart.yaml", []byte("hello: world")) + _, err = Load(illegalChart) + if err.Error() != "validation: chart.metadata.name is required" { + t.Error(err) + } + + // And just to validate that the above was not spurious + illegalChart = filepath.Join(tmpdir, "abs-path2.tgz") + writeTar(illegalChart, "files/whatever.yaml", []byte("hello: world")) + _, err = Load(illegalChart) + if err.Error() != "Chart.yaml file is missing" { + t.Errorf("Unexpected error message: %s", err) + } + + // Finally, test that drive letter gets stripped off on Windows + illegalChart = filepath.Join(tmpdir, "abs-winpath.tgz") + writeTar(illegalChart, "c:\\Chart.yaml", []byte("hello: world")) + _, err = Load(illegalChart) + if err.Error() != "validation: chart.metadata.name is required" { + t.Error(err) + } +} + +func verifyChart(t *testing.T, c *chart.Chart) { + t.Helper() + if c.Name() == "" { + t.Fatalf("No chart metadata found on %v", c) + } + t.Logf("Verifying chart %s", c.Name()) + if len(c.Templates) != 1 { + t.Errorf("Expected 1 template, got %d", len(c.Templates)) + } + + numfiles := 6 + if len(c.Files) != numfiles { + t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files)) + for _, n := range c.Files { + t.Logf("\t%s", n.Name) + } + } + + if len(c.Dependencies()) != 2 { + t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies()), c.Dependencies()) + for _, d := range c.Dependencies() { + t.Logf("\tSubchart: %s\n", d.Name()) + } + } + + expect := map[string]map[string]string{ + "alpine": { + "version": "0.1.0", + }, + "mariner": { + "version": "4.3.2", + }, + } + + for _, dep := range c.Dependencies() { + if dep.Metadata == nil { + t.Fatalf("expected metadata on dependency: %v", dep) + } + exp, ok := expect[dep.Name()] + if !ok { + t.Fatalf("Unknown dependency %s", dep.Name()) + } + if exp["version"] != dep.Metadata.Version { + t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version) + } + } + +} + +func verifyDependencies(t *testing.T, c *chart.Chart) { + if len(c.Metadata.Dependencies) != 2 { + t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) + } + tests := []*chart.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + for i, tt := range tests { + d := c.Metadata.Dependencies[i] + if d.Name != tt.Name { + t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) + } + if d.Version != tt.Version { + t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) + } + if d.Repository != tt.Repository { + t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) + } + } +} + +func verifyDependenciesLock(t *testing.T, c *chart.Chart) { + if len(c.Metadata.Dependencies) != 2 { + t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) + } + tests := []*chart.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + for i, tt := range tests { + d := c.Metadata.Dependencies[i] + if d.Name != tt.Name { + t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) + } + if d.Version != tt.Version { + t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) + } + if d.Repository != tt.Repository { + t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) + } + } +} + +func verifyFrobnitz(t *testing.T, c *chart.Chart) { + verifyChartFileAndTemplate(t, c, "frobnitz") +} + +func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { + if c.Metadata == nil { + t.Fatal("Metadata is nil") + } + if c.Name() != name { + t.Errorf("Expected %s, got %s", name, c.Name()) + } + if len(c.Templates) != 1 { + t.Fatalf("Expected 1 template, got %d", len(c.Templates)) + } + if c.Templates[0].Name != "templates/template.tpl" { + t.Errorf("Unexpected template: %s", c.Templates[0].Name) + } + if len(c.Templates[0].Data) == 0 { + t.Error("No template data.") + } + if len(c.Files) != 6 { + t.Fatalf("Expected 6 Files, got %d", len(c.Files)) + } + if len(c.Dependencies()) != 2 { + t.Fatalf("Expected 2 Dependency, got %d", len(c.Dependencies())) + } + if len(c.Metadata.Dependencies) != 2 { + t.Fatalf("Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies)) + } + if len(c.Lock.Dependencies) != 2 { + t.Fatalf("Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies)) + } + + for _, dep := range c.Dependencies() { + switch dep.Name() { + case "mariner": + case "alpine": + if len(dep.Templates) != 1 { + t.Fatalf("Expected 1 template, got %d", len(dep.Templates)) + } + if dep.Templates[0].Name != "templates/alpine-pod.yaml" { + t.Errorf("Unexpected template: %s", dep.Templates[0].Name) + } + if len(dep.Templates[0].Data) == 0 { + t.Error("No template data.") + } + if len(dep.Files) != 1 { + t.Fatalf("Expected 1 Files, got %d", len(dep.Files)) + } + if len(dep.Dependencies()) != 2 { + t.Fatalf("Expected 2 Dependency, got %d", len(dep.Dependencies())) + } + default: + t.Errorf("Unexpected dependency %s", dep.Name()) + } + } +} + +func verifyBomStripped(t *testing.T, files []*chart.File) { + for _, file := range files { + if bytes.HasPrefix(file.Data, utf8bom) { + t.Errorf("Byte Order Mark still present in processed file %s", file.Name) + } + } +} diff --git a/pkg/chartutil/testdata/frobnitz_backslash/LICENSE b/pkg/chart/loader/testdata/LICENSE old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/LICENSE rename to pkg/chart/loader/testdata/LICENSE diff --git a/pkg/chartutil/testdata/albatross/Chart.yaml b/pkg/chart/loader/testdata/albatross/Chart.yaml similarity index 100% rename from pkg/chartutil/testdata/albatross/Chart.yaml rename to pkg/chart/loader/testdata/albatross/Chart.yaml diff --git a/pkg/chartutil/testdata/albatross/values.yaml b/pkg/chart/loader/testdata/albatross/values.yaml similarity index 100% rename from pkg/chartutil/testdata/albatross/values.yaml rename to pkg/chart/loader/testdata/albatross/values.yaml diff --git a/pkg/chart/loader/testdata/frobnitz-1.2.3.tgz b/pkg/chart/loader/testdata/frobnitz-1.2.3.tgz new file mode 100644 index 00000000000..b2b76a83c76 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz-1.2.3.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz.v1.tgz b/pkg/chart/loader/testdata/frobnitz.v1.tgz new file mode 100644 index 00000000000..6282f9b73fb Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz.v1.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz_backslash/.helmignore b/pkg/chart/loader/testdata/frobnitz.v1/.helmignore old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/.helmignore rename to pkg/chart/loader/testdata/frobnitz.v1/.helmignore diff --git a/pkg/chartutil/testdata/dependent-chart-alias/requirements.lock b/pkg/chart/loader/testdata/frobnitz.v1/Chart.lock similarity index 100% rename from pkg/chartutil/testdata/dependent-chart-alias/requirements.lock rename to pkg/chart/loader/testdata/frobnitz.v1/Chart.lock diff --git a/pkg/chart/loader/testdata/frobnitz.v1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz.v1/Chart.yaml new file mode 100644 index 00000000000..134cd11090a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v1/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue diff --git a/pkg/chartutil/testdata/frobnitz_backslash/INSTALL.txt b/pkg/chart/loader/testdata/frobnitz.v1/INSTALL.txt old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/INSTALL.txt rename to pkg/chart/loader/testdata/frobnitz.v1/INSTALL.txt diff --git a/pkg/chart/loader/testdata/frobnitz.v1/LICENSE b/pkg/chart/loader/testdata/frobnitz.v1/LICENSE new file mode 100644 index 00000000000..6121943b10a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v1/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/chartutil/testdata/frobnitz_backslash/README.md b/pkg/chart/loader/testdata/frobnitz.v1/README.md old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/README.md rename to pkg/chart/loader/testdata/frobnitz.v1/README.md diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/_ignore_me b/pkg/chart/loader/testdata/frobnitz.v1/charts/_ignore_me old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/charts/_ignore_me rename to pkg/chart/loader/testdata/frobnitz.v1/charts/_ignore_me diff --git a/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/Chart.yaml b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/Chart.yaml new file mode 100644 index 00000000000..79e0d65db6a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/README.md b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/README.md new file mode 100644 index 00000000000..b30b949ddfe --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000000..1c9dd5fa425 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/values.yaml old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml rename to pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000000..61cb6205111 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/templates/alpine-pod.yaml b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000000..21ae20aad53 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/values.yaml b/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/values.yaml old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/values.yaml rename to pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/values.yaml diff --git a/pkg/chart/loader/testdata/frobnitz.v1/charts/mariner-4.3.2.tgz b/pkg/chart/loader/testdata/frobnitz.v1/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000000..3190136b050 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz.v1/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz_backslash/docs/README.md b/pkg/chart/loader/testdata/frobnitz.v1/docs/README.md old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/docs/README.md rename to pkg/chart/loader/testdata/frobnitz.v1/docs/README.md diff --git a/pkg/chartutil/testdata/frobnitz_backslash/icon.svg b/pkg/chart/loader/testdata/frobnitz.v1/icon.svg old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/icon.svg rename to pkg/chart/loader/testdata/frobnitz.v1/icon.svg diff --git a/pkg/chart/loader/testdata/frobnitz.v1/ignore/me.txt b/pkg/chart/loader/testdata/frobnitz.v1/ignore/me.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/requirements.yaml b/pkg/chart/loader/testdata/frobnitz.v1/requirements.yaml similarity index 100% rename from pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/requirements.yaml rename to pkg/chart/loader/testdata/frobnitz.v1/requirements.yaml diff --git a/pkg/chartutil/testdata/frobnitz_backslash/templates/template.tpl b/pkg/chart/loader/testdata/frobnitz.v1/templates/template.tpl old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/templates/template.tpl rename to pkg/chart/loader/testdata/frobnitz.v1/templates/template.tpl diff --git a/pkg/chartutil/testdata/frobnitz_backslash/values.yaml b/pkg/chart/loader/testdata/frobnitz.v1/values.yaml old mode 100755 new mode 100644 similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/values.yaml rename to pkg/chart/loader/testdata/frobnitz.v1/values.yaml diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/.helmignore b/pkg/chart/loader/testdata/frobnitz.v2.reqs/.helmignore new file mode 100644 index 00000000000..9973a57b803 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/Chart.yaml b/pkg/chart/loader/testdata/frobnitz.v2.reqs/Chart.yaml new file mode 100644 index 00000000000..f3ab302910b --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/INSTALL.txt b/pkg/chart/loader/testdata/frobnitz.v2.reqs/INSTALL.txt new file mode 100644 index 00000000000..2010438c200 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/LICENSE b/pkg/chart/loader/testdata/frobnitz.v2.reqs/LICENSE new file mode 100644 index 00000000000..6121943b10a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/README.md b/pkg/chart/loader/testdata/frobnitz.v2.reqs/README.md new file mode 100644 index 00000000000..8cf4cc3d7c0 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/_ignore_me b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/_ignore_me new file mode 100644 index 00000000000..2cecca68249 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/Chart.yaml b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/Chart.yaml new file mode 100644 index 00000000000..79e0d65db6a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/README.md b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/README.md new file mode 100644 index 00000000000..b30b949ddfe --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000000..1c9dd5fa425 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/values.yaml b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000000..42c39c262c3 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000000..61cb6205111 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/templates/alpine-pod.yaml b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000000..21ae20aad53 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/values.yaml b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/values.yaml new file mode 100644 index 00000000000..6c2aab7ba9d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/mariner-4.3.2.tgz b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000000..3190136b050 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/docs/README.md b/pkg/chart/loader/testdata/frobnitz.v2.reqs/docs/README.md new file mode 100644 index 00000000000..d40747cafd2 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/icon.svg b/pkg/chart/loader/testdata/frobnitz.v2.reqs/icon.svg new file mode 100644 index 00000000000..8921306066d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/ignore/me.txt b/pkg/chart/loader/testdata/frobnitz.v2.reqs/ignore/me.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/chartutil/testdata/frobnitz/requirements.yaml b/pkg/chart/loader/testdata/frobnitz.v2.reqs/requirements.yaml similarity index 100% rename from pkg/chartutil/testdata/frobnitz/requirements.yaml rename to pkg/chart/loader/testdata/frobnitz.v2.reqs/requirements.yaml diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/templates/template.tpl b/pkg/chart/loader/testdata/frobnitz.v2.reqs/templates/template.tpl new file mode 100644 index 00000000000..c651ee6a03c --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chart/loader/testdata/frobnitz.v2.reqs/values.yaml b/pkg/chart/loader/testdata/frobnitz.v2.reqs/values.yaml new file mode 100644 index 00000000000..61f50125883 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz.v2.reqs/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/chart/loader/testdata/frobnitz/.helmignore b/pkg/chart/loader/testdata/frobnitz/.helmignore new file mode 100644 index 00000000000..9973a57b803 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/chartutil/testdata/frobnitz/requirements.lock b/pkg/chart/loader/testdata/frobnitz/Chart.lock similarity index 100% rename from pkg/chartutil/testdata/frobnitz/requirements.lock rename to pkg/chart/loader/testdata/frobnitz/Chart.lock diff --git a/pkg/chart/loader/testdata/frobnitz/Chart.yaml b/pkg/chart/loader/testdata/frobnitz/Chart.yaml new file mode 100644 index 00000000000..fcd4a4a3764 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chart/loader/testdata/frobnitz/INSTALL.txt b/pkg/chart/loader/testdata/frobnitz/INSTALL.txt new file mode 100644 index 00000000000..2010438c200 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/chart/loader/testdata/frobnitz/LICENSE b/pkg/chart/loader/testdata/frobnitz/LICENSE new file mode 100644 index 00000000000..6121943b10a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/chart/loader/testdata/frobnitz/README.md b/pkg/chart/loader/testdata/frobnitz/README.md new file mode 100644 index 00000000000..8cf4cc3d7c0 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/chart/loader/testdata/frobnitz/charts/_ignore_me b/pkg/chart/loader/testdata/frobnitz/charts/_ignore_me new file mode 100644 index 00000000000..2cecca68249 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/chart/loader/testdata/frobnitz/charts/alpine/Chart.yaml b/pkg/chart/loader/testdata/frobnitz/charts/alpine/Chart.yaml new file mode 100644 index 00000000000..79e0d65db6a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/chart/loader/testdata/frobnitz/charts/alpine/README.md b/pkg/chart/loader/testdata/frobnitz/charts/alpine/README.md new file mode 100644 index 00000000000..b30b949ddfe --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000000..1c9dd5fa425 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml b/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000000..42c39c262c3 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000000..61cb6205111 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/pkg/chart/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000000..21ae20aad53 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/chart/loader/testdata/frobnitz/charts/alpine/values.yaml b/pkg/chart/loader/testdata/frobnitz/charts/alpine/values.yaml new file mode 100644 index 00000000000..6c2aab7ba9d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/chart/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/chart/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000000..3190136b050 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz/docs/README.md b/pkg/chart/loader/testdata/frobnitz/docs/README.md new file mode 100644 index 00000000000..d40747cafd2 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/chart/loader/testdata/frobnitz/icon.svg b/pkg/chart/loader/testdata/frobnitz/icon.svg new file mode 100644 index 00000000000..8921306066d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/chart/loader/testdata/frobnitz/ignore/me.txt b/pkg/chart/loader/testdata/frobnitz/ignore/me.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/chart/loader/testdata/frobnitz/templates/template.tpl b/pkg/chart/loader/testdata/frobnitz/templates/template.tpl new file mode 100644 index 00000000000..c651ee6a03c --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chart/loader/testdata/frobnitz/values.yaml b/pkg/chart/loader/testdata/frobnitz/values.yaml new file mode 100644 index 00000000000..61f50125883 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/chart/loader/testdata/frobnitz_backslash-1.2.3.tgz b/pkg/chart/loader/testdata/frobnitz_backslash-1.2.3.tgz new file mode 100644 index 00000000000..a9d4c11d82b Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_backslash-1.2.3.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/.helmignore b/pkg/chart/loader/testdata/frobnitz_backslash/.helmignore new file mode 100755 index 00000000000..9973a57b803 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/chartutil/testdata/frobnitz_backslash/requirements.lock b/pkg/chart/loader/testdata/frobnitz_backslash/Chart.lock similarity index 100% rename from pkg/chartutil/testdata/frobnitz_backslash/requirements.lock rename to pkg/chart/loader/testdata/frobnitz_backslash/Chart.lock diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_backslash/Chart.yaml new file mode 100755 index 00000000000..b1dd40a5d01 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +name: frobnitz_backslash +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/INSTALL.txt b/pkg/chart/loader/testdata/frobnitz_backslash/INSTALL.txt new file mode 100755 index 00000000000..2010438c200 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/LICENSE b/pkg/chart/loader/testdata/frobnitz_backslash/LICENSE new file mode 100755 index 00000000000..6121943b10a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/README.md b/pkg/chart/loader/testdata/frobnitz_backslash/README.md new file mode 100755 index 00000000000..8cf4cc3d7c0 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/_ignore_me b/pkg/chart/loader/testdata/frobnitz_backslash/charts/_ignore_me new file mode 100755 index 00000000000..2cecca68249 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml new file mode 100755 index 00000000000..79e0d65db6a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/README.md b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/README.md new file mode 100755 index 00000000000..b30b949ddfe --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml new file mode 100755 index 00000000000..1c9dd5fa425 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml new file mode 100755 index 00000000000..42c39c262c3 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100755 index 00000000000..61cb6205111 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml new file mode 100755 index 00000000000..0ac5ca6a806 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service | quote }} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml new file mode 100755 index 00000000000..6c2aab7ba9d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz b/pkg/chart/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz new file mode 100755 index 00000000000..3190136b050 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/docs/README.md b/pkg/chart/loader/testdata/frobnitz_backslash/docs/README.md new file mode 100755 index 00000000000..d40747cafd2 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/icon.svg b/pkg/chart/loader/testdata/frobnitz_backslash/icon.svg new file mode 100755 index 00000000000..8921306066d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/ignore/me.txt b/pkg/chart/loader/testdata/frobnitz_backslash/ignore/me.txt new file mode 100755 index 00000000000..e69de29bb2d diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/templates/template.tpl b/pkg/chart/loader/testdata/frobnitz_backslash/templates/template.tpl new file mode 100755 index 00000000000..c651ee6a03c --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/values.yaml b/pkg/chart/loader/testdata/frobnitz_backslash/values.yaml new file mode 100755 index 00000000000..61f50125883 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_backslash/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom.tgz b/pkg/chart/loader/testdata/frobnitz_with_bom.tgz new file mode 100644 index 00000000000..be0cd027db3 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_with_bom.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/.helmignore b/pkg/chart/loader/testdata/frobnitz_with_bom/.helmignore new file mode 100644 index 00000000000..7a4b92da268 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/.helmignore @@ -0,0 +1 @@ +īģŋignore/ diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.lock b/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.lock new file mode 100644 index 00000000000..ed43b227f32 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.lock @@ -0,0 +1,8 @@ +īģŋdependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.yaml new file mode 100644 index 00000000000..21b21f0b508 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.yaml @@ -0,0 +1,27 @@ +īģŋapiVersion: v1 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/INSTALL.txt b/pkg/chart/loader/testdata/frobnitz_with_bom/INSTALL.txt new file mode 100644 index 00000000000..77c4e724a75 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/INSTALL.txt @@ -0,0 +1 @@ +īģŋThis is an install document. The client may display this. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/LICENSE b/pkg/chart/loader/testdata/frobnitz_with_bom/LICENSE new file mode 100644 index 00000000000..c27b00bf232 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/LICENSE @@ -0,0 +1 @@ +īģŋLICENSE placeholder. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/README.md b/pkg/chart/loader/testdata/frobnitz_with_bom/README.md new file mode 100644 index 00000000000..e9c40031b2c --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/README.md @@ -0,0 +1,11 @@ +īģŋ# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/_ignore_me b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/_ignore_me new file mode 100644 index 00000000000..a7e3a38b726 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/_ignore_me @@ -0,0 +1 @@ +īģŋThis should be ignored by the loader, but may be included in a chart. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml new file mode 100644 index 00000000000..adb9853c63c --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +īģŋapiVersion: v1 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/README.md b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/README.md new file mode 100644 index 00000000000..ea7526beefe --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/README.md @@ -0,0 +1,9 @@ +īģŋThis example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000000..1ad84b34634 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +īģŋapiVersion: v1 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000000..f690d53c422 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +īģŋ# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000000..61cb6205111 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000000..f3e662a281b --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +īģŋapiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml new file mode 100644 index 00000000000..6b7cb259602 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +īģŋ# The pod name +name: "my-alpine" diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000000..3190136b050 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/docs/README.md b/pkg/chart/loader/testdata/frobnitz_with_bom/docs/README.md new file mode 100644 index 00000000000..816c3e43168 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/docs/README.md @@ -0,0 +1 @@ +īģŋThis is a placeholder for documentation. diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/icon.svg b/pkg/chart/loader/testdata/frobnitz_with_bom/icon.svg new file mode 100644 index 00000000000..8921306066d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/ignore/me.txt b/pkg/chart/loader/testdata/frobnitz_with_bom/ignore/me.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/templates/template.tpl b/pkg/chart/loader/testdata/frobnitz_with_bom/templates/template.tpl new file mode 100644 index 00000000000..bb29c5491ea --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/templates/template.tpl @@ -0,0 +1 @@ +īģŋHello {{.Name | default "world"}} diff --git a/pkg/chart/loader/testdata/frobnitz_with_bom/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_bom/values.yaml new file mode 100644 index 00000000000..c24ceadf902 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_bom/values.yaml @@ -0,0 +1,6 @@ +īģŋ# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/.helmignore b/pkg/chart/loader/testdata/frobnitz_with_dev_null/.helmignore new file mode 100644 index 00000000000..9973a57b803 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/Chart.lock b/pkg/chart/loader/testdata/frobnitz_with_dev_null/Chart.lock new file mode 100644 index 00000000000..6fcc2ed9fbe --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_dev_null/Chart.yaml new file mode 100644 index 00000000000..fcd4a4a3764 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/INSTALL.txt b/pkg/chart/loader/testdata/frobnitz_with_dev_null/INSTALL.txt new file mode 100644 index 00000000000..2010438c200 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/LICENSE b/pkg/chart/loader/testdata/frobnitz_with_dev_null/LICENSE new file mode 100644 index 00000000000..6121943b10a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/README.md b/pkg/chart/loader/testdata/frobnitz_with_dev_null/README.md new file mode 100644 index 00000000000..8cf4cc3d7c0 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me new file mode 100644 index 00000000000..2cecca68249 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml new file mode 100644 index 00000000000..79e0d65db6a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md new file mode 100644 index 00000000000..b30b949ddfe --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000000..1c9dd5fa425 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000000..42c39c262c3 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000000..61cb6205111 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000000..21ae20aad53 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml new file mode 100644 index 00000000000..6c2aab7ba9d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000000..3190136b050 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/docs/README.md b/pkg/chart/loader/testdata/frobnitz_with_dev_null/docs/README.md new file mode 100644 index 00000000000..d40747cafd2 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/icon.svg b/pkg/chart/loader/testdata/frobnitz_with_dev_null/icon.svg new file mode 100644 index 00000000000..8921306066d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/ignore/me.txt b/pkg/chart/loader/testdata/frobnitz_with_dev_null/ignore/me.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/null b/pkg/chart/loader/testdata/frobnitz_with_dev_null/null new file mode 120000 index 00000000000..dc1dc0cde0f --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/null @@ -0,0 +1 @@ +/dev/null \ No newline at end of file diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/templates/template.tpl b/pkg/chart/loader/testdata/frobnitz_with_dev_null/templates/template.tpl new file mode 100644 index 00000000000..c651ee6a03c --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chart/loader/testdata/frobnitz_with_dev_null/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_dev_null/values.yaml new file mode 100644 index 00000000000..61f50125883 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_dev_null/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/.helmignore b/pkg/chart/loader/testdata/frobnitz_with_symlink/.helmignore new file mode 100644 index 00000000000..9973a57b803 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/Chart.lock b/pkg/chart/loader/testdata/frobnitz_with_symlink/Chart.lock new file mode 100644 index 00000000000..6fcc2ed9fbe --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_symlink/Chart.yaml new file mode 100644 index 00000000000..fcd4a4a3764 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/INSTALL.txt b/pkg/chart/loader/testdata/frobnitz_with_symlink/INSTALL.txt new file mode 100644 index 00000000000..2010438c200 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/README.md b/pkg/chart/loader/testdata/frobnitz_with_symlink/README.md new file mode 100644 index 00000000000..8cf4cc3d7c0 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/_ignore_me b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/_ignore_me new file mode 100644 index 00000000000..2cecca68249 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml new file mode 100644 index 00000000000..79e0d65db6a --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md new file mode 100644 index 00000000000..b30b949ddfe --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000000..1c9dd5fa425 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000000..42c39c262c3 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000000..61cb6205111 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000000..21ae20aad53 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml new file mode 100644 index 00000000000..6c2aab7ba9d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000000..3190136b050 Binary files /dev/null and b/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/docs/README.md b/pkg/chart/loader/testdata/frobnitz_with_symlink/docs/README.md new file mode 100644 index 00000000000..d40747cafd2 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/icon.svg b/pkg/chart/loader/testdata/frobnitz_with_symlink/icon.svg new file mode 100644 index 00000000000..8921306066d --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/ignore/me.txt b/pkg/chart/loader/testdata/frobnitz_with_symlink/ignore/me.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/templates/template.tpl b/pkg/chart/loader/testdata/frobnitz_with_symlink/templates/template.tpl new file mode 100644 index 00000000000..c651ee6a03c --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chart/loader/testdata/frobnitz_with_symlink/values.yaml b/pkg/chart/loader/testdata/frobnitz_with_symlink/values.yaml new file mode 100644 index 00000000000..61f50125883 --- /dev/null +++ b/pkg/chart/loader/testdata/frobnitz_with_symlink/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/chart/loader/testdata/genfrob.sh b/pkg/chart/loader/testdata/genfrob.sh new file mode 100755 index 00000000000..35fdd59f20c --- /dev/null +++ b/pkg/chart/loader/testdata/genfrob.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# Pack the albatross chart into the mariner chart. +echo "Packing albatross into mariner" +tar -zcvf mariner/charts/albatross-0.1.0.tgz albatross + +echo "Packing mariner into frobnitz" +tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner +tar -zcvf frobnitz_backslash/charts/mariner-4.3.2.tgz mariner + +# Pack the frobnitz chart. +echo "Packing frobnitz" +tar --exclude=ignore/* -zcvf frobnitz-1.2.3.tgz frobnitz +tar --exclude=ignore/* -zcvf frobnitz_backslash-1.2.3.tgz frobnitz_backslash diff --git a/pkg/chart/loader/testdata/mariner/Chart.yaml b/pkg/chart/loader/testdata/mariner/Chart.yaml new file mode 100644 index 00000000000..92dc4b39096 --- /dev/null +++ b/pkg/chart/loader/testdata/mariner/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +name: mariner +description: A Helm chart for Kubernetes +version: 4.3.2 +home: "" +dependencies: + - name: albatross + repository: https://example.com/mariner/charts + version: "0.1.0" diff --git a/pkg/chart/loader/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/chart/loader/testdata/mariner/charts/albatross-0.1.0.tgz new file mode 100644 index 00000000000..128ef82f7a0 Binary files /dev/null and b/pkg/chart/loader/testdata/mariner/charts/albatross-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/mariner/templates/placeholder.tpl b/pkg/chart/loader/testdata/mariner/templates/placeholder.tpl similarity index 100% rename from pkg/chartutil/testdata/mariner/templates/placeholder.tpl rename to pkg/chart/loader/testdata/mariner/templates/placeholder.tpl diff --git a/pkg/chartutil/testdata/mariner/values.yaml b/pkg/chart/loader/testdata/mariner/values.yaml similarity index 100% rename from pkg/chartutil/testdata/mariner/values.yaml rename to pkg/chart/loader/testdata/mariner/values.yaml diff --git a/pkg/chart/metadata.go b/pkg/chart/metadata.go new file mode 100644 index 00000000000..1925e45ac65 --- /dev/null +++ b/pkg/chart/metadata.go @@ -0,0 +1,160 @@ +/* +Copyright The Helm Authors. +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 chart + +import ( + "strings" + "unicode" + + "github.com/Masterminds/semver/v3" +) + +// Maintainer describes a Chart maintainer. +type Maintainer struct { + // Name is a user name or organization name + Name string `json:"name,omitempty"` + // Email is an optional email address to contact the named maintainer + Email string `json:"email,omitempty"` + // URL is an optional URL to an address for the named maintainer + URL string `json:"url,omitempty"` +} + +// Validate checks valid data and sanitizes string characters. +func (m *Maintainer) Validate() error { + m.Name = sanitizeString(m.Name) + m.Email = sanitizeString(m.Email) + m.URL = sanitizeString(m.URL) + return nil +} + +// Metadata for a Chart file. This models the structure of a Chart.yaml file. +type Metadata struct { + // The name of the chart. Required. + Name string `json:"name,omitempty"` + // The URL to a relevant project page, git repo, or contact person + Home string `json:"home,omitempty"` + // Source is the URL to the source code of this chart + Sources []string `json:"sources,omitempty"` + // A SemVer 2 conformant version string of the chart. Required. + Version string `json:"version,omitempty"` + // A one-sentence description of the chart + Description string `json:"description,omitempty"` + // A list of string keywords + Keywords []string `json:"keywords,omitempty"` + // A list of name and URL/email address combinations for the maintainer(s) + Maintainers []*Maintainer `json:"maintainers,omitempty"` + // The URL to an icon file. + Icon string `json:"icon,omitempty"` + // The API Version of this chart. Required. + APIVersion string `json:"apiVersion,omitempty"` + // The condition to check to enable chart + Condition string `json:"condition,omitempty"` + // The tags to check to enable chart + Tags string `json:"tags,omitempty"` + // The version of the application enclosed inside of this chart. + AppVersion string `json:"appVersion,omitempty"` + // Whether or not this chart is deprecated + Deprecated bool `json:"deprecated,omitempty"` + // Annotations are additional mappings uninterpreted by Helm, + // made available for inspection by other applications. + Annotations map[string]string `json:"annotations,omitempty"` + // KubeVersion is a SemVer constraint specifying the version of Kubernetes required. + KubeVersion string `json:"kubeVersion,omitempty"` + // Dependencies are a list of dependencies for a chart. + Dependencies []*Dependency `json:"dependencies,omitempty"` + // Specifies the chart type: application or library + Type string `json:"type,omitempty"` +} + +// Validate checks the metadata for known issues and sanitizes string +// characters. +func (md *Metadata) Validate() error { + if md == nil { + return ValidationError("chart.metadata is required") + } + + md.Name = sanitizeString(md.Name) + md.Description = sanitizeString(md.Description) + md.Home = sanitizeString(md.Home) + md.Icon = sanitizeString(md.Icon) + md.Condition = sanitizeString(md.Condition) + md.Tags = sanitizeString(md.Tags) + md.AppVersion = sanitizeString(md.AppVersion) + md.KubeVersion = sanitizeString(md.KubeVersion) + for i := range md.Sources { + md.Sources[i] = sanitizeString(md.Sources[i]) + } + for i := range md.Keywords { + md.Keywords[i] = sanitizeString(md.Keywords[i]) + } + + if md.APIVersion == "" { + return ValidationError("chart.metadata.apiVersion is required") + } + if md.Name == "" { + return ValidationError("chart.metadata.name is required") + } + if md.Version == "" { + return ValidationError("chart.metadata.version is required") + } + if !isValidSemver(md.Version) { + return ValidationErrorf("chart.metadata.version %q is invalid", md.Version) + } + if !isValidChartType(md.Type) { + return ValidationError("chart.metadata.type must be application or library") + } + + for _, m := range md.Maintainers { + if err := m.Validate(); err != nil { + return err + } + } + + // Aliases need to be validated here to make sure that the alias name does + // not contain any illegal characters. + for _, dependency := range md.Dependencies { + if err := dependency.Validate(); err != nil { + return err + } + } + return nil +} + +func isValidChartType(in string) bool { + switch in { + case "", "application", "library": + return true + } + return false +} + +func isValidSemver(v string) bool { + _, err := semver.NewVersion(v) + return err == nil +} + +// sanitizeString normalize spaces and removes non-printable characters. +func sanitizeString(str string) string { + return strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return ' ' + } + if unicode.IsPrint(r) { + return r + } + return -1 + }, str) +} diff --git a/pkg/chart/metadata_test.go b/pkg/chart/metadata_test.go new file mode 100644 index 00000000000..9f881a4e105 --- /dev/null +++ b/pkg/chart/metadata_test.go @@ -0,0 +1,100 @@ +/* +Copyright The Helm Authors. + +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 chart + +import ( + "testing" +) + +func TestValidate(t *testing.T) { + tests := []struct { + md *Metadata + err error + }{ + { + nil, + ValidationError("chart.metadata is required"), + }, + { + &Metadata{Name: "test", Version: "1.0"}, + ValidationError("chart.metadata.apiVersion is required"), + }, + { + &Metadata{APIVersion: "v2", Version: "1.0"}, + ValidationError("chart.metadata.name is required"), + }, + { + &Metadata{Name: "test", APIVersion: "v2"}, + ValidationError("chart.metadata.version is required"), + }, + { + &Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "test"}, + ValidationError("chart.metadata.type must be application or library"), + }, + { + &Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "application"}, + nil, + }, + { + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "dependency", Alias: "legal-alias"}, + }, + }, + nil, + }, + { + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "bad", Alias: "illegal alias"}, + }, + }, + ValidationError("dependency \"bad\" has disallowed characters in the alias"), + }, + { + &Metadata{APIVersion: "v2", Name: "test", Version: "1.2.3.4"}, + ValidationError("chart.metadata.version \"1.2.3.4\" is invalid"), + }, + } + + for _, tt := range tests { + result := tt.md.Validate() + if result != tt.err { + t.Errorf("expected '%s', got '%s'", tt.err, result) + } + } +} + +func TestValidate_sanitize(t *testing.T) { + md := &Metadata{APIVersion: "v2", Name: "test", Version: "1.0", Description: "\adescr\u0081iption\rtest", Maintainers: []*Maintainer{{Name: "\r"}}} + if err := md.Validate(); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if md.Description != "description test" { + t.Fatalf("description was not sanitized: %q", md.Description) + } + if md.Maintainers[0].Name != " " { + t.Fatal("maintainer name was not sanitized") + } +} diff --git a/pkg/chartutil/capabilities.go b/pkg/chartutil/capabilities.go index c87c0368ea4..c002e33f22b 100644 --- a/pkg/chartutil/capabilities.go +++ b/pkg/chartutil/capabilities.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -17,55 +17,88 @@ package chartutil import ( "fmt" - "runtime" + "strconv" - "k8s.io/apimachinery/pkg/version" - tversion "k8s.io/helm/pkg/proto/hapi/version" + "k8s.io/client-go/kubernetes/scheme" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + + helmversion "helm.sh/helm/v3/internal/version" +) + +const ( + k8sVersionMajor = 1 + k8sVersionMinor = 20 ) var ( // DefaultVersionSet is the default version set, which includes only Core V1 ("v1"). - DefaultVersionSet = NewVersionSet("v1") - - // DefaultKubeVersion is the default kubernetes version - DefaultKubeVersion = &version.Info{ - Major: "1", - Minor: "9", - GitVersion: "v1.9.0", - GoVersion: runtime.Version(), - Compiler: runtime.Compiler, - Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + DefaultVersionSet = allKnownVersions() + + // DefaultCapabilities is the default set of capabilities. + DefaultCapabilities = &Capabilities{ + KubeVersion: KubeVersion{ + Version: fmt.Sprintf("v%d.%d.0", k8sVersionMajor, k8sVersionMinor), + Major: strconv.Itoa(k8sVersionMajor), + Minor: strconv.Itoa(k8sVersionMinor), + }, + APIVersions: DefaultVersionSet, + HelmVersion: helmversion.Get(), } ) -// Capabilities describes the capabilities of the Kubernetes cluster that Tiller is attached to. +// Capabilities describes the capabilities of the Kubernetes cluster. type Capabilities struct { - // List of all supported API versions + // KubeVersion is the Kubernetes version. + KubeVersion KubeVersion + // APIversions are supported Kubernetes API versions. APIVersions VersionSet - // KubeVerison is the Kubernetes version - KubeVersion *version.Info - // TillerVersion is the Tiller version - // - // This always comes from pkg/version.GetVersionProto(). - TillerVersion *tversion.Version + // HelmVersion is the build information for this helm version + HelmVersion helmversion.BuildInfo } -// VersionSet is a set of Kubernetes API versions. -type VersionSet map[string]interface{} - -// NewVersionSet creates a new version set from a list of strings. -func NewVersionSet(apiVersions ...string) VersionSet { - vs := VersionSet{} - for _, v := range apiVersions { - vs[v] = struct{}{} - } - return vs +// KubeVersion is the Kubernetes version. +type KubeVersion struct { + Version string // Kubernetes version + Major string // Kubernetes major version + Minor string // Kubernetes minor version } +// String implements fmt.Stringer +func (kv *KubeVersion) String() string { return kv.Version } + +// GitVersion returns the Kubernetes version string. +// +// Deprecated: use KubeVersion.Version. +func (kv *KubeVersion) GitVersion() string { return kv.Version } + +// VersionSet is a set of Kubernetes API versions. +type VersionSet []string + // Has returns true if the version string is in the set. // -// vs.Has("extensions/v1beta1") +// vs.Has("apps/v1") func (v VersionSet) Has(apiVersion string) bool { - _, ok := v[apiVersion] - return ok + for _, x := range v { + if x == apiVersion { + return true + } + } + return false +} + +func allKnownVersions() VersionSet { + // We should register the built in extension APIs as well so CRDs are + // supported in the default version set. This has caused problems with `helm + // template` in the past, so let's be safe + apiextensionsv1beta1.AddToScheme(scheme.Scheme) + apiextensionsv1.AddToScheme(scheme.Scheme) + + groups := scheme.Scheme.PrioritizedVersionsAllGroups() + vs := make(VersionSet, 0, len(groups)) + for _, gv := range groups { + vs = append(vs, gv.String()) + } + return vs } diff --git a/pkg/chartutil/capabilities_test.go b/pkg/chartutil/capabilities_test.go index ac20f0038c3..7134abfc59d 100644 --- a/pkg/chartutil/capabilities_test.go +++ b/pkg/chartutil/capabilities_test.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -20,13 +20,13 @@ import ( ) func TestVersionSet(t *testing.T) { - vs := NewVersionSet("v1", "extensions/v1beta1") + vs := VersionSet{"v1", "apps/v1"} if d := len(vs); d != 2 { t.Errorf("Expected 2 versions, got %d", d) } - if !vs.Has("extensions/v1beta1") { - t.Error("Expected to find extensions/v1beta1") + if !vs.Has("apps/v1") { + t.Error("Expected to find apps/v1") } if vs.Has("Spanish/inquisition") { @@ -38,17 +38,31 @@ func TestDefaultVersionSet(t *testing.T) { if !DefaultVersionSet.Has("v1") { t.Error("Expected core v1 version set") } - if d := len(DefaultVersionSet); d != 1 { - t.Errorf("Expected only one version, got %d", d) - } } -func TestCapabilities(t *testing.T) { - cap := Capabilities{ - APIVersions: DefaultVersionSet, +func TestDefaultCapabilities(t *testing.T) { + kv := DefaultCapabilities.KubeVersion + if kv.String() != "v1.20.0" { + t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String()) + } + if kv.Version != "v1.20.0" { + t.Errorf("Expected default KubeVersion.Version to be v1.20.0, got %q", kv.Version) + } + if kv.GitVersion() != "v1.20.0" { + t.Errorf("Expected default KubeVersion.GitVersion() to be v1.20.0, got %q", kv.Version) + } + if kv.Major != "1" { + t.Errorf("Expected default KubeVersion.Major to be 1, got %q", kv.Major) } + if kv.Minor != "20" { + t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor) + } +} + +func TestDefaultCapabilitiesHelmVersion(t *testing.T) { + hv := DefaultCapabilities.HelmVersion - if !cap.APIVersions.Has("v1") { - t.Error("APIVersions should have v1") + if hv.Version != "v3.5" { + t.Errorf("Expected default HelmVersion to be v3.5, got %q", hv.Version) } } diff --git a/pkg/chartutil/chartfile.go b/pkg/chartutil/chartfile.go index c2879cdae57..808a902b128 100644 --- a/pkg/chartutil/chartfile.go +++ b/pkg/chartutil/chartfile.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,46 +17,41 @@ limitations under the License. package chartutil import ( - "errors" - "fmt" "io/ioutil" "os" "path/filepath" - "github.com/ghodss/yaml" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" - "k8s.io/helm/pkg/proto/hapi/chart" + "helm.sh/helm/v3/pkg/chart" ) -// ApiVersionV1 is the API version number for version 1. -// -// This is ApiVersionV1 instead of APIVersionV1 to match the protobuf-generated name. -const ApiVersionV1 = "v1" // nolint - -// UnmarshalChartfile takes raw Chart.yaml data and unmarshals it. -func UnmarshalChartfile(data []byte) (*chart.Metadata, error) { - y := &chart.Metadata{} - err := yaml.Unmarshal(data, y) - if err != nil { - return nil, err - } - return y, nil -} - // LoadChartfile loads a Chart.yaml file into a *chart.Metadata. func LoadChartfile(filename string) (*chart.Metadata, error) { b, err := ioutil.ReadFile(filename) if err != nil { return nil, err } - return UnmarshalChartfile(b) + y := new(chart.Metadata) + err = yaml.Unmarshal(b, y) + return y, err } // SaveChartfile saves the given metadata as a Chart.yaml file at the given path. // // 'filename' should be the complete path and filename ('foo/Chart.yaml') func SaveChartfile(filename string, cf *chart.Metadata) error { + // Pull out the dependencies of a v1 Chart, since there's no way + // to tell the serializer to skip a field for just this use case + savedDependencies := cf.Dependencies + if cf.APIVersion == chart.APIVersionV1 { + cf.Dependencies = nil + } out, err := yaml.Marshal(cf) + if cf.APIVersion == chart.APIVersionV1 { + cf.Dependencies = savedDependencies + } if err != nil { return err } @@ -70,28 +65,28 @@ func IsChartDir(dirName string) (bool, error) { if fi, err := os.Stat(dirName); err != nil { return false, err } else if !fi.IsDir() { - return false, fmt.Errorf("%q is not a directory", dirName) + return false, errors.Errorf("%q is not a directory", dirName) } - chartYaml := filepath.Join(dirName, "Chart.yaml") + chartYaml := filepath.Join(dirName, ChartfileName) if _, err := os.Stat(chartYaml); os.IsNotExist(err) { - return false, fmt.Errorf("no Chart.yaml exists in directory %q", dirName) + return false, errors.Errorf("no %s exists in directory %q", ChartfileName, dirName) } chartYamlContent, err := ioutil.ReadFile(chartYaml) if err != nil { - return false, fmt.Errorf("cannot read Chart.Yaml in directory %q", dirName) + return false, errors.Errorf("cannot read %s in directory %q", ChartfileName, dirName) } - chartContent, err := UnmarshalChartfile(chartYamlContent) - if err != nil { + chartContent := new(chart.Metadata) + if err := yaml.Unmarshal(chartYamlContent, &chartContent); err != nil { return false, err } if chartContent == nil { - return false, errors.New("chart metadata (Chart.yaml) missing") + return false, errors.Errorf("chart metadata (%s) missing", ChartfileName) } if chartContent.Name == "" { - return false, errors.New("invalid chart (Chart.yaml): name must not be empty") + return false, errors.Errorf("invalid chart (%s): name must not be empty", ChartfileName) } return true, nil diff --git a/pkg/chartutil/chartfile_test.go b/pkg/chartutil/chartfile_test.go old mode 100755 new mode 100644 index 5b36dc95566..fb5f1537694 --- a/pkg/chartutil/chartfile_test.go +++ b/pkg/chartutil/chartfile_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ package chartutil import ( "testing" - "k8s.io/helm/pkg/proto/hapi/chart" + "helm.sh/helm/v3/pkg/chart" ) const testfile = "testdata/chartfiletest.yaml" @@ -39,9 +39,8 @@ func verifyChartfile(t *testing.T, f *chart.Metadata, name string) { t.Fatal("Failed verifyChartfile because f is nil") } - // Api instead of API because it was generated via protobuf. - if f.ApiVersion != ApiVersionV1 { - t.Errorf("Expected API Version %q, got %q", ApiVersionV1, f.ApiVersion) + if f.APIVersion != chart.APIVersionV1 { + t.Errorf("Expected API Version %q, got %q", chart.APIVersionV1, f.APIVersion) } if f.Name != name { diff --git a/pkg/chartutil/coalesce.go b/pkg/chartutil/coalesce.go new file mode 100644 index 00000000000..e086d8b6e68 --- /dev/null +++ b/pkg/chartutil/coalesce.go @@ -0,0 +1,207 @@ +/* +Copyright The Helm Authors. + +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 chartutil + +import ( + "log" + + "github.com/mitchellh/copystructure" + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chart" +) + +// CoalesceValues coalesces all of the values in a chart (and its subcharts). +// +// Values are coalesced together using the following rules: +// +// - Values in a higher level chart always override values in a lower-level +// dependency chart +// - Scalar values and arrays are replaced, maps are merged +// - 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 map[string]interface{}) (Values, error) { + v, err := copystructure.Copy(vals) + if err != nil { + return vals, err + } + + valsCopy := v.(map[string]interface{}) + // if we have an empty map, make sure it is initialized + if valsCopy == nil { + valsCopy = make(map[string]interface{}) + } + return coalesce(chrt, valsCopy) +} + +// coalesce coalesces the dest values and the chart values, giving priority to the dest values. +// +// This is a helper function for CoalesceValues. +func coalesce(ch *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) { + coalesceValues(ch, dest) + return coalesceDeps(ch, dest) +} + +// coalesceDeps coalesces the dependencies of the given chart. +func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) { + for _, subchart := range chrt.Dependencies() { + if c, ok := dest[subchart.Name()]; !ok { + // If dest doesn't already have the key, create it. + dest[subchart.Name()] = make(map[string]interface{}) + } else if !istable(c) { + return dest, errors.Errorf("type mismatch on %s: %t", subchart.Name(), c) + } + if dv, ok := dest[subchart.Name()]; ok { + dvmap := dv.(map[string]interface{}) + + // Get globals out of dest and merge them into dvmap. + coalesceGlobals(dvmap, dest) + + // Now coalesce the rest of the values. + var err error + dest[subchart.Name()], err = coalesce(subchart, dvmap) + if err != nil { + return dest, err + } + } + } + return dest, nil +} + +// coalesceGlobals copies the globals out of src and merges them into dest. +// +// For convenience, returns dest. +func coalesceGlobals(dest, src map[string]interface{}) { + var dg, sg map[string]interface{} + + if destglob, ok := dest[GlobalKey]; !ok { + dg = make(map[string]interface{}) + } else if dg, ok = destglob.(map[string]interface{}); !ok { + log.Printf("warning: skipping globals because destination %s is not a table.", GlobalKey) + return + } + + if srcglob, ok := src[GlobalKey]; !ok { + sg = make(map[string]interface{}) + } else if sg, ok = srcglob.(map[string]interface{}); !ok { + log.Printf("warning: skipping globals because source %s is not a table.", GlobalKey) + return + } + + // EXPERIMENTAL: In the past, we have disallowed globals to test tables. This + // reverses that decision. It may somehow be possible to introduce a loop + // here, but I haven't found a way. So for the time being, let's allow + // tables in globals. + for key, val := range sg { + if istable(val) { + vv := copyMap(val.(map[string]interface{})) + if destv, ok := dg[key]; !ok { + // Here there is no merge. We're just adding. + dg[key] = vv + } else { + if destvmap, ok := destv.(map[string]interface{}); !ok { + log.Printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key) + } else { + // Basically, we reverse order of coalesce here to merge + // top-down. + CoalesceTables(vv, destvmap) + dg[key] = vv + continue + } + } + } else if dv, ok := dg[key]; ok && istable(dv) { + // It's not clear if this condition can actually ever trigger. + log.Printf("key %s is table. Skipping", key) + continue + } + // TODO: Do we need to do any additional checking on the value? + dg[key] = val + } + dest[GlobalKey] = dg +} + +func copyMap(src map[string]interface{}) map[string]interface{} { + m := make(map[string]interface{}, len(src)) + for k, v := range src { + m[k] = v + } + return m +} + +// coalesceValues builds up a values map for a particular chart. +// +// Values in v will override the values in the chart. +func coalesceValues(c *chart.Chart, v map[string]interface{}) { + for key, val := range c.Values { + if value, ok := v[key]; ok { + if value == nil { + // When the YAML value is null, we remove the value's key. + // This allows Helm's various sources of values (value files or --set) to + // remove incompatible keys from any previous chart, file, or set values. + delete(v, key) + } else if dest, ok := value.(map[string]interface{}); ok { + // if v[key] is a table, merge nv's val table into v[key]. + src, ok := val.(map[string]interface{}) + if !ok { + // If the original value is nil, there is nothing to coalesce, so we don't print + // the warning but simply continue + if val != nil { + log.Printf("warning: skipped value for %s: Not a table.", key) + } + continue + } + // Because v has higher precedence than nv, dest values override src + // values. + CoalesceTables(dest, src) + } + } else { + // If the key is not in v, copy it from nv. + v[key] = val + } + } +} + +// CoalesceTables merges a source map into a destination map. +// +// dest is considered authoritative. +func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { + // When --reuse-values is set but there are no modifications yet, return new values + if src == nil { + return dst + } + if dst == nil { + return src + } + // Because dest has higher precedence than src, dest values override src + // values. + for key, val := range src { + if dv, ok := dst[key]; ok && dv == nil { + delete(dst, key) + } else if !ok { + dst[key] = val + } else if istable(val) { + if istable(dv) { + CoalesceTables(dv.(map[string]interface{}), val.(map[string]interface{})) + } else { + log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val) + } + } else if istable(dv) && val != nil { + log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val) + } + } + return dst +} diff --git a/pkg/chartutil/coalesce_test.go b/pkg/chartutil/coalesce_test.go new file mode 100644 index 00000000000..5a4656d711a --- /dev/null +++ b/pkg/chartutil/coalesce_test.go @@ -0,0 +1,308 @@ +/* +Copyright The Helm Authors. + +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 chartutil + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v3/pkg/chart" +) + +// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362 +var testCoalesceValuesYaml = []byte(` +top: yup +bottom: null +right: Null +left: NULL +front: ~ +back: "" +nested: + boat: null + +global: + name: Ishmael + subject: Queequeg + nested: + boat: true + +pequod: + global: + name: Stinky + harpooner: Tashtego + nested: + boat: false + sail: true + ahab: + scope: whale + boat: null + nested: + foo: true + bar: null +`) + +func withDeps(c *chart.Chart, deps ...*chart.Chart) *chart.Chart { + c.AddDependency(deps...) + return c +} + +func TestCoalesceValues(t *testing.T) { + is := assert.New(t) + + c := withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "moby"}, + Values: map[string]interface{}{ + "back": "exists", + "bottom": "exists", + "front": "exists", + "left": "exists", + "name": "moby", + "nested": map[string]interface{}{"boat": true}, + "override": "bad", + "right": "exists", + "scope": "moby", + "top": "nope", + }, + }, + withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "pequod"}, + Values: map[string]interface{}{"name": "pequod", "scope": "pequod"}, + }, + &chart.Chart{ + Metadata: &chart.Metadata{Name: "ahab"}, + Values: map[string]interface{}{ + "scope": "ahab", + "name": "ahab", + "boat": true, + "nested": map[string]interface{}{"foo": false, "bar": true}, + }, + }, + ), + &chart.Chart{ + Metadata: &chart.Metadata{Name: "spouter"}, + Values: map[string]interface{}{"scope": "spouter"}, + }, + ) + + vals, err := ReadValues(testCoalesceValuesYaml) + if err != nil { + t.Fatal(err) + } + + // taking a copy of the values before passing it + // to CoalesceValues as argument, so that we can + // use it for asserting later + valsCopy := make(Values, len(vals)) + for key, value := range vals { + valsCopy[key] = value + } + + v, err := CoalesceValues(c, vals) + if err != nil { + t.Fatal(err) + } + j, _ := json.MarshalIndent(v, "", " ") + t.Logf("Coalesced Values: %s", string(j)) + + tests := []struct { + tpl string + expect string + }{ + {"{{.top}}", "yup"}, + {"{{.back}}", ""}, + {"{{.name}}", "moby"}, + {"{{.global.name}}", "Ishmael"}, + {"{{.global.subject}}", "Queequeg"}, + {"{{.global.harpooner}}", ""}, + {"{{.pequod.name}}", "pequod"}, + {"{{.pequod.ahab.name}}", "ahab"}, + {"{{.pequod.ahab.scope}}", "whale"}, + {"{{.pequod.ahab.nested.foo}}", "true"}, + {"{{.pequod.ahab.global.name}}", "Ishmael"}, + {"{{.pequod.ahab.global.subject}}", "Queequeg"}, + {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, + {"{{.pequod.global.name}}", "Ishmael"}, + {"{{.pequod.global.subject}}", "Queequeg"}, + {"{{.spouter.global.name}}", "Ishmael"}, + {"{{.spouter.global.harpooner}}", ""}, + + {"{{.global.nested.boat}}", "true"}, + {"{{.pequod.global.nested.boat}}", "true"}, + {"{{.spouter.global.nested.boat}}", "true"}, + {"{{.pequod.global.nested.sail}}", "true"}, + {"{{.spouter.global.nested.sail}}", ""}, + } + + for _, tt := range tests { + if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect { + t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o) + } + } + + nullKeys := []string{"bottom", "right", "left", "front"} + for _, nullKey := range nullKeys { + if _, ok := v[nullKey]; ok { + t.Errorf("Expected key %q to be removed, still present", nullKey) + } + } + + if _, ok := v["nested"].(map[string]interface{})["boat"]; ok { + t.Error("Expected nested boat key to be removed, still present") + } + + subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{}) + if _, ok := subchart["boat"]; ok { + t.Error("Expected subchart boat key to be removed, still present") + } + + if _, ok := subchart["nested"].(map[string]interface{})["bar"]; ok { + t.Error("Expected subchart nested bar key to be removed, still present") + } + + // CoalesceValues should not mutate the passed arguments + is.Equal(valsCopy, vals) +} + +func TestCoalesceTables(t *testing.T) { + dst := map[string]interface{}{ + "name": "Ishmael", + "address": map[string]interface{}{ + "street": "123 Spouter Inn Ct.", + "city": "Nantucket", + "country": nil, + }, + "details": map[string]interface{}{ + "friends": []string{"Tashtego"}, + }, + "boat": "pequod", + "hole": nil, + } + src := map[string]interface{}{ + "occupation": "whaler", + "address": map[string]interface{}{ + "state": "MA", + "street": "234 Spouter Inn Ct.", + "country": "US", + }, + "details": "empty", + "boat": map[string]interface{}{ + "mast": true, + }, + "hole": "black", + } + + // What we expect is that anything in dst overrides anything in src, but that + // otherwise the values are coalesced. + CoalesceTables(dst, src) + + if dst["name"] != "Ishmael" { + t.Errorf("Unexpected name: %s", dst["name"]) + } + if dst["occupation"] != "whaler" { + t.Errorf("Unexpected occupation: %s", dst["occupation"]) + } + + addr, ok := dst["address"].(map[string]interface{}) + if !ok { + t.Fatal("Address went away.") + } + + if addr["street"].(string) != "123 Spouter Inn Ct." { + t.Errorf("Unexpected address: %v", addr["street"]) + } + + if addr["city"].(string) != "Nantucket" { + t.Errorf("Unexpected city: %v", addr["city"]) + } + + if addr["state"].(string) != "MA" { + t.Errorf("Unexpected state: %v", addr["state"]) + } + + if _, ok = addr["country"]; ok { + t.Error("The country is not left out.") + } + + if det, ok := dst["details"].(map[string]interface{}); !ok { + t.Fatalf("Details is the wrong type: %v", dst["details"]) + } else if _, ok := det["friends"]; !ok { + t.Error("Could not find your friends. Maybe you don't have any. :-(") + } + + if dst["boat"].(string) != "pequod" { + t.Errorf("Expected boat string, got %v", dst["boat"]) + } + + if _, ok = dst["hole"]; ok { + t.Error("The hole still exists.") + } + + dst2 := map[string]interface{}{ + "name": "Ishmael", + "address": map[string]interface{}{ + "street": "123 Spouter Inn Ct.", + "city": "Nantucket", + "country": "US", + }, + "details": map[string]interface{}{ + "friends": []string{"Tashtego"}, + }, + "boat": "pequod", + "hole": "black", + } + + // What we expect is that anything in dst should have all values set, + // this happens when the --reuse-values flag is set but the chart has no modifications yet + CoalesceTables(dst2, nil) + + if dst2["name"] != "Ishmael" { + t.Errorf("Unexpected name: %s", dst2["name"]) + } + + addr2, ok := dst2["address"].(map[string]interface{}) + if !ok { + t.Fatal("Address went away.") + } + + if addr2["street"].(string) != "123 Spouter Inn Ct." { + t.Errorf("Unexpected address: %v", addr2["street"]) + } + + if addr2["city"].(string) != "Nantucket" { + t.Errorf("Unexpected city: %v", addr2["city"]) + } + + if addr2["country"].(string) != "US" { + t.Errorf("Unexpected Country: %v", addr2["country"]) + } + + if det2, ok := dst2["details"].(map[string]interface{}); !ok { + t.Fatalf("Details is the wrong type: %v", dst2["details"]) + } else if _, ok := det2["friends"]; !ok { + t.Error("Could not find your friends. Maybe you don't have any. :-(") + } + + if dst2["boat"].(string) != "pequod" { + t.Errorf("Expected boat string, got %v", dst2["boat"]) + } + + if dst2["hole"].(string) != "black" { + t.Errorf("Expected hole string, got %v", dst2["boat"]) + } +} diff --git a/pkg/chartutil/compatible.go b/pkg/chartutil/compatible.go new file mode 100644 index 00000000000..f4656c91357 --- /dev/null +++ b/pkg/chartutil/compatible.go @@ -0,0 +1,34 @@ +/* +Copyright The Helm Authors. + +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 chartutil + +import "github.com/Masterminds/semver/v3" + +// IsCompatibleRange compares a version to a constraint. +// It returns true if the version matches the constraint, and false in all other cases. +func IsCompatibleRange(constraint, ver string) bool { + sv, err := semver.NewVersion(ver) + if err != nil { + return false + } + + c, err := semver.NewConstraint(constraint) + if err != nil { + return false + } + return c.Check(sv) +} diff --git a/pkg/chartutil/compatible_test.go b/pkg/chartutil/compatible_test.go new file mode 100644 index 00000000000..df7be616121 --- /dev/null +++ b/pkg/chartutil/compatible_test.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm Authors. + +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 version represents the current version of the project. +package chartutil + +import "testing" + +func TestIsCompatibleRange(t *testing.T) { + tests := []struct { + constraint string + ver string + expected bool + }{ + {"v2.0.0-alpha.4", "v2.0.0-alpha.4", true}, + {"v2.0.0-alpha.3", "v2.0.0-alpha.4", false}, + {"v2.0.0", "v2.0.0-alpha.4", false}, + {"v2.0.0-alpha.4", "v2.0.0", false}, + {"~v2.0.0", "v2.0.1", true}, + {"v2", "v2.0.0", true}, + {">2.0.0", "v2.1.1", true}, + {"v2.1.*", "v2.1.1", true}, + } + + for _, tt := range tests { + if IsCompatibleRange(tt.constraint, tt.ver) != tt.expected { + t.Errorf("expected constraint %s to be %v for %s", tt.constraint, tt.expected, tt.ver) + } + } +} diff --git a/pkg/chartutil/create.go b/pkg/chartutil/create.go index 9f2a8cc1fbe..ee6bf7721af 100644 --- a/pkg/chartutil/create.go +++ b/pkg/chartutil/create.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,36 +18,91 @@ package chartutil import ( "fmt" + "io" "io/ioutil" "os" "path/filepath" + "regexp" + "strings" - "k8s.io/helm/pkg/proto/hapi/chart" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" ) +// chartName is a regular expression for testing the supplied name of a chart. +// This regular expression is probably stricter than it needs to be. We can relax it +// somewhat. Newline characters, as well as $, quotes, +, parens, and % are known to be +// problematic. +var chartName = regexp.MustCompile("^[a-zA-Z0-9._-]+$") + const ( // ChartfileName is the default Chart file name. ChartfileName = "Chart.yaml" // ValuesfileName is the default values file name. ValuesfileName = "values.yaml" + // SchemafileName is the default values schema file name. + SchemafileName = "values.schema.json" // TemplatesDir is the relative directory name for templates. TemplatesDir = "templates" // ChartsDir is the relative directory name for charts dependencies. ChartsDir = "charts" + // TemplatesTestsDir is the relative directory name for tests. + TemplatesTestsDir = TemplatesDir + sep + "tests" // IgnorefileName is the name of the Helm ignore file. IgnorefileName = ".helmignore" // IngressFileName is the name of the example ingress file. - IngressFileName = "ingress.yaml" + IngressFileName = TemplatesDir + sep + "ingress.yaml" // DeploymentName is the name of the example deployment file. - DeploymentName = "deployment.yaml" + DeploymentName = TemplatesDir + sep + "deployment.yaml" // ServiceName is the name of the example service file. - ServiceName = "service.yaml" + ServiceName = TemplatesDir + sep + "service.yaml" + // ServiceAccountName is the name of the example serviceaccount file. + ServiceAccountName = TemplatesDir + sep + "serviceaccount.yaml" + // HorizontalPodAutoscalerName is the name of the example hpa file. + HorizontalPodAutoscalerName = TemplatesDir + sep + "hpa.yaml" // NotesName is the name of the example NOTES.txt file. - NotesName = "NOTES.txt" - // HelpersName is the name of the example NOTES.txt file. - HelpersName = "_helpers.tpl" + NotesName = TemplatesDir + sep + "NOTES.txt" + // HelpersName is the name of the example helpers file. + HelpersName = TemplatesDir + sep + "_helpers.tpl" + // TestConnectionName is the name of the example test file. + TestConnectionName = TemplatesTestsDir + sep + "test-connection.yaml" ) +// maxChartNameLength is lower than the limits we know of with certain file systems, +// and with certain Kubernetes fields. +const maxChartNameLength = 250 + +const sep = string(filepath.Separator) + +const defaultChartfile = `apiVersion: v2 +name: %s +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" +` + const defaultValues = `# Default values for %s. # This is a YAML-formatted file. # Declare variables to be passed into your templates. @@ -56,12 +111,36 @@ replicaCount: 1 image: repository: nginx - tag: stable pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" +imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + service: type: ClusterIP port: 80 @@ -71,9 +150,13 @@ ingress: annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" - path: / hosts: - - chart-example.local + - host: chart-example.local + paths: + - path: / + backend: + serviceName: chart-example.local + servicePort: 80 tls: [] # - secretName: chart-example-tls # hosts: @@ -85,11 +168,18 @@ resources: {} # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: - # cpu: 100m - # memory: 128Mi + # cpu: 100m + # memory: 128Mi # requests: - # cpu: 100m - # memory: 128Mi + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 nodeSelector: {} @@ -114,77 +204,92 @@ const defaultIgnore = `# Patterns to ignore when building packages. *.swp *.bak *.tmp +*.orig *~ # Various IDEs .project .idea/ *.tmproj +.vscode/ ` const defaultIngress = `{{- if .Values.ingress.enabled -}} {{- $fullName := include ".fullname" . -}} -{{- $ingressPath := .Values.ingress.path -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} apiVersion: extensions/v1beta1 +{{- end }} kind: Ingress metadata: name: {{ $fullName }} labels: - app: {{ template ".name" . }} - chart: {{ template ".chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} -{{- with .Values.ingress.annotations }} + {{- include ".labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} annotations: -{{ toYaml . | indent 4 }} -{{- end }} + {{- toYaml . | nindent 4 }} + {{- end }} spec: -{{- if .Values.ingress.tls }} + {{- if .Values.ingress.tls }} tls: - {{- range .Values.ingress.tls }} + {{- range .Values.ingress.tls }} - hosts: - {{- range .hosts }} - - {{ . }} - {{- end }} + {{- range .hosts }} + - {{ . | quote }} + {{- end }} secretName: {{ .secretName }} + {{- end }} {{- end }} -{{- end }} rules: - {{- range .Values.ingress.hosts }} - - host: {{ . }} + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} http: paths: - - path: {{ $ingressPath }} + {{- range .paths }} + - path: {{ .path }} backend: serviceName: {{ $fullName }} - servicePort: http + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} {{- end }} -{{- end }} ` -const defaultDeployment = `apiVersion: apps/v1beta2 +const defaultDeployment = `apiVersion: apps/v1 kind: Deployment metadata: - name: {{ template ".fullname" . }} + name: {{ include ".fullname" . }} labels: - app: {{ template ".name" . }} - chart: {{ template ".chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} + {{- include ".labels" . | nindent 4 }} spec: + {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} + {{- end }} selector: matchLabels: - app: {{ template ".name" . }} - release: {{ .Release.Name }} + {{- include ".selectorLabels" . | nindent 6 }} template: metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} labels: - app: {{ template ".name" . }} - release: {{ .Release.Name }} + {{- include ".selectorLabels" . | nindent 8 }} spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include ".serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http @@ -199,30 +304,27 @@ spec: path: / port: http resources: -{{ toYaml .Values.resources | indent 12 }} - {{- with .Values.nodeSelector }} + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} nodeSelector: -{{ toYaml . | indent 8 }} - {{- end }} - {{- with .Values.affinity }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} affinity: -{{ toYaml . | indent 8 }} - {{- end }} - {{- with .Values.tolerations }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} tolerations: -{{ toYaml . | indent 8 }} - {{- end }} + {{- toYaml . | nindent 8 }} + {{- end }} ` const defaultService = `apiVersion: v1 kind: Service metadata: - name: {{ template ".fullname" . }} + name: {{ include ".fullname" . }} labels: - app: {{ template ".name" . }} - chart: {{ template ".chart" . }} - release: {{ .Release.Name }} - heritage: {{ .Release.Service }} + {{- include ".labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: @@ -231,38 +333,83 @@ spec: protocol: TCP name: http selector: - app: {{ template ".name" . }} - release: {{ .Release.Name }} + {{- include ".selectorLabels" . | nindent 4 }} +` + +const defaultServiceAccount = `{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include ".serviceAccountName" . }} + labels: + {{- include ".labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} +` + +const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include ".fullname" . }} + labels: + {{- include ".labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include ".fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} ` const defaultNotes = `1. Get the application URL by running these commands: {{- if .Values.ingress.enabled }} -{{- range .Values.ingress.hosts }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template ".fullname" . }}) + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include ".fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get svc -w {{ template ".fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ template ".fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include ".fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include ".fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") echo http://$SERVICE_IP:{{ .Values.service.port }} {{- 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}") + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include ".name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Visit http://127.0.0.1:8080 to use your application" - kubectl port-forward $POD_NAME 8080:80 + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT {{- end }} ` -const defaultHelpers = `{{/* vim: set filetype=mustache: */}} -{{/* +const defaultHelpers = `{{/* Expand the name of the chart. */}} {{- define ".name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} {{/* Create a default fully qualified app name. @@ -270,44 +417,116 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this If release name contains chart name it will be used as a full name. */}} {{- define ".fullname" -}} -{{- if .Values.fullnameOverride -}} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- if contains $name .Release.Name -}} -{{- .Release.Name | trunc 63 | trimSuffix "-" -}} -{{- else -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} -{{- end -}} -{{- end -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define ".chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} -{{- end -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define ".labels" -}} +helm.sh/chart: {{ include ".chart" . }} +{{ include ".selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define ".selectorLabels" -}} +app.kubernetes.io/name: {{ include ".name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define ".serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include ".fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} +` + +const defaultTestConnection = `apiVersion: v1 +kind: Pod +metadata: + name: "{{ include ".fullname" . }}-test-connection" + labels: + {{- include ".labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include ".fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never ` +// Stderr is an io.Writer to which error messages can be written +// +// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward +// compatibility. +var Stderr io.Writer = os.Stderr + // CreateFrom creates a new chart, but scaffolds it from the src chart. -func CreateFrom(chartfile *chart.Metadata, dest string, src string) error { - schart, err := Load(src) +func CreateFrom(chartfile *chart.Metadata, dest, src string) error { + schart, err := loader.Load(src) if err != nil { - return fmt.Errorf("could not load %s: %s", src, err) + return errors.Wrapf(err, "could not load %s", src) } schart.Metadata = chartfile - var updatedTemplates []*chart.Template + var updatedTemplates []*chart.File for _, template := range schart.Templates { - newData := Transform(string(template.Data), "", schart.Metadata.Name) - updatedTemplates = append(updatedTemplates, &chart.Template{Name: template.Name, Data: newData}) + newData := transform(string(template.Data), schart.Name()) + updatedTemplates = append(updatedTemplates, &chart.File{Name: template.Name, Data: newData}) } schart.Templates = updatedTemplates - schart.Values = &chart.Config{Raw: string(Transform(schart.Values.Raw, "", schart.Metadata.Name))} + b, err := yaml.Marshal(schart.Values) + if err != nil { + return errors.Wrap(err, "reading values file") + } + + var m map[string]interface{} + if err := yaml.Unmarshal(transform(string(b), schart.Name()), &m); err != nil { + return errors.Wrap(err, "transforming values file") + } + schart.Values = m + + // SaveDir looks for the file values.yaml when saving rather than the values + // key in order to preserve the comments in the YAML. The name placeholder + // needs to be replaced on that file. + for _, f := range schart.Raw { + if f.Name == ValuesfileName { + f.Data = transform(string(f.Data), schart.Name()) + } + } return SaveDir(schart, dest) } @@ -325,7 +544,13 @@ func CreateFrom(chartfile *chart.Metadata, dest string, src string) error { // If Chart.yaml or any directories cannot be created, this will return an // error. In such a case, this will attempt to clean up by removing the // new chart directory. -func Create(chartfile *chart.Metadata, dir string) (string, error) { +func Create(name, dir string) (string, error) { + + // Sanity-check the name of a chart so user doesn't create one that causes problems. + if err := validateChartName(name); err != nil { + return "", err + } + path, err := filepath.Abs(dir) if err != nil { return path, err @@ -334,39 +559,27 @@ func Create(chartfile *chart.Metadata, dir string) (string, error) { if fi, err := os.Stat(path); err != nil { return path, err } else if !fi.IsDir() { - return path, fmt.Errorf("no such directory %s", path) + return path, errors.Errorf("no such directory %s", path) } - n := chartfile.Name - cdir := filepath.Join(path, n) + cdir := filepath.Join(path, name) if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() { - return cdir, fmt.Errorf("file %s already exists and is not a directory", cdir) - } - if err := os.MkdirAll(cdir, 0755); err != nil { - return cdir, err - } - - cf := filepath.Join(cdir, ChartfileName) - if _, err := os.Stat(cf); err != nil { - if err := SaveChartfile(cf, chartfile); err != nil { - return cdir, err - } - } - - for _, d := range []string{TemplatesDir, ChartsDir} { - if err := os.MkdirAll(filepath.Join(cdir, d), 0755); err != nil { - return cdir, err - } + return cdir, errors.Errorf("file %s already exists and is not a directory", cdir) } files := []struct { path string content []byte }{ + { + // Chart.yaml + path: filepath.Join(cdir, ChartfileName), + content: []byte(fmt.Sprintf(defaultChartfile, name)), + }, { // values.yaml path: filepath.Join(cdir, ValuesfileName), - content: []byte(fmt.Sprintf(defaultValues, chartfile.Name)), + content: []byte(fmt.Sprintf(defaultValues, name)), }, { // .helmignore @@ -375,39 +588,81 @@ func Create(chartfile *chart.Metadata, dir string) (string, error) { }, { // ingress.yaml - path: filepath.Join(cdir, TemplatesDir, IngressFileName), - content: Transform(defaultIngress, "", chartfile.Name), + path: filepath.Join(cdir, IngressFileName), + content: transform(defaultIngress, name), }, { // deployment.yaml - path: filepath.Join(cdir, TemplatesDir, DeploymentName), - content: Transform(defaultDeployment, "", chartfile.Name), + path: filepath.Join(cdir, DeploymentName), + content: transform(defaultDeployment, name), }, { // service.yaml - path: filepath.Join(cdir, TemplatesDir, ServiceName), - content: Transform(defaultService, "", chartfile.Name), + path: filepath.Join(cdir, ServiceName), + content: transform(defaultService, name), + }, + { + // serviceaccount.yaml + path: filepath.Join(cdir, ServiceAccountName), + content: transform(defaultServiceAccount, name), + }, + { + // hpa.yaml + path: filepath.Join(cdir, HorizontalPodAutoscalerName), + content: transform(defaultHorizontalPodAutoscaler, name), }, { // NOTES.txt - path: filepath.Join(cdir, TemplatesDir, NotesName), - content: Transform(defaultNotes, "", chartfile.Name), + path: filepath.Join(cdir, NotesName), + content: transform(defaultNotes, name), }, { // _helpers.tpl - path: filepath.Join(cdir, TemplatesDir, HelpersName), - content: Transform(defaultHelpers, "", chartfile.Name), + path: filepath.Join(cdir, HelpersName), + content: transform(defaultHelpers, name), + }, + { + // test-connection.yaml + path: filepath.Join(cdir, TestConnectionName), + content: transform(defaultTestConnection, name), }, } for _, file := range files { if _, err := os.Stat(file.path); err == nil { - // File exists and is okay. Skip it. - continue + // There is no handle to a preferred output stream here. + fmt.Fprintf(Stderr, "WARNING: File %q already exists. Overwriting.\n", file.path) } - if err := ioutil.WriteFile(file.path, file.content, 0644); err != nil { + if err := writeFile(file.path, file.content); err != nil { return cdir, err } } + // Need to add the ChartsDir explicitly as it does not contain any file OOTB + if err := os.MkdirAll(filepath.Join(cdir, ChartsDir), 0755); err != nil { + return cdir, err + } return cdir, nil } + +// transform performs a string replacement of the specified source for +// a given key with the replacement string +func transform(src, replacement string) []byte { + return []byte(strings.ReplaceAll(src, "", replacement)) +} + +func writeFile(name string, content []byte) error { + if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil { + return err + } + return ioutil.WriteFile(name, content, 0644) +} + +func validateChartName(name string) error { + if name == "" || len(name) > maxChartNameLength { + return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength) + } + if !chartName.MatchString(name) { + return fmt.Errorf("chart name must match the regular expression %q", chartName.String()) + } + return nil +} diff --git a/pkg/chartutil/create_test.go b/pkg/chartutil/create_test.go index e9af83ad284..9a473fc5902 100644 --- a/pkg/chartutil/create_test.go +++ b/pkg/chartutil/create_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,13 +17,14 @@ limitations under the License. package chartutil import ( + "bytes" "io/ioutil" "os" "path/filepath" - "strings" "testing" - "k8s.io/helm/pkg/proto/hapi/chart" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" ) func TestCreate(t *testing.T) { @@ -33,48 +34,39 @@ func TestCreate(t *testing.T) { } defer os.RemoveAll(tdir) - cf := &chart.Metadata{Name: "foo"} - - c, err := Create(cf, tdir) + c, err := Create("foo", tdir) if err != nil { t.Fatal(err) } dir := filepath.Join(tdir, "foo") - mychart, err := LoadDir(c) + mychart, err := loader.LoadDir(c) if err != nil { t.Fatalf("Failed to load newly created chart %q: %s", c, err) } - if mychart.Metadata.Name != "foo" { - t.Errorf("Expected name to be 'foo', got %q", mychart.Metadata.Name) - } - - for _, d := range []string{TemplatesDir, ChartsDir} { - if fi, err := os.Stat(filepath.Join(dir, d)); err != nil { - t.Errorf("Expected %s dir: %s", d, err) - } else if !fi.IsDir() { - t.Errorf("Expected %s to be a directory.", d) - } + if mychart.Name() != "foo" { + t.Errorf("Expected name to be 'foo', got %q", mychart.Name()) } - for _, f := range []string{ChartfileName, ValuesfileName, IgnorefileName} { - if fi, err := os.Stat(filepath.Join(dir, f)); err != nil { + for _, f := range []string{ + ChartfileName, + DeploymentName, + HelpersName, + IgnorefileName, + NotesName, + ServiceAccountName, + ServiceName, + TemplatesDir, + TemplatesTestsDir, + TestConnectionName, + ValuesfileName, + } { + if _, err := os.Stat(filepath.Join(dir, f)); err != nil { t.Errorf("Expected %s file: %s", f, err) - } else if fi.IsDir() { - t.Errorf("Expected %s to be a file.", f) } } - - for _, f := range []string{NotesName, DeploymentName, ServiceName, HelpersName} { - if fi, err := os.Stat(filepath.Join(dir, TemplatesDir, f)); err != nil { - t.Errorf("Expected %s file: %s", f, err) - } else if fi.IsDir() { - t.Errorf("Expected %s to be a file.", f) - } - } - } func TestCreateFrom(t *testing.T) { @@ -84,51 +76,110 @@ func TestCreateFrom(t *testing.T) { } defer os.RemoveAll(tdir) - cf := &chart.Metadata{Name: "foo"} - srcdir := "./testdata/mariner" + cf := &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "foo", + Version: "0.1.0", + } + srcdir := "./testdata/frobnitz/charts/mariner" if err := CreateFrom(cf, tdir, srcdir); err != nil { t.Fatal(err) } dir := filepath.Join(tdir, "foo") - c := filepath.Join(tdir, cf.Name) - mychart, err := LoadDir(c) + mychart, err := loader.LoadDir(c) if err != nil { t.Fatalf("Failed to load newly created chart %q: %s", c, err) } - if mychart.Metadata.Name != "foo" { - t.Errorf("Expected name to be 'foo', got %q", mychart.Metadata.Name) + if mychart.Name() != "foo" { + t.Errorf("Expected name to be 'foo', got %q", mychart.Name()) } - for _, d := range []string{TemplatesDir, ChartsDir} { - if fi, err := os.Stat(filepath.Join(dir, d)); err != nil { - t.Errorf("Expected %s dir: %s", d, err) - } else if !fi.IsDir() { - t.Errorf("Expected %s to be a directory.", d) + for _, f := range []string{ + ChartfileName, + ValuesfileName, + filepath.Join(TemplatesDir, "placeholder.tpl"), + } { + if _, err := os.Stat(filepath.Join(dir, f)); err != nil { + t.Errorf("Expected %s file: %s", f, err) } - } - for _, f := range []string{ChartfileName, ValuesfileName, "requirements.yaml"} { - if fi, err := os.Stat(filepath.Join(dir, f)); err != nil { - t.Errorf("Expected %s file: %s", f, err) - } else if fi.IsDir() { - t.Errorf("Expected %s to be a file.", f) + // Check each file to make sure has been replaced + b, err := ioutil.ReadFile(filepath.Join(dir, f)) + if err != nil { + t.Errorf("Unable to read file %s: %s", f, err) + } + if bytes.Contains(b, []byte("")) { + t.Errorf("File %s contains ", f) } } +} - for _, f := range []string{"placeholder.tpl"} { - if fi, err := os.Stat(filepath.Join(dir, TemplatesDir, f)); err != nil { - t.Errorf("Expected %s file: %s", f, err) - } else if fi.IsDir() { - t.Errorf("Expected %s to be a file.", f) - } +// TestCreate_Overwrite is a regression test for making sure that files are overwritten. +func TestCreate_Overwrite(t *testing.T) { + tdir, err := ioutil.TempDir("", "helm-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tdir) + + var errlog bytes.Buffer + + if _, err := Create("foo", tdir); err != nil { + t.Fatal(err) + } + + dir := filepath.Join(tdir, "foo") + + tplname := filepath.Join(dir, "templates/hpa.yaml") + writeFile(tplname, []byte("FOO")) + + // Now re-run the create + Stderr = &errlog + if _, err := Create("foo", tdir); err != nil { + t.Fatal(err) } - // Ensure we replace `` - if strings.Contains(mychart.Values.Raw, "") { - t.Errorf("Did not expect %s to be present in %s", "", mychart.Values.Raw) + data, err := ioutil.ReadFile(tplname) + if err != nil { + t.Fatal(err) + } + + if string(data) == "FOO" { + t.Fatal("File that should have been modified was not.") + } + + if errlog.Len() == 0 { + t.Errorf("Expected warnings about overwriting files.") + } +} + +func TestValidateChartName(t *testing.T) { + for name, shouldPass := range map[string]bool{ + "": false, + "abcdefghijklmnopqrstuvwxyz-_.": true, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": true, + "$hello": false, + "Hellô": false, + "he%%o": false, + "he\nllo": false, + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": false, + } { + if err := validateChartName(name); (err != nil) == shouldPass { + t.Errorf("test for %q failed", name) + } } } diff --git a/pkg/chartutil/dependencies.go b/pkg/chartutil/dependencies.go new file mode 100644 index 00000000000..d2e7d6dc97f --- /dev/null +++ b/pkg/chartutil/dependencies.go @@ -0,0 +1,285 @@ +/* +Copyright The Helm Authors. +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 chartutil + +import ( + "log" + "strings" + + "helm.sh/helm/v3/pkg/chart" +) + +// ProcessDependencies checks through this chart's dependencies, processing accordingly. +func ProcessDependencies(c *chart.Chart, v Values) error { + if err := processDependencyEnabled(c, v, ""); err != nil { + return err + } + return processDependencyImportValues(c) +} + +// processDependencyConditions disables charts based on condition path value in values +func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath string) { + if reqs == nil { + return + } + for _, r := range reqs { + for _, c := range strings.Split(strings.TrimSpace(r.Condition), ",") { + if len(c) > 0 { + // retrieve value + vv, err := cvals.PathValue(cpath + c) + if err == nil { + // if not bool, warn + if bv, ok := vv.(bool); ok { + r.Enabled = bv + break + } else { + log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name) + } + } else if _, ok := err.(ErrNoValue); !ok { + // this is a real error + log.Printf("Warning: PathValue returned error %v", err) + } + } + } + } +} + +// processDependencyTags disables charts based on tags in values +func processDependencyTags(reqs []*chart.Dependency, cvals Values) { + if reqs == nil { + return + } + vt, err := cvals.Table("tags") + if err != nil { + return + } + for _, r := range reqs { + var hasTrue, hasFalse bool + for _, k := range r.Tags { + if b, ok := vt[k]; ok { + // if not bool, warn + if bv, ok := b.(bool); ok { + if bv { + hasTrue = true + } else { + hasFalse = true + } + } else { + log.Printf("Warning: Tag '%s' for chart %s returned non-bool value", k, r.Name) + } + } + } + if !hasTrue && hasFalse { + r.Enabled = false + } else if hasTrue || !hasTrue && !hasFalse { + r.Enabled = true + } + } +} + +func getAliasDependency(charts []*chart.Chart, dep *chart.Dependency) *chart.Chart { + for _, c := range charts { + if c == nil { + continue + } + if c.Name() != dep.Name { + continue + } + if !IsCompatibleRange(dep.Version, c.Metadata.Version) { + continue + } + + out := *c + md := *c.Metadata + out.Metadata = &md + + if dep.Alias != "" { + md.Name = dep.Alias + } + return &out + } + return nil +} + +// processDependencyEnabled removes disabled charts from dependencies +func processDependencyEnabled(c *chart.Chart, v map[string]interface{}, path string) error { + if c.Metadata.Dependencies == nil { + return nil + } + + var chartDependencies []*chart.Chart + // If any dependency is not a part of Chart.yaml + // then this should be added to chartDependencies. + // However, if the dependency is already specified in Chart.yaml + // we should not add it, as it would be anyways processed from Chart.yaml + +Loop: + for _, existing := range c.Dependencies() { + for _, req := range c.Metadata.Dependencies { + if existing.Name() == req.Name && IsCompatibleRange(req.Version, existing.Metadata.Version) { + continue Loop + } + } + chartDependencies = append(chartDependencies, existing) + } + + for _, req := range c.Metadata.Dependencies { + if chartDependency := getAliasDependency(c.Dependencies(), req); chartDependency != nil { + chartDependencies = append(chartDependencies, chartDependency) + } + if req.Alias != "" { + req.Name = req.Alias + } + } + c.SetDependencies(chartDependencies...) + + // set all to true + for _, lr := range c.Metadata.Dependencies { + lr.Enabled = true + } + cvals, err := CoalesceValues(c, v) + if err != nil { + return err + } + // flag dependencies as enabled/disabled + processDependencyTags(c.Metadata.Dependencies, cvals) + processDependencyConditions(c.Metadata.Dependencies, cvals, path) + // make a map of charts to remove + rm := map[string]struct{}{} + for _, r := range c.Metadata.Dependencies { + if !r.Enabled { + // remove disabled chart + rm[r.Name] = struct{}{} + } + } + // don't keep disabled charts in new slice + cd := []*chart.Chart{} + copy(cd, c.Dependencies()[:0]) + for _, n := range c.Dependencies() { + if _, ok := rm[n.Metadata.Name]; !ok { + cd = append(cd, n) + } + } + // don't keep disabled charts in metadata + cdMetadata := []*chart.Dependency{} + copy(cdMetadata, c.Metadata.Dependencies[:0]) + for _, n := range c.Metadata.Dependencies { + if _, ok := rm[n.Name]; !ok { + cdMetadata = append(cdMetadata, n) + } + } + + // recursively call self to process sub dependencies + for _, t := range cd { + subpath := path + t.Metadata.Name + "." + if err := processDependencyEnabled(t, cvals, subpath); err != nil { + return err + } + } + // set the correct dependencies in metadata + c.Metadata.Dependencies = nil + c.Metadata.Dependencies = append(c.Metadata.Dependencies, cdMetadata...) + c.SetDependencies(cd...) + + return nil +} + +// pathToMap creates a nested map given a YAML path in dot notation. +func pathToMap(path string, data map[string]interface{}) map[string]interface{} { + if path == "." { + return data + } + return set(parsePath(path), data) +} + +func set(path []string, data map[string]interface{}) map[string]interface{} { + if len(path) == 0 { + return nil + } + cur := data + for i := len(path) - 1; i >= 0; i-- { + cur = map[string]interface{}{path[i]: cur} + } + return cur +} + +// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. +func processImportValues(c *chart.Chart) error { + if c.Metadata.Dependencies == nil { + return nil + } + // combine chart values and empty config to get Values + cvals, err := CoalesceValues(c, nil) + if err != nil { + return err + } + b := make(map[string]interface{}) + // import values from each dependency if specified in import-values + for _, r := range c.Metadata.Dependencies { + var outiv []interface{} + for _, riv := range r.ImportValues { + switch iv := riv.(type) { + case map[string]interface{}: + child := iv["child"].(string) + parent := iv["parent"].(string) + + outiv = append(outiv, map[string]string{ + "child": child, + "parent": parent, + }) + + // get child table + vv, err := cvals.Table(r.Name + "." + child) + if err != nil { + log.Printf("Warning: ImportValues missing table from chart %s: %v", r.Name, err) + continue + } + // create value map from child to be merged into parent + b = CoalesceTables(cvals, pathToMap(parent, vv.AsMap())) + case string: + child := "exports." + iv + outiv = append(outiv, map[string]string{ + "child": child, + "parent": ".", + }) + vm, err := cvals.Table(r.Name + "." + child) + if err != nil { + log.Printf("Warning: ImportValues missing table: %v", err) + continue + } + b = CoalesceTables(b, vm.AsMap()) + } + } + // set our formatted import values + r.ImportValues = outiv + } + + // set the new values + c.Values = CoalesceTables(b, cvals) + + return nil +} + +// processDependencyImportValues imports specified chart values from child to parent. +func processDependencyImportValues(c *chart.Chart) error { + for _, d := range c.Dependencies() { + // recurse + if err := processDependencyImportValues(d); err != nil { + return err + } + } + return processImportValues(c) +} diff --git a/pkg/chartutil/dependencies_test.go b/pkg/chartutil/dependencies_test.go new file mode 100644 index 00000000000..bcb1d40e58f --- /dev/null +++ b/pkg/chartutil/dependencies_test.go @@ -0,0 +1,426 @@ +/* +Copyright The Helm Authors. +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 chartutil + +import ( + "os" + "path/filepath" + "sort" + "strconv" + "testing" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" +) + +func loadChart(t *testing.T, path string) *chart.Chart { + t.Helper() + c, err := loader.Load(path) + if err != nil { + t.Fatalf("failed to load testdata: %s", err) + } + return c +} + +func TestLoadDependency(t *testing.T) { + tests := []*chart.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + + check := func(deps []*chart.Dependency) { + if len(deps) != 2 { + t.Errorf("expected 2 dependencies, got %d", len(deps)) + } + for i, tt := range tests { + if deps[i].Name != tt.Name { + t.Errorf("expected dependency named %q, got %q", tt.Name, deps[i].Name) + } + if deps[i].Version != tt.Version { + t.Errorf("expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, deps[i].Version) + } + if deps[i].Repository != tt.Repository { + t.Errorf("expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, deps[i].Repository) + } + } + } + c := loadChart(t, "testdata/frobnitz") + check(c.Metadata.Dependencies) + check(c.Lock.Dependencies) +} + +func TestDependencyEnabled(t *testing.T) { + type M = map[string]interface{} + tests := []struct { + name string + v M + e []string // expected charts including duplicates in alphanumeric order + }{{ + "tags with no effect", + M{"tags": M{"nothinguseful": false}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"}, + }, { + "tags disabling a group", + M{"tags": M{"front-end": false}}, + []string{"parentchart"}, + }, { + "tags disabling a group and enabling a different group", + M{"tags": M{"front-end": false, "back-end": true}}, + []string{"parentchart", "parentchart.subchart2", "parentchart.subchart2.subchartb", "parentchart.subchart2.subchartc"}, + }, { + "tags disabling only children, children still enabled since tag front-end=true in values.yaml", + M{"tags": M{"subcharta": false, "subchartb": false}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"}, + }, { + "tags disabling all parents/children with additional tag re-enabling a parent", + M{"tags": M{"front-end": false, "subchart1": true, "back-end": false}}, + []string{"parentchart", "parentchart.subchart1"}, + }, { + "conditions enabling the parent charts, but back-end (b, c) is still disabled via values.yaml", + M{"subchart1": M{"enabled": true}, "subchart2": M{"enabled": true}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2"}, + }, { + "conditions disabling the parent charts, effectively disabling children", + M{"subchart1": M{"enabled": false}, "subchart2": M{"enabled": false}}, + []string{"parentchart"}, + }, { + "conditions a child using the second condition path of child's condition", + M{"subchart1": M{"subcharta": M{"enabled": false}}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subchartb"}, + }, { + "tags enabling a parent/child group with condition disabling one child", + M{"subchart2": M{"subchartc": M{"enabled": false}}, "tags": M{"back-end": true}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2", "parentchart.subchart2.subchartb"}, + }, { + "tags will not enable a child if parent is explicitly disabled with condition", + M{"subchart1": M{"enabled": false}, "tags": M{"front-end": true}}, + []string{"parentchart"}, + }, { + "subcharts with alias also respect conditions", + M{"subchart1": M{"enabled": false}, "subchart2alias": M{"enabled": true, "subchartb": M{"enabled": true}}}, + []string{"parentchart", "parentchart.subchart2alias", "parentchart.subchart2alias.subchartb"}, + }} + + for _, tc := range tests { + c := loadChart(t, "testdata/subpop") + t.Run(tc.name, func(t *testing.T) { + if err := processDependencyEnabled(c, tc.v, ""); err != nil { + t.Fatalf("error processing enabled dependencies %v", err) + } + + names := extractChartNames(c) + if len(names) != len(tc.e) { + t.Fatalf("slice lengths do not match got %v, expected %v", len(names), len(tc.e)) + } + for i := range names { + if names[i] != tc.e[i] { + t.Fatalf("slice values do not match got %v, expected %v", names, tc.e) + } + } + }) + } +} + +// extractCharts recursively searches chart dependencies returning all charts found +func extractChartNames(c *chart.Chart) []string { + var out []string + var fn func(c *chart.Chart) + fn = func(c *chart.Chart) { + out = append(out, c.ChartPath()) + for _, d := range c.Dependencies() { + fn(d) + } + } + fn(c) + sort.Strings(out) + return out +} + +func TestProcessDependencyImportValues(t *testing.T) { + c := loadChart(t, "testdata/subpop") + + e := make(map[string]string) + + e["imported-chart1.SC1bool"] = "true" + e["imported-chart1.SC1float"] = "3.14" + e["imported-chart1.SC1int"] = "100" + e["imported-chart1.SC1string"] = "dollywood" + e["imported-chart1.SC1extra1"] = "11" + e["imported-chart1.SPextra1"] = "helm rocks" + e["imported-chart1.SC1extra1"] = "11" + + e["imported-chartA.SCAbool"] = "false" + e["imported-chartA.SCAfloat"] = "3.1" + e["imported-chartA.SCAint"] = "55" + e["imported-chartA.SCAstring"] = "jabba" + e["imported-chartA.SPextra3"] = "1.337" + e["imported-chartA.SC1extra2"] = "1.337" + e["imported-chartA.SCAnested1.SCAnested2"] = "true" + + e["imported-chartA-B.SCAbool"] = "false" + e["imported-chartA-B.SCAfloat"] = "3.1" + e["imported-chartA-B.SCAint"] = "55" + e["imported-chartA-B.SCAstring"] = "jabba" + + e["imported-chartA-B.SCBbool"] = "true" + e["imported-chartA-B.SCBfloat"] = "7.77" + e["imported-chartA-B.SCBint"] = "33" + e["imported-chartA-B.SCBstring"] = "boba" + e["imported-chartA-B.SPextra5"] = "k8s" + e["imported-chartA-B.SC1extra5"] = "tiller" + + e["overridden-chart1.SC1bool"] = "false" + e["overridden-chart1.SC1float"] = "3.141592" + e["overridden-chart1.SC1int"] = "99" + e["overridden-chart1.SC1string"] = "pollywog" + e["overridden-chart1.SPextra2"] = "42" + + e["overridden-chartA.SCAbool"] = "true" + e["overridden-chartA.SCAfloat"] = "41.3" + e["overridden-chartA.SCAint"] = "808" + e["overridden-chartA.SCAstring"] = "jabberwocky" + e["overridden-chartA.SPextra4"] = "true" + + e["overridden-chartA-B.SCAbool"] = "true" + e["overridden-chartA-B.SCAfloat"] = "41.3" + e["overridden-chartA-B.SCAint"] = "808" + e["overridden-chartA-B.SCAstring"] = "jabberwocky" + e["overridden-chartA-B.SCBbool"] = "false" + e["overridden-chartA-B.SCBfloat"] = "1.99" + e["overridden-chartA-B.SCBint"] = "77" + e["overridden-chartA-B.SCBstring"] = "jango" + e["overridden-chartA-B.SPextra6"] = "111" + e["overridden-chartA-B.SCAextra1"] = "23" + e["overridden-chartA-B.SCBextra1"] = "13" + e["overridden-chartA-B.SC1extra6"] = "77" + + // `exports` style + e["SCBexported1B"] = "1965" + e["SC1extra7"] = "true" + e["SCBexported2A"] = "blaster" + e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" + + if err := processDependencyImportValues(c); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + cc := Values(c.Values) + for kk, vv := range e { + pv, err := cc.PathValue(kk) + if err != nil { + t.Fatalf("retrieving import values table %v %v", kk, err) + } + + switch pv := pv.(type) { + case float64: + if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv { + t.Errorf("failed to match imported float value %v with expected %v", s, vv) + } + case bool: + if b := strconv.FormatBool(pv); b != vv { + t.Errorf("failed to match imported bool value %v with expected %v", b, vv) + } + default: + if pv != vv { + t.Errorf("failed to match imported string value %q with expected %q", pv, vv) + } + } + } +} + +func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) { + c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart") + nameOverride := "parent-chart-prod" + + if err := processDependencyImportValues(c); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 1 { + t.Fatal("expected no changes in dependencies") + } + + if len(c.Metadata.Dependencies) != 1 { + t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies)) + } + + prodDependencyValues := c.Dependencies()[0].Values + if prodDependencyValues["nameOverride"] != nameOverride { + t.Fatalf("dependency chart name should be %s but got %s", nameOverride, prodDependencyValues["nameOverride"]) + } +} + +func TestGetAliasDependency(t *testing.T) { + c := loadChart(t, "testdata/frobnitz") + req := c.Metadata.Dependencies + + if len(req) == 0 { + t.Fatalf("there are no dependencies to test") + } + + // Success case + aliasChart := getAliasDependency(c.Dependencies(), req[0]) + if aliasChart == nil { + t.Fatalf("failed to get dependency chart for alias %s", req[0].Name) + } + if req[0].Alias != "" { + if aliasChart.Name() != req[0].Alias { + t.Fatalf("dependency chart name should be %s but got %s", req[0].Alias, aliasChart.Name()) + } + } else if aliasChart.Name() != req[0].Name { + t.Fatalf("dependency chart name should be %s but got %s", req[0].Name, aliasChart.Name()) + } + + if req[0].Version != "" { + if !IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) { + t.Fatalf("dependency chart version is not in the compatible range") + } + } + + // Failure case + req[0].Name = "something-else" + if aliasChart := getAliasDependency(c.Dependencies(), req[0]); aliasChart != nil { + t.Fatalf("expected no chart but got %s", aliasChart.Name()) + } + + req[0].Version = "something else which is not in the compatible range" + if IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) { + t.Fatalf("dependency chart version which is not in the compatible range should cause a failure other than a success ") + } +} + +func TestDependentChartAliases(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-alias") + req := c.Metadata.Dependencies + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 3 { + t.Fatal("expected alias dependencies to be added") + } + + if len(c.Dependencies()) != len(c.Metadata.Dependencies) { + t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies())) + } + + aliasChart := getAliasDependency(c.Dependencies(), req[2]) + + if aliasChart == nil { + t.Fatalf("failed to get dependency chart for alias %s", req[2].Name) + } + if req[2].Alias != "" { + if aliasChart.Name() != req[2].Alias { + t.Fatalf("dependency chart name should be %s but got %s", req[2].Alias, aliasChart.Name()) + } + } else if aliasChart.Name() != req[2].Name { + t.Fatalf("dependency chart name should be %s but got %s", req[2].Name, aliasChart.Name()) + } + + req[2].Name = "dummy-name" + if aliasChart := getAliasDependency(c.Dependencies(), req[2]); aliasChart != nil { + t.Fatalf("expected no chart but got %s", aliasChart.Name()) + } + +} + +func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-no-requirements-yaml") + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected no changes in dependencies") + } +} + +func TestDependentChartWithSubChartsHelmignore(t *testing.T) { + // FIXME what does this test? + loadChart(t, "testdata/dependent-chart-helmignore") +} + +func TestDependentChartsWithSubChartsSymlink(t *testing.T) { + joonix := filepath.Join("testdata", "joonix") + if err := os.Symlink(filepath.Join("..", "..", "frobnitz"), filepath.Join(joonix, "charts", "frobnitz")); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(filepath.Join(joonix, "charts", "frobnitz")) + c := loadChart(t, joonix) + + if c.Name() != "joonix" { + t.Fatalf("unexpected chart name: %s", c.Name()) + } + if n := len(c.Dependencies()); n != 1 { + t.Fatalf("expected 1 dependency for this chart, but got %d", n) + } +} + +func TestDependentChartsWithSubchartsAllSpecifiedInDependency(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-with-all-in-requirements-yaml") + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected no changes in dependencies") + } + + if len(c.Dependencies()) != len(c.Metadata.Dependencies) { + t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies())) + } +} + +func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-with-mixed-requirements-yaml") + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected no changes in dependencies") + } + + if len(c.Metadata.Dependencies) != 1 { + t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies)) + } +} diff --git a/pkg/chartutil/doc.go b/pkg/chartutil/doc.go index 1190d968da3..8f06bcc9acd 100644 --- a/pkg/chartutil/doc.go +++ b/pkg/chartutil/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ limitations under the License. /*Package chartutil contains tools for working with charts. -Charts are described in the protocol buffer definition (pkg/proto/hapi/charts). -This packe provides utilities for serializing and deserializing charts. +Charts are described in the chart package (pkg/chart). +This package provides utilities for serializing and deserializing charts. A chart can be represented on the file system in one of two ways: @@ -25,20 +25,20 @@ A chart can be represented on the file system in one of two ways: - As a tarred gzipped file containing a directory that then contains a Chart.yaml file. -This package provides utilitites for working with those file formats. +This package provides utilities for working with those file formats. -The preferred way of loading a chart is using 'chartutil.Load`: +The preferred way of loading a chart is using 'loader.Load`: - chart, err := chartutil.Load(filename) + chart, err := loader.Load(filename) This will attempt to discover whether the file at 'filename' is a directory or a chart archive. It will then load accordingly. For accepting raw compressed tar file data from an io.Reader, the -'chartutil.LoadArchive()' will read in the data, uncompress it, and unpack it +'loader.LoadArchive()' will read in the data, uncompress it, and unpack it into a Chart. -When creating charts in memory, use the 'k8s.io/helm/pkg/proto/hapi/chart' +When creating charts in memory, use the 'helm.sh/helm/pkg/chart' package directly. */ -package chartutil // import "k8s.io/helm/pkg/chartutil" +package chartutil // import "helm.sh/helm/v3/pkg/chartutil" diff --git a/pkg/chartutil/errors.go b/pkg/chartutil/errors.go new file mode 100644 index 00000000000..fcdcc27eaa2 --- /dev/null +++ b/pkg/chartutil/errors.go @@ -0,0 +1,35 @@ +/* +Copyright The Helm Authors. + +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 chartutil + +import ( + "fmt" +) + +// ErrNoTable indicates that a chart does not have a matching table. +type ErrNoTable struct { + Key string +} + +func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e.Key) } + +// ErrNoValue indicates that Values does not contain a key with a value +type ErrNoValue struct { + Key string +} + +func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e.Key) } diff --git a/pkg/chartutil/errors_test.go b/pkg/chartutil/errors_test.go new file mode 100644 index 00000000000..3f63e3733f3 --- /dev/null +++ b/pkg/chartutil/errors_test.go @@ -0,0 +1,37 @@ +/* +Copyright The Helm Authors. + +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 chartutil + +import ( + "testing" +) + +func TestErrorNoTableDoesNotPanic(t *testing.T) { + x := "empty" + + y := ErrNoTable{x} + + t.Logf("error is: %s", y) +} + +func TestErrorNoValueDoesNotPanic(t *testing.T) { + x := "empty" + + y := ErrNoValue{x} + + t.Logf("error is: %s", y) +} diff --git a/pkg/chartutil/expand.go b/pkg/chartutil/expand.go index 126e14e8005..6ad09e4173e 100644 --- a/pkg/chartutil/expand.go +++ b/pkg/chartutil/expand.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,59 +17,66 @@ limitations under the License. package chartutil import ( - "archive/tar" - "compress/gzip" "io" + "io/ioutil" "os" "path/filepath" + + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" ) // Expand uncompresses and extracts a chart into the specified directory. func Expand(dir string, r io.Reader) error { - gr, err := gzip.NewReader(r) + files, err := loader.LoadArchiveFiles(r) if err != nil { return err } - defer gr.Close() - tr := tar.NewReader(gr) - for { - header, err := tr.Next() - if err == io.EOF { - break - } else if err != nil { - 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 + // Get the name of the chart + var chartName string + for _, file := range files { + if file.Name == "Chart.yaml" { + ch := &chart.Metadata{} + if err := yaml.Unmarshal(file.Data, ch); err != nil { + return errors.Wrap(err, "cannot load Chart.yaml") } + chartName = ch.Name } + } + if chartName == "" { + return errors.New("chart name not specified") + } - path := filepath.Clean(filepath.Join(dir, header.Name)) - info := header.FileInfo() - if info.IsDir() { - if err = os.MkdirAll(path, info.Mode()); err != nil { - return err - } - continue - } + // Find the base directory + chartdir, err := securejoin.SecureJoin(dir, chartName) + if err != nil { + return err + } - file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) + // Copy all files verbatim. We don't parse these files because parsing can remove + // comments. + for _, file := range files { + outpath, err := securejoin.SecureJoin(chartdir, file.Name) if err != nil { return err } - _, err = io.Copy(file, tr) - if err != nil { - file.Close() + + // Make sure the necessary subdirs get created. + basedir := filepath.Dir(outpath) + if err := os.MkdirAll(basedir, 0755); err != nil { + return err + } + + if err := ioutil.WriteFile(outpath, file.Data, 0644); err != nil { return err } - file.Close() } + return nil } diff --git a/pkg/chartutil/expand_test.go b/pkg/chartutil/expand_test.go new file mode 100644 index 00000000000..9a85e324726 --- /dev/null +++ b/pkg/chartutil/expand_test.go @@ -0,0 +1,133 @@ +/* +Copyright The Helm Authors. + +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 chartutil + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestExpand(t *testing.T) { + dest, err := ioutil.TempDir("", "helm-testing-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dest) + + reader, err := os.Open("testdata/frobnitz-1.2.3.tgz") + if err != nil { + t.Fatal(err) + } + + if err := Expand(dest, reader); err != nil { + t.Fatal(err) + } + + expectedChartPath := filepath.Join(dest, "frobnitz") + fi, err := os.Stat(expectedChartPath) + if err != nil { + t.Fatal(err) + } + if !fi.IsDir() { + t.Fatalf("expected a chart directory at %s", expectedChartPath) + } + + dir, err := os.Open(expectedChartPath) + if err != nil { + t.Fatal(err) + } + + fis, err := dir.Readdir(0) + if err != nil { + t.Fatal(err) + } + + expectLen := 11 + if len(fis) != expectLen { + t.Errorf("Expected %d files, but got %d", expectLen, len(fis)) + } + + for _, fi := range fis { + expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name())) + if err != nil { + t.Fatal(err) + } + // os.Stat can return different values for directories, based on the OS + // for Linux, for example, os.Stat alwaty returns the size of the directory + // (value-4096) regardless of the size of the contents of the directory + mode := expect.Mode() + if !mode.IsDir() { + if fi.Size() != expect.Size() { + t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) + } + } + } +} + +func TestExpandFile(t *testing.T) { + dest, err := ioutil.TempDir("", "helm-testing-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dest) + + if err := ExpandFile(dest, "testdata/frobnitz-1.2.3.tgz"); err != nil { + t.Fatal(err) + } + + expectedChartPath := filepath.Join(dest, "frobnitz") + fi, err := os.Stat(expectedChartPath) + if err != nil { + t.Fatal(err) + } + if !fi.IsDir() { + t.Fatalf("expected a chart directory at %s", expectedChartPath) + } + + dir, err := os.Open(expectedChartPath) + if err != nil { + t.Fatal(err) + } + + fis, err := dir.Readdir(0) + if err != nil { + t.Fatal(err) + } + + expectLen := 11 + if len(fis) != expectLen { + t.Errorf("Expected %d files, but got %d", expectLen, len(fis)) + } + + for _, fi := range fis { + expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name())) + if err != nil { + t.Fatal(err) + } + // os.Stat can return different values for directories, based on the OS + // for Linux, for example, os.Stat alwaty returns the size of the directory + // (value-4096) regardless of the size of the contents of the directory + mode := expect.Mode() + if !mode.IsDir() { + if fi.Size() != expect.Size() { + t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) + } + } + } +} diff --git a/pkg/chartutil/files.go b/pkg/chartutil/files.go deleted file mode 100644 index ced8ce15af5..00000000000 --- a/pkg/chartutil/files.go +++ /dev/null @@ -1,236 +0,0 @@ -/* -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 chartutil - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "path" - "strings" - - "github.com/ghodss/yaml" - - "github.com/BurntSushi/toml" - "github.com/gobwas/glob" - "github.com/golang/protobuf/ptypes/any" -) - -// Files is a map of files in a chart that can be accessed from a template. -type Files map[string][]byte - -// NewFiles creates a new Files from chart files. -// Given an []*any.Any (the format for files in a chart.Chart), extract a map of files. -func NewFiles(from []*any.Any) Files { - files := map[string][]byte{} - if from != nil { - for _, f := range from { - files[f.TypeUrl] = f.Value - } - } - return files -} - -// GetBytes gets a file by path. -// -// The returned data is raw. In a template context, this is identical to calling -// {{index .Files $path}}. -// -// This is intended to be accessed from within a template, so a missed key returns -// an empty []byte. -func (f Files) GetBytes(name string) []byte { - v, ok := f[name] - if !ok { - return []byte{} - } - return v -} - -// Get returns a string representation of the given file. -// -// Fetch the contents of a file as a string. It is designed to be called in a -// template. -// -// {{.Files.Get "foo"}} -func (f Files) Get(name string) string { - return string(f.GetBytes(name)) -} - -// Glob takes a glob pattern and returns another files object only containing -// matched files. -// -// This is designed to be called from a template. -// -// {{ range $name, $content := .Files.Glob("foo/**") }} -// {{ $name }}: | -// {{ .Files.Get($name) | indent 4 }}{{ end }} -func (f Files) Glob(pattern string) Files { - g, err := glob.Compile(pattern, '/') - if err != nil { - g, _ = glob.Compile("**") - } - - nf := NewFiles(nil) - for name, contents := range f { - if g.Match(name) { - nf[name] = contents - } - } - - return nf -} - -// AsConfig turns a Files group and flattens it to a YAML map suitable for -// including in the 'data' section of a Kubernetes ConfigMap definition. -// Duplicate keys will be overwritten, so be aware that your file names -// (regardless of path) should be unique. -// -// This is designed to be called from a template, and will return empty string -// (via ToYaml function) if it cannot be serialized to YAML, or if the Files -// object is nil. -// -// The output will not be indented, so you will want to pipe this to the -// 'indent' template function. -// -// data: -// {{ .Files.Glob("config/**").AsConfig() | indent 4 }} -func (f Files) AsConfig() string { - if f == nil { - return "" - } - - m := map[string]string{} - - // Explicitly convert to strings, and file names - for k, v := range f { - m[path.Base(k)] = string(v) - } - - return ToYaml(m) -} - -// AsSecrets returns the base64-encoded value of a Files object suitable for -// including in the 'data' section of a Kubernetes Secret definition. -// Duplicate keys will be overwritten, so be aware that your file names -// (regardless of path) should be unique. -// -// This is designed to be called from a template, and will return empty string -// (via ToYaml function) if it cannot be serialized to YAML, or if the Files -// object is nil. -// -// The output will not be indented, so you will want to pipe this to the -// 'indent' template function. -// -// data: -// {{ .Files.Glob("secrets/*").AsSecrets() }} -func (f Files) AsSecrets() string { - if f == nil { - return "" - } - - m := map[string]string{} - - for k, v := range f { - m[path.Base(k)] = base64.StdEncoding.EncodeToString(v) - } - - return ToYaml(m) -} - -// Lines returns each line of a named file (split by "\n") as a slice, so it can -// be ranged over in your templates. -// -// This is designed to be called from a template. -// -// {{ range .Files.Lines "foo/bar.html" }} -// {{ . }}{{ end }} -func (f Files) Lines(path string) []string { - if f == nil || f[path] == nil { - return []string{} - } - - return strings.Split(string(f[path]), "\n") -} - -// ToYaml takes an interface, marshals it to yaml, and returns a string. It will -// always return a string, even on marshal error (empty string). -// -// This is designed to be called from a template. -func ToYaml(v interface{}) string { - data, err := yaml.Marshal(v) - if err != nil { - // Swallow errors inside of a template. - return "" - } - return strings.TrimSuffix(string(data), "\n") -} - -// FromYaml converts a YAML document into a map[string]interface{}. -// -// This is not a general-purpose YAML parser, and will not parse all valid -// YAML documents. Additionally, because its intended use is within templates -// it tolerates errors. It will insert the returned error message string into -// m["Error"] in the returned map. -func FromYaml(str string) map[string]interface{} { - m := map[string]interface{}{} - - if err := yaml.Unmarshal([]byte(str), &m); err != nil { - m["Error"] = err.Error() - } - return m -} - -// ToToml takes an interface, marshals it to toml, and returns a string. It will -// always return a string, even on marshal error (empty string). -// -// This is designed to be called from a template. -func ToToml(v interface{}) string { - b := bytes.NewBuffer(nil) - e := toml.NewEncoder(b) - err := e.Encode(v) - if err != nil { - return err.Error() - } - return b.String() -} - -// ToJson takes an interface, marshals it to json, and returns a string. It will -// always return a string, even on marshal error (empty string). -// -// This is designed to be called from a template. -func ToJson(v interface{}) string { - data, err := json.Marshal(v) - if err != nil { - // Swallow errors inside of a template. - return "" - } - return string(data) -} - -// FromJson converts a JSON document into a map[string]interface{}. -// -// This is not a general-purpose JSON parser, and will not parse all valid -// JSON documents. Additionally, because its intended use is within templates -// it tolerates errors. It will insert the returned error message string into -// m["Error"] in the returned map. -func FromJson(str string) map[string]interface{} { - m := map[string]interface{}{} - - if err := json.Unmarshal([]byte(str), &m); err != nil { - m["Error"] = err.Error() - } - return m -} diff --git a/pkg/chartutil/files_test.go b/pkg/chartutil/files_test.go deleted file mode 100644 index 5cec358839b..00000000000 --- a/pkg/chartutil/files_test.go +++ /dev/null @@ -1,217 +0,0 @@ -/* -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 chartutil - -import ( - "testing" - - "github.com/golang/protobuf/ptypes/any" - "github.com/stretchr/testify/assert" -) - -var cases = []struct { - path, data string -}{ - {"ship/captain.txt", "The Captain"}, - {"ship/stowaway.txt", "Legatt"}, - {"story/name.txt", "The Secret Sharer"}, - {"story/author.txt", "Joseph Conrad"}, - {"multiline/test.txt", "bar\nfoo"}, -} - -func getTestFiles() []*any.Any { - a := []*any.Any{} - for _, c := range cases { - a = append(a, &any.Any{TypeUrl: c.path, Value: []byte(c.data)}) - } - return a -} - -func TestNewFiles(t *testing.T) { - files := NewFiles(getTestFiles()) - if len(files) != len(cases) { - t.Errorf("Expected len() = %d, got %d", len(cases), len(files)) - } - - for i, f := range cases { - if got := string(files.GetBytes(f.path)); got != f.data { - t.Errorf("%d: expected %q, got %q", i, f.data, got) - } - if got := files.Get(f.path); got != f.data { - t.Errorf("%d: expected %q, got %q", i, f.data, got) - } - } -} - -func TestFileGlob(t *testing.T) { - as := assert.New(t) - - f := NewFiles(getTestFiles()) - - matched := f.Glob("story/**") - - as.Len(matched, 2, "Should be two files in glob story/**") - as.Equal("Joseph Conrad", matched.Get("story/author.txt")) -} - -func TestToConfig(t *testing.T) { - as := assert.New(t) - - f := NewFiles(getTestFiles()) - out := f.Glob("**/captain.txt").AsConfig() - as.Equal("captain.txt: The Captain", out) - - out = f.Glob("ship/**").AsConfig() - as.Equal("captain.txt: The Captain\nstowaway.txt: Legatt", out) -} - -func TestToSecret(t *testing.T) { - as := assert.New(t) - - f := NewFiles(getTestFiles()) - - out := f.Glob("ship/**").AsSecrets() - as.Equal("captain.txt: VGhlIENhcHRhaW4=\nstowaway.txt: TGVnYXR0", out) -} - -func TestLines(t *testing.T) { - as := assert.New(t) - - f := NewFiles(getTestFiles()) - - out := f.Lines("multiline/test.txt") - as.Len(out, 2) - - as.Equal("bar", out[0]) -} - -func TestToYaml(t *testing.T) { - expect := "foo: bar" - v := struct { - Foo string `json:"foo"` - }{ - Foo: "bar", - } - - if got := ToYaml(v); got != expect { - t.Errorf("Expected %q, got %q", expect, got) - } -} - -func TestToToml(t *testing.T) { - expect := "foo = \"bar\"\n" - v := struct { - Foo string `toml:"foo"` - }{ - Foo: "bar", - } - - if got := ToToml(v); got != expect { - t.Errorf("Expected %q, got %q", expect, got) - } - - // Regression for https://github.com/kubernetes/helm/issues/2271 - dict := map[string]map[string]string{ - "mast": { - "sail": "white", - }, - } - got := ToToml(dict) - expect = "[mast]\n sail = \"white\"\n" - if got != expect { - t.Errorf("Expected:\n%s\nGot\n%s\n", expect, got) - } -} - -func TestFromYaml(t *testing.T) { - doc := `hello: world -one: - two: three -` - dict := FromYaml(doc) - if err, ok := dict["Error"]; ok { - t.Fatalf("Parse error: %s", err) - } - - if len(dict) != 2 { - t.Fatal("expected two elements.") - } - - world := dict["hello"] - if world.(string) != "world" { - t.Fatal("Expected the world. Is that too much to ask?") - } - - // This should fail because we don't currently support lists: - doc2 := ` -- one -- two -- three -` - dict = FromYaml(doc2) - if _, ok := dict["Error"]; !ok { - t.Fatal("Expected parser error") - } -} - -func TestToJson(t *testing.T) { - expect := `{"foo":"bar"}` - v := struct { - Foo string `json:"foo"` - }{ - Foo: "bar", - } - - if got := ToJson(v); got != expect { - t.Errorf("Expected %q, got %q", expect, got) - } -} - -func TestFromJson(t *testing.T) { - doc := `{ - "hello": "world", - "one": { - "two": "three" - } -} -` - dict := FromJson(doc) - if err, ok := dict["Error"]; ok { - t.Fatalf("Parse error: %s", err) - } - - if len(dict) != 2 { - t.Fatal("expected two elements.") - } - - world := dict["hello"] - if world.(string) != "world" { - t.Fatal("Expected the world. Is that too much to ask?") - } - - // This should fail because we don't currently support lists: - doc2 := ` -[ - "one", - "two", - "three" -] -` - dict = FromJson(doc2) - if _, ok := dict["Error"]; !ok { - t.Fatal("Expected parser error") - } -} diff --git a/pkg/chartutil/jsonschema.go b/pkg/chartutil/jsonschema.go new file mode 100644 index 00000000000..753dc98c1eb --- /dev/null +++ b/pkg/chartutil/jsonschema.go @@ -0,0 +1,87 @@ +/* +Copyright The Helm Authors. + +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 chartutil + +import ( + "bytes" + "fmt" + "strings" + + "github.com/pkg/errors" + "github.com/xeipuuv/gojsonschema" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/chart" +) + +// ValidateAgainstSchema checks that values does not violate the structure laid out in schema +func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error { + var sb strings.Builder + if chrt.Schema != nil { + err := ValidateAgainstSingleSchema(values, chrt.Schema) + if err != nil { + sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) + sb.WriteString(err.Error()) + } + } + + // For each dependency, recursively call this function with the coalesced values + for _, subchart := range chrt.Dependencies() { + subchartValues := values[subchart.Name()].(map[string]interface{}) + if err := ValidateAgainstSchema(subchart, subchartValues); err != nil { + sb.WriteString(err.Error()) + } + } + + if sb.Len() > 0 { + return errors.New(sb.String()) + } + + return nil +} + +// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema +func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) error { + valuesData, err := yaml.Marshal(values) + if err != nil { + return err + } + valuesJSON, err := yaml.YAMLToJSON(valuesData) + if err != nil { + return err + } + if bytes.Equal(valuesJSON, []byte("null")) { + valuesJSON = []byte("{}") + } + schemaLoader := gojsonschema.NewBytesLoader(schemaJSON) + valuesLoader := gojsonschema.NewBytesLoader(valuesJSON) + + result, err := gojsonschema.Validate(schemaLoader, valuesLoader) + if err != nil { + return err + } + + if !result.Valid() { + var sb strings.Builder + for _, desc := range result.Errors() { + sb.WriteString(fmt.Sprintf("- %s\n", desc)) + } + return errors.New(sb.String()) + } + + return nil +} diff --git a/pkg/chartutil/jsonschema_test.go b/pkg/chartutil/jsonschema_test.go new file mode 100644 index 00000000000..a0acd5a7f29 --- /dev/null +++ b/pkg/chartutil/jsonschema_test.go @@ -0,0 +1,143 @@ +/* +Copyright The Helm Authors. + +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 chartutil + +import ( + "io/ioutil" + "testing" + + "helm.sh/helm/v3/pkg/chart" +) + +func TestValidateAgainstSingleSchema(t *testing.T) { + values, err := ReadValuesFile("./testdata/test-values.yaml") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + schema, err := ioutil.ReadFile("./testdata/test-values.schema.json") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + + if err := ValidateAgainstSingleSchema(values, schema); err != nil { + t.Errorf("Error validating Values against Schema: %s", err) + } +} + +func TestValidateAgainstSingleSchemaNegative(t *testing.T) { + values, err := ReadValuesFile("./testdata/test-values-negative.yaml") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + schema, err := ioutil.ReadFile("./testdata/test-values.schema.json") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + + var errString string + if err := ValidateAgainstSingleSchema(values, schema); err == nil { + t.Fatalf("Expected an error, but got nil") + } else { + errString = err.Error() + } + + expectedErrString := `- (root): employmentInfo is required +- age: Must be greater than or equal to 0 +` + if errString != expectedErrString { + t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) + } +} + +const subchartSchema = `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Values", + "type": "object", + "properties": { + "age": { + "description": "Age", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "age" + ] +} +` + +func TestValidateAgainstSchema(t *testing.T) { + subchartJSON := []byte(subchartSchema) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "subchart", + }, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "chrt", + }, + } + chrt.AddDependency(subchart) + + vals := map[string]interface{}{ + "name": "John", + "subchart": map[string]interface{}{ + "age": 25, + }, + } + + if err := ValidateAgainstSchema(chrt, vals); err != nil { + t.Errorf("Error validating Values against Schema: %s", err) + } +} + +func TestValidateAgainstSchemaNegative(t *testing.T) { + subchartJSON := []byte(subchartSchema) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "subchart", + }, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "chrt", + }, + } + chrt.AddDependency(subchart) + + vals := map[string]interface{}{ + "name": "John", + "subchart": map[string]interface{}{}, + } + + var errString string + if err := ValidateAgainstSchema(chrt, vals); err == nil { + t.Fatalf("Expected an error, but got nil") + } else { + errString = err.Error() + } + + expectedErrString := `subchart: +- (root): age is required +` + if errString != expectedErrString { + t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) + } +} diff --git a/pkg/chartutil/load.go b/pkg/chartutil/load.go deleted file mode 100644 index c5246b8d76a..00000000000 --- a/pkg/chartutil/load.go +++ /dev/null @@ -1,288 +0,0 @@ -/* -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 chartutil - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "github.com/golang/protobuf/ptypes/any" - - "k8s.io/helm/pkg/ignore" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/sympath" -) - -// Load takes a string name, tries to resolve it to a file or directory, and then loads it. -// -// This is the preferred way to load a chart. It will discover the chart encoding -// and hand off to the appropriate chart reader. -// -// If a .helmignore file is present, the directory loader will skip loading any files -// matching it. But .helmignore is not evaluated when reading out of an archive. -func Load(name string) (*chart.Chart, error) { - fi, err := os.Stat(name) - if err != nil { - return nil, err - } - if fi.IsDir() { - if validChart, err := IsChartDir(name); !validChart { - return nil, err - } - return LoadDir(name) - } - return LoadFile(name) -} - -// BufferedFile represents an archive file buffered for later processing. -type BufferedFile struct { - Name string - Data []byte -} - -// LoadArchive loads from a reader containing a compressed tar archive. -func LoadArchive(in io.Reader) (*chart.Chart, error) { - unzipped, err := gzip.NewReader(in) - if err != nil { - return &chart.Chart{}, err - } - defer unzipped.Close() - - files := []*BufferedFile{} - tr := tar.NewReader(unzipped) - for { - b := bytes.NewBuffer(nil) - hd, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return &chart.Chart{}, err - } - - if hd.FileInfo().IsDir() { - // Use this instead of hd.Typeflag because we don't have to do any - // inference chasing. - continue - } - - // Archive could contain \ if generated on Windows - delimiter := "/" - if strings.ContainsRune(hd.Name, '\\') { - delimiter = "\\" - } - - parts := strings.Split(hd.Name, delimiter) - n := strings.Join(parts[1:], delimiter) - - // Normalize the path to the / delimiter - n = strings.Replace(n, delimiter, "/", -1) - - if parts[0] == "Chart.yaml" { - return nil, errors.New("chart yaml not in base directory") - } - - if _, err := io.Copy(b, tr); err != nil { - return &chart.Chart{}, err - } - - files = append(files, &BufferedFile{Name: n, Data: b.Bytes()}) - b.Reset() - } - - if len(files) == 0 { - return nil, errors.New("no files in chart archive") - } - - return LoadFiles(files) -} - -// LoadFiles loads from in-memory files. -func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { - c := &chart.Chart{} - subcharts := map[string][]*BufferedFile{} - - for _, f := range files { - if f.Name == "Chart.yaml" { - m, err := UnmarshalChartfile(f.Data) - if err != nil { - return c, err - } - c.Metadata = m - } else if f.Name == "values.toml" { - return c, errors.New("values.toml is illegal as of 2.0.0-alpha.2") - } else if f.Name == "values.yaml" { - c.Values = &chart.Config{Raw: string(f.Data)} - } else if strings.HasPrefix(f.Name, "templates/") { - c.Templates = append(c.Templates, &chart.Template{Name: f.Name, Data: f.Data}) - } else if strings.HasPrefix(f.Name, "charts/") { - if filepath.Ext(f.Name) == ".prov" { - c.Files = append(c.Files, &any.Any{TypeUrl: f.Name, Value: f.Data}) - continue - } - cname := strings.TrimPrefix(f.Name, "charts/") - if strings.IndexAny(cname, "._") == 0 { - // Ignore charts/ that start with . or _. - continue - } - parts := strings.SplitN(cname, "/", 2) - scname := parts[0] - subcharts[scname] = append(subcharts[scname], &BufferedFile{Name: cname, Data: f.Data}) - } else { - c.Files = append(c.Files, &any.Any{TypeUrl: f.Name, Value: f.Data}) - } - } - - // Ensure that we got a Chart.yaml file - if c.Metadata == nil { - return c, errors.New("chart metadata (Chart.yaml) missing") - } - if c.Metadata.Name == "" { - return c, errors.New("invalid chart (Chart.yaml): name must not be empty") - } - - for n, files := range subcharts { - var sc *chart.Chart - var err error - if strings.IndexAny(n, "_.") == 0 { - continue - } else if filepath.Ext(n) == ".tgz" { - file := files[0] - if file.Name != n { - return c, fmt.Errorf("error unpacking tar in %s: expected %s, got %s", c.Metadata.Name, n, file.Name) - } - // Untar the chart and add to c.Dependencies - b := bytes.NewBuffer(file.Data) - sc, err = LoadArchive(b) - } else { - // We have to trim the prefix off of every file, and ignore any file - // that is in charts/, but isn't actually a chart. - buff := make([]*BufferedFile, 0, len(files)) - for _, f := range files { - parts := strings.SplitN(f.Name, "/", 2) - if len(parts) < 2 { - continue - } - f.Name = parts[1] - buff = append(buff, f) - } - sc, err = LoadFiles(buff) - } - - if err != nil { - return c, fmt.Errorf("error unpacking %s in %s: %s", n, c.Metadata.Name, err) - } - - c.Dependencies = append(c.Dependencies, sc) - } - - return c, nil -} - -// LoadFile loads from an archive file. -func LoadFile(name string) (*chart.Chart, error) { - if fi, err := os.Stat(name); err != nil { - return nil, err - } else if fi.IsDir() { - return nil, errors.New("cannot load a directory") - } - - raw, err := os.Open(name) - if err != nil { - return nil, err - } - defer raw.Close() - - return LoadArchive(raw) -} - -// LoadDir loads from a directory. -// -// This loads charts only from directories. -func LoadDir(dir string) (*chart.Chart, error) { - topdir, err := filepath.Abs(dir) - if err != nil { - return nil, err - } - - // Just used for errors. - c := &chart.Chart{} - - rules := ignore.Empty() - ifile := filepath.Join(topdir, ignore.HelmIgnore) - if _, err := os.Stat(ifile); err == nil { - r, err := ignore.ParseFile(ifile) - if err != nil { - return c, err - } - rules = r - } - rules.AddDefaults() - - files := []*BufferedFile{} - topdir += string(filepath.Separator) - - walk := func(name string, fi os.FileInfo, err error) error { - n := strings.TrimPrefix(name, topdir) - if n == "" { - // No need to process top level. Avoid bug with helmignore .* matching - // empty names. See issue 1779. - return nil - } - - // Normalize to / since it will also work on Windows - n = filepath.ToSlash(n) - - if err != nil { - return err - } - if fi.IsDir() { - // Directory-based ignore rules should involve skipping the entire - // contents of that directory. - if rules.Ignore(n, fi) { - return filepath.SkipDir - } - return nil - } - - // If a .helmignore file matches, skip this file. - if rules.Ignore(n, fi) { - return nil - } - - data, err := ioutil.ReadFile(name) - if err != nil { - return fmt.Errorf("error reading %s: %s", n, err) - } - - files = append(files, &BufferedFile{Name: n, Data: data}) - return nil - } - if err = sympath.Walk(topdir, walk); err != nil { - return c, err - } - - return LoadFiles(files) -} diff --git a/pkg/chartutil/load_test.go b/pkg/chartutil/load_test.go deleted file mode 100644 index 45450048999..00000000000 --- a/pkg/chartutil/load_test.go +++ /dev/null @@ -1,253 +0,0 @@ -/* -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 chartutil - -import ( - "path" - "testing" - - "k8s.io/helm/pkg/proto/hapi/chart" -) - -func TestLoadDir(t *testing.T) { - c, err := Load("testdata/frobnitz") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyFrobnitz(t, c) - verifyChart(t, c) - verifyRequirements(t, c) -} - -func TestLoadFile(t *testing.T) { - c, err := Load("testdata/frobnitz-1.2.3.tgz") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyFrobnitz(t, c) - verifyChart(t, c) - verifyRequirements(t, c) -} - -func TestLoadFiles(t *testing.T) { - goodFiles := []*BufferedFile{ - { - Name: ChartfileName, - Data: []byte(`apiVersion: v1 -name: frobnitz -description: This is a frobnitz. -version: "1.2.3" -keywords: - - frobnitz - - sprocket - - dodad -maintainers: - - name: The Helm Team - email: helm@example.com - - name: Someone Else - email: nobody@example.com -sources: - - https://example.com/foo/bar -home: http://example.com -icon: https://example.com/64x64.png -`), - }, - { - Name: ValuesfileName, - Data: []byte(defaultValues), - }, - { - Name: path.Join("templates", DeploymentName), - Data: []byte(defaultDeployment), - }, - { - Name: path.Join("templates", ServiceName), - Data: []byte(defaultService), - }, - } - - c, err := LoadFiles(goodFiles) - if err != nil { - t.Errorf("Expected good files to be loaded, got %v", err) - } - - if c.Metadata.Name != "frobnitz" { - t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Metadata.Name) - } - - if c.Values.Raw != defaultValues { - t.Error("Expected chart values to be populated with default values") - } - - if len(c.Templates) != 2 { - t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) - } - - c, err = LoadFiles([]*BufferedFile{}) - if err == nil { - t.Fatal("Expected err to be non-nil") - } - if err.Error() != "chart metadata (Chart.yaml) missing" { - t.Errorf("Expected chart metadata missing error, got '%s'", err.Error()) - } - - // legacy check - c, err = LoadFiles([]*BufferedFile{ - { - Name: "values.toml", - Data: []byte{}, - }, - }) - if err == nil { - t.Fatal("Expected err to be non-nil") - } - if err.Error() != "values.toml is illegal as of 2.0.0-alpha.2" { - t.Errorf("Expected values.toml to be illegal, got '%s'", err.Error()) - } -} - -// Packaging the chart on a Windows machine will produce an -// archive that has \\ as delimiters. Test that we support these archives -func TestLoadFileBackslash(t *testing.T) { - c, err := Load("testdata/frobnitz_backslash-1.2.3.tgz") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyChartFileAndTemplate(t, c, "frobnitz_backslash") - verifyChart(t, c) - verifyRequirements(t, c) -} - -func verifyChart(t *testing.T, c *chart.Chart) { - if c.Metadata.Name == "" { - t.Fatalf("No chart metadata found on %v", c) - } - t.Logf("Verifying chart %s", c.Metadata.Name) - if len(c.Templates) != 1 { - t.Errorf("Expected 1 template, got %d", len(c.Templates)) - } - - numfiles := 8 - if len(c.Files) != numfiles { - t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files)) - for _, n := range c.Files { - t.Logf("\t%s", n.TypeUrl) - } - } - - if len(c.Dependencies) != 2 { - t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies), c.Dependencies) - for _, d := range c.Dependencies { - t.Logf("\tSubchart: %s\n", d.Metadata.Name) - } - } - - expect := map[string]map[string]string{ - "alpine": { - "version": "0.1.0", - }, - "mariner": { - "version": "4.3.2", - }, - } - - for _, dep := range c.Dependencies { - if dep.Metadata == nil { - t.Fatalf("expected metadata on dependency: %v", dep) - } - exp, ok := expect[dep.Metadata.Name] - if !ok { - t.Fatalf("Unknown dependency %s", dep.Metadata.Name) - } - if exp["version"] != dep.Metadata.Version { - t.Errorf("Expected %s version %s, got %s", dep.Metadata.Name, exp["version"], dep.Metadata.Version) - } - } - -} - -func verifyRequirements(t *testing.T, c *chart.Chart) { - r, err := LoadRequirements(c) - if err != nil { - t.Fatal(err) - } - if len(r.Dependencies) != 2 { - t.Errorf("Expected 2 requirements, got %d", len(r.Dependencies)) - } - tests := []*Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, - {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, - } - for i, tt := range tests { - d := r.Dependencies[i] - if d.Name != tt.Name { - t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) - } - if d.Version != tt.Version { - t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) - } - if d.Repository != tt.Repository { - t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) - } - } -} -func verifyRequirementsLock(t *testing.T, c *chart.Chart) { - r, err := LoadRequirementsLock(c) - if err != nil { - t.Fatal(err) - } - if len(r.Dependencies) != 2 { - t.Errorf("Expected 2 requirements, got %d", len(r.Dependencies)) - } - tests := []*Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, - {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, - } - for i, tt := range tests { - d := r.Dependencies[i] - if d.Name != tt.Name { - t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) - } - if d.Version != tt.Version { - t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) - } - if d.Repository != tt.Repository { - t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) - } - } -} - -func verifyFrobnitz(t *testing.T, c *chart.Chart) { - verifyChartFileAndTemplate(t, c, "frobnitz") -} - -func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { - - verifyChartfile(t, c.Metadata, name) - - if len(c.Templates) != 1 { - t.Fatalf("Expected 1 template, got %d", len(c.Templates)) - } - - if c.Templates[0].Name != "templates/template.tpl" { - t.Errorf("Unexpected template: %s", c.Templates[0].Name) - } - - if len(c.Templates[0].Data) == 0 { - t.Error("No template data.") - } -} diff --git a/pkg/chartutil/requirements.go b/pkg/chartutil/requirements.go deleted file mode 100644 index ce761a6fc65..00000000000 --- a/pkg/chartutil/requirements.go +++ /dev/null @@ -1,456 +0,0 @@ -/* -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 chartutil - -import ( - "errors" - "log" - "strings" - "time" - - "github.com/ghodss/yaml" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/version" -) - -const ( - requirementsName = "requirements.yaml" - lockfileName = "requirements.lock" -) - -var ( - // ErrRequirementsNotFound indicates that a requirements.yaml is not found. - ErrRequirementsNotFound = errors.New(requirementsName + " not found") - // ErrLockfileNotFound indicates that a requirements.lock is not found. - ErrLockfileNotFound = errors.New(lockfileName + " not found") -) - -// Dependency describes a chart upon which another chart depends. -// -// Dependencies can be used to express developer intent, or to capture the state -// of a chart. -type Dependency struct { - // Name is the name of the dependency. - // - // This must mach the name in the dependency's Chart.yaml. - Name string `json:"name"` - // Version is the version (range) of this chart. - // - // A lock file will always produce a single version, while a dependency - // may contain a semantic version range. - Version string `json:"version,omitempty"` - // The URL to the repository. - // - // Appending `index.yaml` to this string should result in a URL that can be - // used to fetch the repository index. - Repository string `json:"repository"` - // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled ) - Condition string `json:"condition,omitempty"` - // Tags can be used to group charts for enabling/disabling together - Tags []string `json:"tags,omitempty"` - // Enabled bool determines if chart should be loaded - Enabled bool `json:"enabled,omitempty"` - // ImportValues holds the mapping of source values to parent key to be imported. Each item can be a - // string or pair of child/parent sublist items. - ImportValues []interface{} `json:"import-values,omitempty"` - // Alias usable alias to be used for the chart - Alias string `json:"alias,omitempty"` -} - -// ErrNoRequirementsFile to detect error condition -type ErrNoRequirementsFile error - -// Requirements is a list of requirements for a chart. -// -// Requirements are charts upon which this chart depends. This expresses -// developer intent. -type Requirements struct { - Dependencies []*Dependency `json:"dependencies"` -} - -// RequirementsLock is a lock file for requirements. -// -// It represents the state that the dependencies should be in. -type RequirementsLock struct { - // Genderated is the date the lock file was last generated. - Generated time.Time `json:"generated"` - // Digest is a hash of the requirements file used to generate it. - Digest string `json:"digest"` - // Dependencies is the list of dependencies that this lock file has locked. - Dependencies []*Dependency `json:"dependencies"` -} - -// LoadRequirements loads a requirements file from an in-memory chart. -func LoadRequirements(c *chart.Chart) (*Requirements, error) { - var data []byte - for _, f := range c.Files { - if f.TypeUrl == requirementsName { - data = f.Value - } - } - if len(data) == 0 { - return nil, ErrRequirementsNotFound - } - r := &Requirements{} - return r, yaml.Unmarshal(data, r) -} - -// LoadRequirementsLock loads a requirements lock file. -func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) { - var data []byte - for _, f := range c.Files { - if f.TypeUrl == lockfileName { - data = f.Value - } - } - if len(data) == 0 { - return nil, ErrLockfileNotFound - } - r := &RequirementsLock{} - return r, yaml.Unmarshal(data, r) -} - -// ProcessRequirementsConditions disables charts based on condition path value in values -func ProcessRequirementsConditions(reqs *Requirements, cvals Values) { - var cond string - var conds []string - if reqs == nil || len(reqs.Dependencies) == 0 { - return - } - for _, r := range reqs.Dependencies { - var hasTrue, hasFalse bool - cond = string(r.Condition) - // check for list - if len(cond) > 0 { - if strings.Contains(cond, ",") { - conds = strings.Split(strings.TrimSpace(cond), ",") - } else { - conds = []string{strings.TrimSpace(cond)} - } - for _, c := range conds { - if len(c) > 0 { - // retrieve value - vv, err := cvals.PathValue(c) - if err == nil { - // if not bool, warn - if bv, ok := vv.(bool); ok { - if bv { - hasTrue = true - } else { - hasFalse = true - } - } else { - log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name) - } - } else if _, ok := err.(ErrNoValue); !ok { - // this is a real error - log.Printf("Warning: PathValue returned error %v", err) - - } - if vv != nil { - // got first value, break loop - break - } - } - } - if !hasTrue && hasFalse { - r.Enabled = false - } else if hasTrue { - r.Enabled = true - - } - } - - } - -} - -// ProcessRequirementsTags disables charts based on tags in values -func ProcessRequirementsTags(reqs *Requirements, cvals Values) { - vt, err := cvals.Table("tags") - if err != nil { - return - - } - if reqs == nil || len(reqs.Dependencies) == 0 { - return - } - for _, r := range reqs.Dependencies { - if len(r.Tags) > 0 { - tags := r.Tags - - var hasTrue, hasFalse bool - for _, k := range tags { - if b, ok := vt[k]; ok { - // if not bool, warn - if bv, ok := b.(bool); ok { - if bv { - hasTrue = true - } else { - hasFalse = true - } - } else { - log.Printf("Warning: Tag '%s' for chart %s returned non-bool value", k, r.Name) - } - } - } - if !hasTrue && hasFalse { - r.Enabled = false - } else if hasTrue || !hasTrue && !hasFalse { - r.Enabled = true - - } - - } - } - -} - -func getAliasDependency(charts []*chart.Chart, aliasChart *Dependency) *chart.Chart { - var chartFound chart.Chart - for _, existingChart := range charts { - if existingChart == nil { - continue - } - if existingChart.Metadata == nil { - continue - } - if existingChart.Metadata.Name != aliasChart.Name { - continue - } - if !version.IsCompatibleRange(aliasChart.Version, existingChart.Metadata.Version) { - continue - } - chartFound = *existingChart - newMetadata := *existingChart.Metadata - if aliasChart.Alias != "" { - newMetadata.Name = aliasChart.Alias - } - chartFound.Metadata = &newMetadata - return &chartFound - } - return nil -} - -// ProcessRequirementsEnabled removes disabled charts from dependencies -func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error { - reqs, err := LoadRequirements(c) - if err != nil { - // if not just missing requirements file, return error - if nerr, ok := err.(ErrNoRequirementsFile); !ok { - return nerr - } - - // no requirements to process - return nil - } - - var chartDependencies []*chart.Chart - // If any dependency is not a part of requirements.yaml - // then this should be added to chartDependencies. - // However, if the dependency is already specified in requirements.yaml - // we should not add it, as it would be anyways processed from requirements.yaml - - for _, existingDependency := range c.Dependencies { - var dependencyFound bool - for _, req := range reqs.Dependencies { - if existingDependency.Metadata.Name == req.Name && version.IsCompatibleRange(req.Version, existingDependency.Metadata.Version) { - dependencyFound = true - break - } - } - if !dependencyFound { - chartDependencies = append(chartDependencies, existingDependency) - } - } - - for _, req := range reqs.Dependencies { - if chartDependency := getAliasDependency(c.Dependencies, req); chartDependency != nil { - chartDependencies = append(chartDependencies, chartDependency) - } - if req.Alias != "" { - req.Name = req.Alias - } - } - c.Dependencies = chartDependencies - - // set all to true - for _, lr := range reqs.Dependencies { - lr.Enabled = true - } - cvals, err := CoalesceValues(c, v) - if err != nil { - return err - } - // convert our values back into config - yvals, err := cvals.YAML() - if err != nil { - return err - } - cc := chart.Config{Raw: yvals} - // flag dependencies as enabled/disabled - ProcessRequirementsTags(reqs, cvals) - ProcessRequirementsConditions(reqs, cvals) - // make a map of charts to remove - rm := map[string]bool{} - for _, r := range reqs.Dependencies { - if !r.Enabled { - // remove disabled chart - rm[r.Name] = true - } - } - // don't keep disabled charts in new slice - cd := []*chart.Chart{} - copy(cd, c.Dependencies[:0]) - for _, n := range c.Dependencies { - if _, ok := rm[n.Metadata.Name]; !ok { - cd = append(cd, n) - } - - } - // recursively call self to process sub dependencies - for _, t := range cd { - err := ProcessRequirementsEnabled(t, &cc) - // if its not just missing requirements file, return error - if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil { - return nerr - } - } - c.Dependencies = cd - - return nil -} - -// pathToMap creates a nested map given a YAML path in dot notation. -func pathToMap(path string, data map[string]interface{}) map[string]interface{} { - if path == "." { - return data - } - ap := strings.Split(path, ".") - if len(ap) == 0 { - return nil - } - n := []map[string]interface{}{} - // created nested map for each key, adding to slice - for _, v := range ap { - nm := make(map[string]interface{}) - nm[v] = make(map[string]interface{}) - n = append(n, nm) - } - // find the last key (map) and set our data - for i, d := range n { - for k := range d { - z := i + 1 - if z == len(n) { - n[i][k] = data - break - } - n[i][k] = n[z] - } - } - - return n[0] -} - -// getParents returns a slice of parent charts in reverse order. -func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart { - if len(out) == 0 { - out = []*chart.Chart{c} - } - for _, ch := range c.Dependencies { - if len(ch.Dependencies) > 0 { - out = append(out, ch) - out = getParents(ch, out) - } - } - - return out -} - -// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. -func processImportValues(c *chart.Chart) error { - reqs, err := LoadRequirements(c) - if err != nil { - return err - } - // combine chart values and empty config to get Values - cvals, err := CoalesceValues(c, &chart.Config{}) - if err != nil { - return err - } - b := make(map[string]interface{}, 0) - // import values from each dependency if specified in import-values - for _, r := range reqs.Dependencies { - if len(r.ImportValues) > 0 { - var outiv []interface{} - for _, riv := range r.ImportValues { - switch iv := riv.(type) { - case map[string]interface{}: - nm := map[string]string{ - "child": iv["child"].(string), - "parent": iv["parent"].(string), - } - outiv = append(outiv, nm) - s := r.Name + "." + nm["child"] - // get child table - vv, err := cvals.Table(s) - if err != nil { - log.Printf("Warning: ImportValues missing table: %v", err) - continue - } - // create value map from child to be merged into parent - vm := pathToMap(nm["parent"], vv.AsMap()) - b = coalesceTables(cvals, vm) - case string: - nm := map[string]string{ - "child": "exports." + iv, - "parent": ".", - } - outiv = append(outiv, nm) - s := r.Name + "." + nm["child"] - vm, err := cvals.Table(s) - if err != nil { - log.Printf("Warning: ImportValues missing table: %v", err) - continue - } - b = coalesceTables(b, vm.AsMap()) - } - } - // set our formatted import values - r.ImportValues = outiv - } - } - b = coalesceTables(b, cvals) - y, err := yaml.Marshal(b) - if err != nil { - return err - } - - // set the new values - c.Values = &chart.Config{Raw: string(y)} - - return nil -} - -// ProcessRequirementsImportValues imports specified chart values from child to parent. -func ProcessRequirementsImportValues(c *chart.Chart) error { - pc := getParents(c, nil) - for i := len(pc) - 1; i >= 0; i-- { - processImportValues(pc[i]) - } - - return nil -} diff --git a/pkg/chartutil/requirements_test.go b/pkg/chartutil/requirements_test.go deleted file mode 100644 index 24388f86c61..00000000000 --- a/pkg/chartutil/requirements_test.go +++ /dev/null @@ -1,499 +0,0 @@ -/* -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 chartutil - -import ( - "sort" - "testing" - - "strconv" - - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/version" -) - -func TestLoadRequirements(t *testing.T) { - c, err := Load("testdata/frobnitz") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyRequirements(t, c) -} - -func TestLoadRequirementsLock(t *testing.T) { - c, err := Load("testdata/frobnitz") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyRequirementsLock(t, c) -} -func TestRequirementsTagsNonValue(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // tags with no effect - v := &chart.Config{Raw: "tags:\n nothinguseful: false\n\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart", "subchart1", "subcharta", "subchartb"} - - verifyRequirementsEnabled(t, c, v, e) -} -func TestRequirementsTagsDisabledL1(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // tags disabling a group - v := &chart.Config{Raw: "tags:\n front-end: false\n\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart"} - - verifyRequirementsEnabled(t, c, v, e) -} -func TestRequirementsTagsEnabledL1(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // tags disabling a group and enabling a different group - v := &chart.Config{Raw: "tags:\n front-end: false\n\n back-end: true\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart", "subchart2", "subchartb", "subchartc"} - - verifyRequirementsEnabled(t, c, v, e) -} - -func TestRequirementsTagsDisabledL2(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // tags disabling only children, children still enabled since tag front-end=true in values.yaml - v := &chart.Config{Raw: "tags:\n subcharta: false\n\n subchartb: false\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart", "subchart1", "subcharta", "subchartb"} - - verifyRequirementsEnabled(t, c, v, e) -} -func TestRequirementsTagsDisabledL1Mixed(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // tags disabling all parents/children with additional tag re-enabling a parent - v := &chart.Config{Raw: "tags:\n front-end: false\n\n subchart1: true\n\n back-end: false\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart", "subchart1"} - - verifyRequirementsEnabled(t, c, v, e) -} -func TestRequirementsConditionsNonValue(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // tags with no effect - v := &chart.Config{Raw: "subchart1:\n nothinguseful: false\n\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart", "subchart1", "subcharta", "subchartb"} - - verifyRequirementsEnabled(t, c, v, e) -} -func TestRequirementsConditionsEnabledL1Both(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // conditions enabling the parent charts, but back-end (b, c) is still disabled via values.yaml - v := &chart.Config{Raw: "subchart1:\n enabled: true\nsubchart2:\n enabled: true\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart", "subchart1", "subchart2", "subcharta", "subchartb"} - - verifyRequirementsEnabled(t, c, v, e) -} -func TestRequirementsConditionsDisabledL1Both(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // conditions disabling the parent charts, effectively disabling children - v := &chart.Config{Raw: "subchart1:\n enabled: false\nsubchart2:\n enabled: false\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart"} - - verifyRequirementsEnabled(t, c, v, e) -} - -func TestRequirementsConditionsSecond(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // conditions a child using the second condition path of child's condition - v := &chart.Config{Raw: "subchart1:\n subcharta:\n enabled: false\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart", "subchart1", "subchartb"} - - verifyRequirementsEnabled(t, c, v, e) -} -func TestRequirementsCombinedDisabledL2(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // tags enabling a parent/child group with condition disabling one child - v := &chart.Config{Raw: "subchartc:\n enabled: false\ntags:\n back-end: true\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart", "subchart1", "subchart2", "subcharta", "subchartb", "subchartb"} - - verifyRequirementsEnabled(t, c, v, e) -} -func TestRequirementsCombinedDisabledL1(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - // tags will not enable a child if parent is explicitly disabled with condition - v := &chart.Config{Raw: "subchart1:\n enabled: false\ntags:\n front-end: true\n"} - // expected charts including duplicates in alphanumeric order - e := []string{"parentchart"} - - verifyRequirementsEnabled(t, c, v, e) -} - -func verifyRequirementsEnabled(t *testing.T, c *chart.Chart, v *chart.Config, e []string) { - out := []*chart.Chart{} - err := ProcessRequirementsEnabled(c, v) - if err != nil { - t.Errorf("Error processing enabled requirements %v", err) - } - out = extractCharts(c, out) - // build list of chart names - p := []string{} - for _, r := range out { - p = append(p, r.Metadata.Name) - } - //sort alphanumeric and compare to expectations - sort.Strings(p) - if len(p) != len(e) { - t.Errorf("Error slice lengths do not match got %v, expected %v", len(p), len(e)) - return - } - for i := range p { - if p[i] != e[i] { - t.Errorf("Error slice values do not match got %v, expected %v", p[i], e[i]) - } - } -} - -// extractCharts recursively searches chart dependencies returning all charts found -func extractCharts(c *chart.Chart, out []*chart.Chart) []*chart.Chart { - - if len(c.Metadata.Name) > 0 { - out = append(out, c) - } - for _, d := range c.Dependencies { - out = extractCharts(d, out) - } - return out -} -func TestProcessRequirementsImportValues(t *testing.T) { - c, err := Load("testdata/subpop") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - - v := &chart.Config{Raw: ""} - - e := make(map[string]string) - - e["imported-chart1.SC1bool"] = "true" - e["imported-chart1.SC1float"] = "3.14" - e["imported-chart1.SC1int"] = "100" - e["imported-chart1.SC1string"] = "dollywood" - e["imported-chart1.SC1extra1"] = "11" - e["imported-chart1.SPextra1"] = "helm rocks" - e["imported-chart1.SC1extra1"] = "11" - - e["imported-chartA.SCAbool"] = "false" - e["imported-chartA.SCAfloat"] = "3.1" - e["imported-chartA.SCAint"] = "55" - e["imported-chartA.SCAstring"] = "jabba" - e["imported-chartA.SPextra3"] = "1.337" - e["imported-chartA.SC1extra2"] = "1.337" - e["imported-chartA.SCAnested1.SCAnested2"] = "true" - - e["imported-chartA-B.SCAbool"] = "false" - e["imported-chartA-B.SCAfloat"] = "3.1" - e["imported-chartA-B.SCAint"] = "55" - e["imported-chartA-B.SCAstring"] = "jabba" - - e["imported-chartA-B.SCBbool"] = "true" - e["imported-chartA-B.SCBfloat"] = "7.77" - e["imported-chartA-B.SCBint"] = "33" - e["imported-chartA-B.SCBstring"] = "boba" - e["imported-chartA-B.SPextra5"] = "k8s" - e["imported-chartA-B.SC1extra5"] = "tiller" - - e["overridden-chart1.SC1bool"] = "false" - e["overridden-chart1.SC1float"] = "3.141592" - e["overridden-chart1.SC1int"] = "99" - e["overridden-chart1.SC1string"] = "pollywog" - e["overridden-chart1.SPextra2"] = "42" - - e["overridden-chartA.SCAbool"] = "true" - e["overridden-chartA.SCAfloat"] = "41.3" - e["overridden-chartA.SCAint"] = "808" - e["overridden-chartA.SCAstring"] = "jaberwocky" - e["overridden-chartA.SPextra4"] = "true" - - e["overridden-chartA-B.SCAbool"] = "true" - e["overridden-chartA-B.SCAfloat"] = "41.3" - e["overridden-chartA-B.SCAint"] = "808" - e["overridden-chartA-B.SCAstring"] = "jaberwocky" - e["overridden-chartA-B.SCBbool"] = "false" - e["overridden-chartA-B.SCBfloat"] = "1.99" - e["overridden-chartA-B.SCBint"] = "77" - e["overridden-chartA-B.SCBstring"] = "jango" - e["overridden-chartA-B.SPextra6"] = "111" - e["overridden-chartA-B.SCAextra1"] = "23" - e["overridden-chartA-B.SCBextra1"] = "13" - e["overridden-chartA-B.SC1extra6"] = "77" - - // `exports` style - e["SCBexported1B"] = "1965" - e["SC1extra7"] = "true" - e["SCBexported2A"] = "blaster" - e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" - - verifyRequirementsImportValues(t, c, v, e) -} -func verifyRequirementsImportValues(t *testing.T, c *chart.Chart, v *chart.Config, e map[string]string) { - - err := ProcessRequirementsImportValues(c) - if err != nil { - t.Errorf("Error processing import values requirements %v", err) - } - cv := c.GetValues() - cc, err := ReadValues([]byte(cv.Raw)) - if err != nil { - t.Errorf("Error reading import values %v", err) - } - for kk, vv := range e { - pv, err := cc.PathValue(kk) - if err != nil { - t.Fatalf("Error retrieving import values table %v %v", kk, err) - return - } - - switch pv.(type) { - case float64: - s := strconv.FormatFloat(pv.(float64), 'f', -1, 64) - if s != vv { - t.Errorf("Failed to match imported float value %v with expected %v", s, vv) - return - } - case bool: - b := strconv.FormatBool(pv.(bool)) - if b != vv { - t.Errorf("Failed to match imported bool value %v with expected %v", b, vv) - return - } - default: - if pv.(string) != vv { - t.Errorf("Failed to match imported string value %v with expected %v", pv, vv) - return - } - } - - } -} - -func TestGetAliasDependency(t *testing.T) { - c, err := Load("testdata/frobnitz") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - req, err := LoadRequirements(c) - if err != nil { - t.Fatalf("Failed to load requirement for testdata: %s", err) - } - if len(req.Dependencies) == 0 { - t.Fatalf("There are no requirements to test") - } - - // Success case - aliasChart := getAliasDependency(c.Dependencies, req.Dependencies[0]) - if aliasChart == nil { - t.Fatalf("Failed to get dependency chart for alias %s", req.Dependencies[0].Name) - } - if req.Dependencies[0].Alias != "" { - if aliasChart.Metadata.Name != req.Dependencies[0].Alias { - t.Fatalf("Dependency chart name should be %s but got %s", req.Dependencies[0].Alias, aliasChart.Metadata.Name) - } - } else if aliasChart.Metadata.Name != req.Dependencies[0].Name { - t.Fatalf("Dependency chart name should be %s but got %s", req.Dependencies[0].Name, aliasChart.Metadata.Name) - } - - if req.Dependencies[0].Version != "" { - if !version.IsCompatibleRange(req.Dependencies[0].Version, aliasChart.Metadata.Version) { - t.Fatalf("Dependency chart version is not in the compatible range") - } - - } - - // Failure case - req.Dependencies[0].Name = "something-else" - if aliasChart := getAliasDependency(c.Dependencies, req.Dependencies[0]); aliasChart != nil { - t.Fatalf("expected no chart but got %s", aliasChart.Metadata.Name) - } - - req.Dependencies[0].Version = "something else which is not in the compatible range" - if version.IsCompatibleRange(req.Dependencies[0].Version, aliasChart.Metadata.Version) { - t.Fatalf("Dependency chart version which is not in the compatible range should cause a failure other than a success ") - } - -} - -func TestDependentChartAliases(t *testing.T) { - c, err := Load("testdata/dependent-chart-alias") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - - if len(c.Dependencies) == 0 { - t.Fatal("There are no dependencies to run this test") - } - - origLength := len(c.Dependencies) - if err := ProcessRequirementsEnabled(c, c.Values); err != nil { - t.Fatalf("Expected no errors but got %q", err) - } - - if len(c.Dependencies) == origLength { - t.Fatal("Expected alias dependencies to be added, but did not got that") - } - - reqmts, err := LoadRequirements(c) - if err != nil { - t.Fatalf("Cannot load requirements for test chart, %v", err) - } - - if len(c.Dependencies) != len(reqmts.Dependencies) { - t.Fatalf("Expected number of chart dependencies %d, but got %d", len(reqmts.Dependencies), len(c.Dependencies)) - } - -} - -func TestDependentChartWithSubChartsAbsentInRequirements(t *testing.T) { - c, err := Load("testdata/dependent-chart-no-requirements-yaml") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - - if len(c.Dependencies) != 2 { - t.Fatalf("Expected 2 dependencies for this chart, but got %d", len(c.Dependencies)) - } - - origLength := len(c.Dependencies) - if err := ProcessRequirementsEnabled(c, c.Values); err != nil { - t.Fatalf("Expected no errors but got %q", err) - } - - if len(c.Dependencies) != origLength { - t.Fatal("Expected no changes in dependencies to be, but did something got changed") - } - -} - -func TestDependentChartWithSubChartsHelmignore(t *testing.T) { - if _, err := Load("testdata/dependent-chart-helmignore"); err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } -} - -func TestDependentChartsWithSubChartsSymlink(t *testing.T) { - c, err := Load("testdata/joonix") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - if c.Metadata.Name != "joonix" { - t.Fatalf("Unexpected chart name: %s", c.Metadata.Name) - } - if n := len(c.Dependencies); n != 1 { - t.Fatalf("Expected 1 dependency for this chart, but got %d", n) - } -} - -func TestDependentChartsWithSubchartsAllSpecifiedInRequirements(t *testing.T) { - c, err := Load("testdata/dependent-chart-with-all-in-requirements-yaml") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - - if len(c.Dependencies) == 0 { - t.Fatal("There are no dependencies to run this test") - } - - origLength := len(c.Dependencies) - if err := ProcessRequirementsEnabled(c, c.Values); err != nil { - t.Fatalf("Expected no errors but got %q", err) - } - - if len(c.Dependencies) != origLength { - t.Fatal("Expected no changes in dependencies to be, but did something got changed") - } - - reqmts, err := LoadRequirements(c) - if err != nil { - t.Fatalf("Cannot load requirements for test chart, %v", err) - } - - if len(c.Dependencies) != len(reqmts.Dependencies) { - t.Fatalf("Expected number of chart dependencies %d, but got %d", len(reqmts.Dependencies), len(c.Dependencies)) - } - -} - -func TestDependentChartsWithSomeSubchartsSpecifiedInRequirements(t *testing.T) { - c, err := Load("testdata/dependent-chart-with-mixed-requirements-yaml") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - - if len(c.Dependencies) == 0 { - t.Fatal("There are no dependencies to run this test") - } - - origLength := len(c.Dependencies) - if err := ProcessRequirementsEnabled(c, c.Values); err != nil { - t.Fatalf("Expected no errors but got %q", err) - } - - if len(c.Dependencies) != origLength { - t.Fatal("Expected no changes in dependencies to be, but did something got changed") - } - - reqmts, err := LoadRequirements(c) - if err != nil { - t.Fatalf("Cannot load requirements for test chart, %v", err) - } - - if len(c.Dependencies) <= len(reqmts.Dependencies) { - t.Fatalf("Expected more dependencies than specified in requirements.yaml(%d), but got %d", len(reqmts.Dependencies), len(c.Dependencies)) - } - -} diff --git a/pkg/chartutil/save.go b/pkg/chartutil/save.go index bff32dde519..2ce4eddaf46 100644 --- a/pkg/chartutil/save.go +++ b/pkg/chartutil/save.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,24 +19,31 @@ package chartutil import ( "archive/tar" "compress/gzip" - "errors" + "encoding/json" "fmt" - "io/ioutil" "os" "path/filepath" + "time" - "github.com/ghodss/yaml" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" - "k8s.io/helm/pkg/proto/hapi/chart" + "helm.sh/helm/v3/pkg/chart" ) var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") // SaveDir saves a chart as files in a directory. +// +// This takes the chart name, and creates a new subdirectory inside of the given dest +// directory, writing the chart's contents to that subdirectory. func SaveDir(c *chart.Chart, dest string) error { // Create the chart directory - outdir := filepath.Join(dest, c.Metadata.Name) - if err := os.Mkdir(outdir, 0755); err != nil { + outdir := filepath.Join(dest, c.Name()) + if fi, err := os.Stat(outdir); err == nil && !fi.IsDir() { + return errors.Errorf("file %s already exists and is not a directory", outdir) + } + if err := os.MkdirAll(outdir, 0755); err != nil { return err } @@ -46,47 +53,39 @@ func SaveDir(c *chart.Chart, dest string) error { } // Save values.yaml - if c.Values != nil && len(c.Values.Raw) > 0 { - vf := filepath.Join(outdir, ValuesfileName) - if err := ioutil.WriteFile(vf, []byte(c.Values.Raw), 0755); err != nil { - return err + for _, f := range c.Raw { + if f.Name == ValuesfileName { + vf := filepath.Join(outdir, ValuesfileName) + if err := writeFile(vf, f.Data); err != nil { + return err + } } } - for _, d := range []string{TemplatesDir, ChartsDir} { - if err := os.MkdirAll(filepath.Join(outdir, d), 0755); err != nil { - return err - } - } - - // Save templates - for _, f := range c.Templates { - n := filepath.Join(outdir, f.Name) - if err := ioutil.WriteFile(n, f.Data, 0755); err != nil { + // Save values.schema.json if it exists + if c.Schema != nil { + filename := filepath.Join(outdir, SchemafileName) + if err := writeFile(filename, c.Schema); err != nil { return err } } - // Save files - for _, f := range c.Files { - n := filepath.Join(outdir, f.TypeUrl) - - d := filepath.Dir(n) - if err := os.MkdirAll(d, 0755); err != nil { - return err - } - - if err := ioutil.WriteFile(n, f.Value, 0755); err != nil { - return err + // Save templates and files + for _, o := range [][]*chart.File{c.Templates, c.Files} { + for _, f := range o { + n := filepath.Join(outdir, f.Name) + if err := writeFile(n, f.Data); err != nil { + return err + } } } // Save dependencies base := filepath.Join(outdir, ChartsDir) - for _, dep := range c.Dependencies { + for _, dep := range c.Dependencies() { // Here, we write each dependency as a tar file. if _, err := Save(dep, base); err != nil { - return err + return errors.Wrapf(err, "saving %s", dep.ChartFullPath()) } } return nil @@ -101,32 +100,23 @@ func SaveDir(c *chart.Chart, dest string) error { // // This returns the absolute path to the chart archive file. func Save(c *chart.Chart, outDir string) (string, error) { - // Create archive - if fi, err := os.Stat(outDir); err != nil { - return "", err - } else if !fi.IsDir() { - return "", fmt.Errorf("location %s is not a directory", outDir) + if err := c.Validate(); err != nil { + return "", errors.Wrap(err, "chart validation") } - if c.Metadata == nil { - return "", errors.New("no Chart.yaml data") - } - - cfile := c.Metadata - if cfile.Name == "" { - return "", errors.New("no chart name specified (Chart.yaml)") - } else if cfile.Version == "" { - return "", errors.New("no chart version specified (Chart.yaml)") - } - - filename := fmt.Sprintf("%s-%s.tgz", cfile.Name, cfile.Version) + filename := fmt.Sprintf("%s-%s.tgz", c.Name(), c.Metadata.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 + dir := filepath.Dir(filename) + if stat, err := os.Stat(dir); err != nil { + if os.IsNotExist(err) { + if err2 := os.MkdirAll(dir, 0755); err2 != nil { + return "", err2 + } + } else { + return "", errors.Wrapf(err, "stat %s", dir) } } else if !stat.IsDir() { - return "", fmt.Errorf("is not a directory: %s", filepath.Dir(filename)) + return "", errors.Errorf("is not a directory: %s", dir) } f, err := os.Create(filename) @@ -153,25 +143,61 @@ func Save(c *chart.Chart, outDir string) (string, error) { if err := writeTarContents(twriter, c, ""); err != nil { rollback = true + return filename, err } - return filename, err + return filename, nil } func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { - base := filepath.Join(prefix, c.Metadata.Name) + base := filepath.Join(prefix, c.Name()) + // Pull out the dependencies of a v1 Chart, since there's no way + // to tell the serializer to skip a field for just this use case + savedDependencies := c.Metadata.Dependencies + if c.Metadata.APIVersion == chart.APIVersionV1 { + c.Metadata.Dependencies = nil + } // Save Chart.yaml cdata, err := yaml.Marshal(c.Metadata) + if c.Metadata.APIVersion == chart.APIVersionV1 { + c.Metadata.Dependencies = savedDependencies + } if err != nil { return err } - if err := writeToTar(out, base+"/Chart.yaml", cdata); err != nil { + if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata); err != nil { return err } + // Save Chart.lock + // TODO: remove the APIVersion check when APIVersionV1 is not used anymore + if c.Metadata.APIVersion == chart.APIVersionV2 { + if c.Lock != nil { + ldata, err := yaml.Marshal(c.Lock) + if err != nil { + return err + } + if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil { + return err + } + } + } + // Save values.yaml - if c.Values != nil && len(c.Values.Raw) > 0 { - if err := writeToTar(out, base+"/values.yaml", []byte(c.Values.Raw)); err != nil { + for _, f := range c.Raw { + if f.Name == ValuesfileName { + if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data); err != nil { + return err + } + } + } + + // Save values.schema.json if it exists + if c.Schema != nil { + if !json.Valid(c.Schema) { + return errors.New("Invalid JSON in " + SchemafileName) + } + if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema); err != nil { return err } } @@ -186,15 +212,15 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save files for _, f := range c.Files { - n := filepath.Join(base, f.TypeUrl) - if err := writeToTar(out, n, f.Value); err != nil { + n := filepath.Join(base, f.Name) + if err := writeToTar(out, n, f.Data); err != nil { return err } } // Save dependencies - for _, dep := range c.Dependencies { - if err := writeTarContents(out, dep, base+"/charts"); err != nil { + for _, dep := range c.Dependencies() { + if err := writeTarContents(out, dep, filepath.Join(base, ChartsDir)); err != nil { return err } } @@ -205,15 +231,14 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { func writeToTar(out *tar.Writer, name string, body []byte) error { // TODO: Do we need to create dummy parent directory names if none exist? h := &tar.Header{ - Name: name, - Mode: 0755, - Size: int64(len(body)), + Name: filepath.ToSlash(name), + Mode: 0644, + Size: int64(len(body)), + ModTime: time.Now(), } if err := out.WriteHeader(h); err != nil { return err } - if _, err := out.Write(body); err != nil { - return err - } - return nil + _, err := out.Write(body) + return err } diff --git a/pkg/chartutil/save_test.go b/pkg/chartutil/save_test.go index 5e156429981..b4951535a78 100644 --- a/pkg/chartutil/save_test.go +++ b/pkg/chartutil/save_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,16 +17,118 @@ limitations under the License. package chartutil import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" "io/ioutil" "os" + "path" + "path/filepath" + "regexp" "strings" "testing" + "time" - "github.com/golang/protobuf/ptypes/any" - "k8s.io/helm/pkg/proto/hapi/chart" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" ) func TestSave(t *testing.T) { + tmp := ensure.TempDir(t) + defer os.RemoveAll(tmp) + + for _, dest := range []string{tmp, path.Join(tmp, "newdir")} { + t.Run("outDir="+dest, func(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", + }, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*chart.File{ + {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + } + chartWithInvalidJSON := withSchema(*c, []byte("{")) + + where, err := Save(c, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + if !strings.HasPrefix(where, dest) { + t.Fatalf("Expected %q to start with %q", where, dest) + } + if !strings.HasSuffix(where, ".tgz") { + t.Fatalf("Expected %q to end with .tgz", where) + } + + c2, err := loader.LoadFile(where) + if err != nil { + t.Fatal(err) + } + if c2.Name() != c.Name() { + t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name()) + } + if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" { + t.Fatal("Files data did not match") + } + if c2.Lock != nil { + t.Fatal("Expected v1 chart archive not to contain Chart.lock file") + } + + if !bytes.Equal(c.Schema, c2.Schema) { + indentation := 4 + formattedExpected := Indent(indentation, string(c.Schema)) + formattedActual := Indent(indentation, string(c2.Schema)) + t.Fatalf("Schema data did not match.\nExpected:\n%s\nActual:\n%s", formattedExpected, formattedActual) + } + if _, err := Save(&chartWithInvalidJSON, dest); err == nil { + t.Fatalf("Invalid JSON was not caught while saving chart") + } + + c.Metadata.APIVersion = chart.APIVersionV2 + where, err = Save(c, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + c2, err = loader.LoadFile(where) + if err != nil { + t.Fatal(err) + } + if c2.Lock == nil { + t.Fatal("Expected v2 chart archive to contain a Chart.lock file") + } + if c2.Lock.Digest != c.Lock.Digest { + t.Fatal("Chart.lock data did not match") + } + }) + } +} + +// Creates a copy with a different schema; does not modify anything. +func withSchema(chart chart.Chart, schema []byte) chart.Chart { + chart.Schema = schema + return chart +} + +func Indent(n int, text string) string { + startOfLine := regexp.MustCompile(`(?m)^`) + indentation := strings.Repeat(" ", n) + return startOfLine.ReplaceAllLiteralString(text, indentation) +} + +func TestSavePreservesTimestamps(t *testing.T) { + // Test executes so quickly that if we don't subtract a second, the + // check will fail because `initialCreateTime` will be identical to the + // written timestamp for the files. + initialCreateTime := time.Now().Add(-1 * time.Second) + tmp, err := ioutil.TempDir("", "helm-") if err != nil { t.Fatal(err) @@ -35,42 +137,69 @@ func TestSave(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{ - Name: "ahab", - Version: "1.2.3.4", + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", }, - Values: &chart.Config{ - Raw: "ship: Pequod", + Values: map[string]interface{}{ + "imageName": "testimage", + "imageId": 42, }, - Files: []*any.Any{ - {TypeUrl: "scheherazade/shahryar.txt", Value: []byte("1,001 Nights")}, + Files: []*chart.File{ + {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), } where, err := Save(c, tmp) if err != nil { t.Fatalf("Failed to save: %s", err) } - if !strings.HasPrefix(where, tmp) { - t.Fatalf("Expected %q to start with %q", where, tmp) + + allHeaders, err := retrieveAllHeadersFromTar(where) + if err != nil { + t.Fatalf("Failed to parse tar: %v", err) } - if !strings.HasSuffix(where, ".tgz") { - t.Fatalf("Expected %q to end with .tgz", where) + + for _, header := range allHeaders { + if header.ModTime.Before(initialCreateTime) { + t.Fatalf("File timestamp not preserved: %v", header.ModTime) + } } +} - c2, err := LoadFile(where) +// We could refactor `load.go` to use this `retrieveAllHeadersFromTar` function +// as well, so we are not duplicating components of the code which iterate +// through the tar. +func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) { + raw, err := os.Open(path) if err != nil { - t.Fatal(err) + return nil, err } + defer raw.Close() - if c2.Metadata.Name != c.Metadata.Name { - t.Fatalf("Expected chart archive to have %q, got %q", c.Metadata.Name, c2.Metadata.Name) - } - if c2.Values.Raw != c.Values.Raw { - t.Fatal("Values data did not match") + unzipped, err := gzip.NewReader(raw) + if err != nil { + return nil, err } - if len(c2.Files) != 1 || c2.Files[0].TypeUrl != "scheherazade/shahryar.txt" { - t.Fatal("Files data did not match") + defer unzipped.Close() + + tr := tar.NewReader(unzipped) + headers := []*tar.Header{} + for { + hd, err := tr.Next() + if err == io.EOF { + break + } + + if err != nil { + return nil, err + } + + headers = append(headers, hd) } + + return headers, nil } func TestSaveDir(t *testing.T) { @@ -82,14 +211,15 @@ func TestSaveDir(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{ - Name: "ahab", - Version: "1.2.3.4", + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", }, - Values: &chart.Config{ - Raw: "ship: Pequod", + Files: []*chart.File{ + {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, - Files: []*any.Any{ - {TypeUrl: "scheherazade/shahryar.txt", Value: []byte("1,001 Nights")}, + Templates: []*chart.File{ + {Name: filepath.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, }, } @@ -97,18 +227,20 @@ func TestSaveDir(t *testing.T) { t.Fatalf("Failed to save: %s", err) } - c2, err := LoadDir(tmp + "/ahab") + c2, err := loader.LoadDir(tmp + "/ahab") if err != nil { t.Fatal(err) } - if c2.Metadata.Name != c.Metadata.Name { - t.Fatalf("Expected chart archive to have %q, got %q", c.Metadata.Name, c2.Metadata.Name) + if c2.Name() != c.Name() { + t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name()) } - if c2.Values.Raw != c.Values.Raw { - t.Fatal("Values data did not match") + + if len(c2.Templates) != 1 || c2.Templates[0].Name != filepath.Join(TemplatesDir, "nested", "dir", "thing.yaml") { + t.Fatal("Templates data did not match") } - if len(c2.Files) != 1 || c2.Files[0].TypeUrl != "scheherazade/shahryar.txt" { + + if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" { t.Fatal("Files data did not match") } } diff --git a/pkg/chartutil/testdata/dependent-chart-alias/Chart.lock b/pkg/chartutil/testdata/dependent-chart-alias/Chart.lock new file mode 100644 index 00000000000..6fcc2ed9fbe --- /dev/null +++ b/pkg/chartutil/testdata/dependent-chart-alias/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/chartutil/testdata/dependent-chart-alias/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-alias/Chart.yaml index 7c071c27b3b..751a3aa67b7 100644 --- a/pkg/chartutil/testdata/dependent-chart-alias/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-alias/Chart.yaml @@ -15,3 +15,15 @@ sources: - https://example.com/foo/bar home: http://example.com icon: https://example.com/64x64.png +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts + alias: mariners2 + - name: mariner + version: "4.3.2" + repository: https://example.com/charts + alias: mariners1 diff --git a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/Chart.yaml index 38a4aaa54c5..79e0d65db6a 100644 --- a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/Chart.yaml @@ -1,4 +1,5 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 -home: https://k8s.io/helm +home: https://helm.sh/helm diff --git a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/README.md b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/README.md index a7c84fc416c..b30b949ddfe 100644 --- a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/README.md +++ b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/README.md @@ -6,4 +6,4 @@ couple of parameters. The `values.toml` file contains the default values for the `alpine-pod.yaml` template. -You can install this example using `helm install docs/examples/alpine`. +You can install this example using `helm install ./alpine`. diff --git a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml index 171e3615646..1c9dd5fa425 100644 --- a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6adf..61cb6205111 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml index 08cf3c2c1f5..5bbae10afb3 100644 --- a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml +++ b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml @@ -3,11 +3,9 @@ kind: Pod metadata: name: {{.Release.Name}}-{{.Chart.Name}} labels: - heritage: {{.Release.Service}} + app.kubernetes.io/managed-by: {{.Release.Service}} chartName: {{.Chart.Name}} chartVersion: {{.Chart.Version | quote}} - annotations: - "helm.sh/created": "{{.Release.Time.Seconds}}" spec: restartPolicy: {{default "Never" .restart_policy}} containers: diff --git a/pkg/chartutil/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz index 3af333e76a3..3190136b050 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-alias/requirements.yaml b/pkg/chartutil/testdata/dependent-chart-alias/requirements.yaml deleted file mode 100644 index aab6cddf7cb..00000000000 --- a/pkg/chartutil/testdata/dependent-chart-alias/requirements.yaml +++ /dev/null @@ -1,12 +0,0 @@ -dependencies: - - name: alpine - version: "0.1.0" - repository: https://example.com/charts - - name: mariner - version: "4.3.2" - repository: https://example.com/charts - alias: mariners2 - - name: mariner - version: "4.3.2" - repository: https://example.com/charts - alias: mariners1 diff --git a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml index 38a4aaa54c5..79e0d65db6a 100644 --- a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml @@ -1,4 +1,5 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 -home: https://k8s.io/helm +home: https://helm.sh/helm diff --git a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/README.md b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/README.md index a7c84fc416c..b30b949ddfe 100644 --- a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/README.md +++ b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/README.md @@ -6,4 +6,4 @@ couple of parameters. The `values.toml` file contains the default values for the `alpine-pod.yaml` template. -You can install this example using `helm install docs/examples/alpine`. +You can install this example using `helm install ./alpine`. diff --git a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml index 171e3615646..1c9dd5fa425 100644 --- a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6adf..61cb6205111 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml index 08cf3c2c1f5..5bbae10afb3 100644 --- a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml +++ b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml @@ -3,11 +3,9 @@ kind: Pod metadata: name: {{.Release.Name}}-{{.Chart.Name}} labels: - heritage: {{.Release.Service}} + app.kubernetes.io/managed-by: {{.Release.Service}} chartName: {{.Chart.Name}} chartVersion: {{.Chart.Version | quote}} - annotations: - "helm.sh/created": "{{.Release.Time.Seconds}}" spec: restartPolicy: {{default "Never" .restart_policy}} containers: diff --git a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml index 38a4aaa54c5..79e0d65db6a 100644 --- a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml @@ -1,4 +1,5 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 -home: https://k8s.io/helm +home: https://helm.sh/helm diff --git a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md index a7c84fc416c..b30b949ddfe 100644 --- a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md +++ b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md @@ -6,4 +6,4 @@ couple of parameters. The `values.toml` file contains the default values for the `alpine-pod.yaml` template. -You can install this example using `helm install docs/examples/alpine`. +You can install this example using `helm install ./alpine`. diff --git a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml index 171e3615646..1c9dd5fa425 100644 --- a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6adf..61cb6205111 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml index 08cf3c2c1f5..5bbae10afb3 100644 --- a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml +++ b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml @@ -3,11 +3,9 @@ kind: Pod metadata: name: {{.Release.Name}}-{{.Chart.Name}} labels: - heritage: {{.Release.Service}} + app.kubernetes.io/managed-by: {{.Release.Service}} chartName: {{.Chart.Name}} chartVersion: {{.Chart.Version | quote}} - annotations: - "helm.sh/created": "{{.Release.Time.Seconds}}" spec: restartPolicy: {{default "Never" .restart_policy}} containers: diff --git a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz index 3af333e76a3..3190136b050 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml index 7c071c27b3b..fe7a9968163 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml @@ -15,3 +15,10 @@ sources: - https://example.com/foo/bar home: http://example.com icon: https://example.com/64x64.png +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml index 38a4aaa54c5..79e0d65db6a 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml @@ -1,4 +1,5 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 -home: https://k8s.io/helm +home: https://helm.sh/helm diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md index a7c84fc416c..b30b949ddfe 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md +++ b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md @@ -6,4 +6,4 @@ couple of parameters. The `values.toml` file contains the default values for the `alpine-pod.yaml` template. -You can install this example using `helm install docs/examples/alpine`. +You can install this example using `helm install ./alpine`. diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml index 171e3615646..1c9dd5fa425 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6adf..61cb6205111 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml index 08cf3c2c1f5..5bbae10afb3 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml @@ -3,11 +3,9 @@ kind: Pod metadata: name: {{.Release.Name}}-{{.Chart.Name}} labels: - heritage: {{.Release.Service}} + app.kubernetes.io/managed-by: {{.Release.Service}} chartName: {{.Chart.Name}} chartVersion: {{.Chart.Version | quote}} - annotations: - "helm.sh/created": "{{.Release.Time.Seconds}}" spec: restartPolicy: {{default "Never" .restart_policy}} containers: diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz index 3af333e76a3..3190136b050 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml index 7c071c27b3b..7fc39e28d07 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml @@ -15,3 +15,7 @@ sources: - https://example.com/foo/bar home: http://example.com icon: https://example.com/64x64.png +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml index 38a4aaa54c5..79e0d65db6a 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml @@ -1,4 +1,5 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 -home: https://k8s.io/helm +home: https://helm.sh/helm diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md index a7c84fc416c..b30b949ddfe 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md +++ b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md @@ -6,4 +6,4 @@ couple of parameters. The `values.toml` file contains the default values for the `alpine-pod.yaml` template. -You can install this example using `helm install docs/examples/alpine`. +You can install this example using `helm install ./alpine`. diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml index 171e3615646..1c9dd5fa425 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6adf..61cb6205111 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml index 08cf3c2c1f5..5bbae10afb3 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml @@ -3,11 +3,9 @@ kind: Pod metadata: name: {{.Release.Name}}-{{.Chart.Name}} labels: - heritage: {{.Release.Service}} + app.kubernetes.io/managed-by: {{.Release.Service}} chartName: {{.Chart.Name}} chartVersion: {{.Chart.Version | quote}} - annotations: - "helm.sh/created": "{{.Release.Time.Seconds}}" spec: restartPolicy: {{default "Never" .restart_policy}} containers: diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz index 3af333e76a3..3190136b050 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/requirements.yaml b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/requirements.yaml deleted file mode 100644 index 5f8bdc5a8c6..00000000000 --- a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/requirements.yaml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: - - name: alpine - version: "0.1.0" - repository: https://example.com/charts diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz index fb21cd08fb0..8731dce02cc 100644 Binary files a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz and b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/Chart.lock b/pkg/chartutil/testdata/frobnitz/Chart.lock new file mode 100644 index 00000000000..6fcc2ed9fbe --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/chartutil/testdata/frobnitz/Chart.yaml b/pkg/chartutil/testdata/frobnitz/Chart.yaml index 134cd11090a..fcd4a4a3764 100644 --- a/pkg/chartutil/testdata/frobnitz/Chart.yaml +++ b/pkg/chartutil/testdata/frobnitz/Chart.yaml @@ -18,3 +18,10 @@ icon: https://example.com/64x64.png annotations: extrakey: extravalue anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml index 38a4aaa54c5..79e0d65db6a 100644 --- a/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml @@ -1,4 +1,5 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 -home: https://k8s.io/helm +home: https://helm.sh/helm diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/README.md b/pkg/chartutil/testdata/frobnitz/charts/alpine/README.md index a7c84fc416c..b30b949ddfe 100644 --- a/pkg/chartutil/testdata/frobnitz/charts/alpine/README.md +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/README.md @@ -6,4 +6,4 @@ couple of parameters. The `values.toml` file contains the default values for the `alpine-pod.yaml` template. -You can install this example using `helm install docs/examples/alpine`. +You can install this example using `helm install ./alpine`. diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml index 171e3615646..1c9dd5fa425 100644 --- a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6adf..61cb6205111 100644 Binary files a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/pkg/chartutil/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml index 08cf3c2c1f5..5bbae10afb3 100644 --- a/pkg/chartutil/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml @@ -3,11 +3,9 @@ kind: Pod metadata: name: {{.Release.Name}}-{{.Chart.Name}} labels: - heritage: {{.Release.Service}} + app.kubernetes.io/managed-by: {{.Release.Service}} chartName: {{.Chart.Name}} chartVersion: {{.Chart.Version | quote}} - annotations: - "helm.sh/created": "{{.Release.Time.Seconds}}" spec: restartPolicy: {{default "Never" .restart_policy}} containers: diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz deleted file mode 100644 index 3af333e76a3..00000000000 Binary files a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz and /dev/null differ diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner/Chart.yaml b/pkg/chartutil/testdata/frobnitz/charts/mariner/Chart.yaml new file mode 100644 index 00000000000..92dc4b39096 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/mariner/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +name: mariner +description: A Helm chart for Kubernetes +version: 4.3.2 +home: "" +dependencies: + - name: albatross + repository: https://example.com/mariner/charts + version: "0.1.0" diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml b/pkg/chartutil/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml new file mode 100644 index 00000000000..b5188fde08b --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +name: albatross +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml b/pkg/chartutil/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml new file mode 100644 index 00000000000..3121cd7ce95 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml @@ -0,0 +1,4 @@ +albatross: "true" + +global: + author: Coleridge diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner/templates/placeholder.tpl b/pkg/chartutil/testdata/frobnitz/charts/mariner/templates/placeholder.tpl new file mode 100644 index 00000000000..29c11843ab8 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/mariner/templates/placeholder.tpl @@ -0,0 +1 @@ +# This is a placeholder. diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner/values.yaml b/pkg/chartutil/testdata/frobnitz/charts/mariner/values.yaml new file mode 100644 index 00000000000..b0ccb008629 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/mariner/values.yaml @@ -0,0 +1,7 @@ +# Default values for . +# This is a YAML-formatted file. https://github.com/toml-lang/toml +# Declare name/value pairs to be passed into your templates. +# name: "value" + +: + test: true diff --git a/pkg/chartutil/testdata/frobnitz_backslash-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz_backslash-1.2.3.tgz index 692dd6aba5d..6929659514d 100644 Binary files a/pkg/chartutil/testdata/frobnitz_backslash-1.2.3.tgz and b/pkg/chartutil/testdata/frobnitz_backslash-1.2.3.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz_backslash/Chart.yaml b/pkg/chartutil/testdata/frobnitz_backslash/Chart.yaml deleted file mode 100755 index 49df2a04649..00000000000 --- a/pkg/chartutil/testdata/frobnitz_backslash/Chart.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -name: frobnitz_backslash -description: This is a frobnitz. -version: "1.2.3" -keywords: - - frobnitz - - sprocket - - dodad -maintainers: - - name: The Helm Team - email: helm@example.com - - name: Someone Else - email: nobody@example.com -sources: - - https://example.com/foo/bar -home: http://example.com -icon: https://example.com/64x64.png -annotations: - extrakey: extravalue - anotherkey: anothervalue diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/Chart.yaml deleted file mode 100755 index 38a4aaa54c5..00000000000 --- a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: alpine -description: Deploy a basic Alpine Linux pod -version: 0.1.0 -home: https://k8s.io/helm diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/README.md b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/README.md deleted file mode 100755 index a7c84fc416c..00000000000 --- a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/README.md +++ /dev/null @@ -1,9 +0,0 @@ -This example was generated using the command `helm create alpine`. - -The `templates/` directory contains a very simple pod resource with a -couple of parameters. - -The `values.toml` file contains the default values for the -`alpine-pod.yaml` template. - -You can install this example using `helm install docs/examples/alpine`. diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml deleted file mode 100755 index 171e3615646..00000000000 --- a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: mast1 -description: A Helm chart for Kubernetes -version: 0.1.0 -home: "" diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz deleted file mode 100755 index ced5a4a6adf..00000000000 Binary files a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz and /dev/null differ diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml deleted file mode 100755 index 08cf3c2c1f5..00000000000 --- a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: {{.Release.Name}}-{{.Chart.Name}} - labels: - heritage: {{.Release.Service}} - chartName: {{.Chart.Name}} - chartVersion: {{.Chart.Version | quote}} - annotations: - "helm.sh/created": "{{.Release.Time.Seconds}}" -spec: - restartPolicy: {{default "Never" .restart_policy}} - containers: - - name: waiter - image: "alpine:3.3" - command: ["/bin/sleep","9000"] diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz deleted file mode 100755 index 3af333e76a3..00000000000 Binary files a/pkg/chartutil/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz and /dev/null differ diff --git a/pkg/chartutil/testdata/frobnitz_backslash/requirements.yaml b/pkg/chartutil/testdata/frobnitz_backslash/requirements.yaml deleted file mode 100755 index 5eb0bc98bc3..00000000000 --- a/pkg/chartutil/testdata/frobnitz_backslash/requirements.yaml +++ /dev/null @@ -1,7 +0,0 @@ -dependencies: - - name: alpine - version: "0.1.0" - repository: https://example.com/charts - - name: mariner - version: "4.3.2" - repository: https://example.com/charts diff --git a/pkg/chartutil/testdata/genfrob.sh b/pkg/chartutil/testdata/genfrob.sh index 8f2cddec1a4..35fdd59f20c 100755 --- a/pkg/chartutil/testdata/genfrob.sh +++ b/pkg/chartutil/testdata/genfrob.sh @@ -6,7 +6,9 @@ tar -zcvf mariner/charts/albatross-0.1.0.tgz albatross echo "Packing mariner into frobnitz" tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner +tar -zcvf frobnitz_backslash/charts/mariner-4.3.2.tgz mariner # Pack the frobnitz chart. echo "Packing frobnitz" tar --exclude=ignore/* -zcvf frobnitz-1.2.3.tgz frobnitz +tar --exclude=ignore/* -zcvf frobnitz_backslash-1.2.3.tgz frobnitz_backslash diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock new file mode 100644 index 00000000000..b2f17fb391f --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: dev + repository: file://envs/dev + version: v0.1.0 +- name: prod + repository: file://envs/prod + version: v0.1.0 +digest: sha256:9403fc24f6cf9d6055820126cf7633b4bd1fed3c77e4880c674059f536346182 +generated: "2020-02-03T10:38:51.180474+01:00" diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml new file mode 100644 index 00000000000..24b26d9e5d2 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v2 +name: parent-chart +version: v0.1.0 +appVersion: v0.1.0 +dependencies: + - name: dev + repository: "file://envs/dev" + version: ">= 0.0.1" + condition: dev.enabled,global.dev.enabled + tags: + - dev + import-values: + - data + + - name: prod + repository: "file://envs/prod" + version: ">= 0.0.1" + condition: prod.enabled,global.prod.enabled + tags: + - prod + import-values: + - data \ No newline at end of file diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz new file mode 100644 index 00000000000..d28e1621c86 Binary files /dev/null and b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz new file mode 100644 index 00000000000..a0c5aa84b6a Binary files /dev/null and b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml new file mode 100644 index 00000000000..80a52f5386b --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: dev +version: v0.1.0 +appVersion: v0.1.0 \ No newline at end of file diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml new file mode 100644 index 00000000000..38f03484db6 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml @@ -0,0 +1,9 @@ +# Dev values parent-chart +nameOverride: parent-chart-dev +exports: + data: + resources: + autoscaler: + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml new file mode 100644 index 00000000000..bda4be45811 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: prod +version: v0.1.0 +appVersion: v0.1.0 \ No newline at end of file diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml new file mode 100644 index 00000000000..10cc756b2aa --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml @@ -0,0 +1,9 @@ +# Prod values parent-chart +nameOverride: parent-chart-prod +exports: + data: + resources: + autoscaler: + minReplicas: 2 + maxReplicas: 5 + targetCPUUtilizationPercentage: 90 diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml new file mode 100644 index 00000000000..976e5a8f140 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml @@ -0,0 +1,16 @@ +################################################################################################### +# parent-chart horizontal pod autoscaler +################################################################################################### +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ .Release.Name }}-autoscaler + namespace: {{ .Release.Namespace }} +spec: + scaleTargetRef: + apiVersion: apps/v1beta1 + kind: Deployment + name: {{ .Release.Name }} + minReplicas: {{ required "A valid .Values.resources.autoscaler.minReplicas entry required!" .Values.resources.autoscaler.minReplicas }} + maxReplicas: {{ required "A valid .Values.resources.autoscaler.maxReplicas entry required!" .Values.resources.autoscaler.maxReplicas }} + targetCPUUtilizationPercentage: {{ required "A valid .Values.resources.autoscaler.targetCPUUtilizationPercentage!" .Values.resources.autoscaler.targetCPUUtilizationPercentage }} \ No newline at end of file diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml new file mode 100644 index 00000000000..b812f0a33ca --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml @@ -0,0 +1,10 @@ +# Default values for parent-chart. +nameOverride: parent-chart +tags: + dev: false + prod: true +resources: + autoscaler: + minReplicas: 0 + maxReplicas: 0 + targetCPUUtilizationPercentage: 99 \ No newline at end of file diff --git a/pkg/chartutil/testdata/joonix/charts/.gitkeep b/pkg/chartutil/testdata/joonix/charts/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/chartutil/testdata/joonix/charts/frobnitz b/pkg/chartutil/testdata/joonix/charts/frobnitz deleted file mode 120000 index fde1b78ac5f..00000000000 --- a/pkg/chartutil/testdata/joonix/charts/frobnitz +++ /dev/null @@ -1 +0,0 @@ -../../frobnitz \ No newline at end of file diff --git a/pkg/chartutil/testdata/mariner/Chart.yaml b/pkg/chartutil/testdata/mariner/Chart.yaml deleted file mode 100644 index 4d52794c6f5..00000000000 --- a/pkg/chartutil/testdata/mariner/Chart.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: mariner -description: A Helm chart for Kubernetes -version: 4.3.2 -home: "" diff --git a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz deleted file mode 100644 index 0b66d27fe1b..00000000000 Binary files a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz and /dev/null differ diff --git a/pkg/chartutil/testdata/mariner/requirements.yaml b/pkg/chartutil/testdata/mariner/requirements.yaml deleted file mode 100644 index 0b21d15b743..00000000000 --- a/pkg/chartutil/testdata/mariner/requirements.yaml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: - - name: albatross - repository: https://example.com/mariner/charts - version: "0.1.0" diff --git a/pkg/chartutil/testdata/moby/Chart.yaml b/pkg/chartutil/testdata/moby/Chart.yaml deleted file mode 100644 index b725af91693..00000000000 --- a/pkg/chartutil/testdata/moby/Chart.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: A Helm chart for Kubernetes -name: moby -version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml b/pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml deleted file mode 100644 index d9a3bfd5ff2..00000000000 --- a/pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: A Helm chart for Kubernetes -name: pequod -version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml b/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml deleted file mode 100644 index c3cdf397ddb..00000000000 --- a/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: A Helm chart for Kubernetes -name: ahab -version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/values.yaml b/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/values.yaml deleted file mode 100644 index 86c3f63aac3..00000000000 --- a/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/values.yaml +++ /dev/null @@ -1,2 +0,0 @@ -scope: ahab -name: ahab diff --git a/pkg/chartutil/testdata/moby/charts/pequod/values.yaml b/pkg/chartutil/testdata/moby/charts/pequod/values.yaml deleted file mode 100644 index d6e34b274d6..00000000000 --- a/pkg/chartutil/testdata/moby/charts/pequod/values.yaml +++ /dev/null @@ -1,2 +0,0 @@ -scope: pequod -name: pequod diff --git a/pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml b/pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml deleted file mode 100644 index f6819c06d8d..00000000000 --- a/pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: A Helm chart for Kubernetes -name: spouter -version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/spouter/values.yaml b/pkg/chartutil/testdata/moby/charts/spouter/values.yaml deleted file mode 100644 index f71d92a9ff6..00000000000 --- a/pkg/chartutil/testdata/moby/charts/spouter/values.yaml +++ /dev/null @@ -1 +0,0 @@ -scope: spouter diff --git a/pkg/chartutil/testdata/moby/values.yaml b/pkg/chartutil/testdata/moby/values.yaml deleted file mode 100644 index 54e1ce46323..00000000000 --- a/pkg/chartutil/testdata/moby/values.yaml +++ /dev/null @@ -1,9 +0,0 @@ -scope: moby -name: moby -override: bad -top: nope -bottom: exists -right: exists -left: exists -front: exists -back: exists diff --git a/pkg/chartutil/testdata/subpop/Chart.yaml b/pkg/chartutil/testdata/subpop/Chart.yaml index bbb0941c373..27118672a38 100644 --- a/pkg/chartutil/testdata/subpop/Chart.yaml +++ b/pkg/chartutil/testdata/subpop/Chart.yaml @@ -2,3 +2,40 @@ apiVersion: v1 description: A Helm chart for Kubernetes name: parentchart version: 0.1.0 +dependencies: + - name: subchart1 + repository: http://localhost:10191 + version: 0.1.0 + condition: subchart1.enabled + tags: + - front-end + - subchart1 + import-values: + - child: SC1data + parent: imported-chart1 + - child: SC1data + parent: overridden-chart1 + - child: imported-chartA + parent: imported-chartA + - child: imported-chartA-B + parent: imported-chartA-B + - child: overridden-chartA-B + parent: overridden-chartA-B + - child: SCBexported1A + parent: . + - SCBexported2 + - SC1exported1 + + - name: subchart2 + repository: http://localhost:10191 + version: 0.1.0 + condition: subchart2.enabled + tags: + - back-end + - subchart2 + + - name: subchart2 + alias: subchart2alias + repository: http://localhost:10191 + version: 0.1.0 + condition: subchart2alias.enabled diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml index f58d828189f..9d8c03ee1fd 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml @@ -2,3 +2,35 @@ apiVersion: v1 description: A Helm chart for Kubernetes name: subchart1 version: 0.1.0 +dependencies: + - name: subcharta + repository: http://localhost:10191 + version: 0.1.0 + condition: subcharta.enabled + tags: + - front-end + - subcharta + import-values: + - child: SCAdata + parent: imported-chartA + - child: SCAdata + parent: overridden-chartA + - child: SCAdata + parent: imported-chartA-B + + - name: subchartb + repository: http://localhost:10191 + version: 0.1.0 + condition: subchartb.enabled + import-values: + - child: SCBdata + parent: imported-chartB + - child: SCBdata + parent: imported-chartA-B + - child: exports.SCBexported2 + parent: exports.SCBexported2 + - SCBexported1 + + tags: + - front-end + - subchartb diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml index fdf75aa911a..27501e1e0b2 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: name: {{ .Chart.Name }} labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" spec: type: {{ .Values.service.type }} ports: @@ -12,4 +12,4 @@ spec: protocol: TCP name: {{ .Values.service.name }} selector: - app: {{ .Chart.Name }} + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml index fdf75aa911a..27501e1e0b2 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: name: {{ .Chart.Name }} labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" spec: type: {{ .Values.service.type }} ports: @@ -12,4 +12,4 @@ spec: protocol: TCP name: {{ .Values.service.name }} selector: - app: {{ .Chart.Name }} + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/crds/crdA.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/crds/crdA.yaml new file mode 100644 index 00000000000..fca77fd4b1c --- /dev/null +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/crds/crdA.yaml @@ -0,0 +1,13 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: testCRDs +spec: + group: testCRDGroups + names: + kind: TestCRD + listKind: TestCRDList + plural: TestCRDs + shortNames: + - tc + singular: authconfig diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml deleted file mode 100644 index abfe85e764d..00000000000 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/requirements.yaml +++ /dev/null @@ -1,32 +0,0 @@ -dependencies: - - name: subcharta - repository: http://localhost:10191 - version: 0.1.0 - condition: subcharta.enabled,subchart1.subcharta.enabled - tags: - - front-end - - subcharta - import-values: - - child: SCAdata - parent: imported-chartA - - child: SCAdata - parent: overridden-chartA - - child: SCAdata - parent: imported-chartA-B - - - name: subchartb - repository: http://localhost:10191 - version: 0.1.0 - condition: subchartb.enabled - import-values: - - child: SCBdata - parent: imported-chartB - - child: SCBdata - parent: imported-chartA-B - - child: exports.SCBexported2 - parent: exports.SCBexported2 - - SCBexported1 - - tags: - - front-end - - subchartb diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/service.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/service.yaml index 3835a3d0b13..fee94dced9b 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/service.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/service.yaml @@ -3,12 +3,14 @@ kind: Service metadata: name: {{ .Chart.Name }} labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - namespace: "{{ .Release.Namespace }}" - release-name: "{{ .Release.Name }}" + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + app.kubernetes.io/instance: "{{ .Release.Name }}" kube-version/major: "{{ .Capabilities.KubeVersion.Major }}" kube-version/minor: "{{ .Capabilities.KubeVersion.Minor }}" - kube-version/gitversion: "v{{ .Capabilities.KubeVersion.Major }}.{{ .Capabilities.KubeVersion.Minor }}.0" + kube-version/version: "v{{ .Capabilities.KubeVersion.Major }}.{{ .Capabilities.KubeVersion.Minor }}.0" +{{- if .Capabilities.APIVersions.Has "helm.k8s.io/test" }} + kube-api-version/test: v1 +{{- end }} spec: type: {{ .Values.service.type }} ports: @@ -17,4 +19,4 @@ spec: protocol: TCP name: {{ .Values.service.name }} selector: - app: {{ .Chart.Name }} + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/role.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/role.yaml new file mode 100644 index 00000000000..91b954e5fba --- /dev/null +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/role.yaml @@ -0,0 +1,7 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Chart.Name }}-role +rules: +- resources: ["*"] + verbs: ["get","list","watch"] diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml new file mode 100644 index 00000000000..5d193f1a670 --- /dev/null +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Chart.Name }}-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Chart.Name }}-role +subjects: +- kind: ServiceAccount + name: {{ .Chart.Name }}-sa + namespace: default diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml new file mode 100644 index 00000000000..7126c7d89bc --- /dev/null +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Chart.Name }}-sa diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml index 72d3fa5c8b2..a974e316ad8 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml @@ -23,7 +23,7 @@ overridden-chartA: SCAbool: true SCAfloat: 3.14 SCAint: 100 - SCAstring: "jabathehut" + SCAstring: "jabbathehut" SC1extra3: true imported-chartA-B: diff --git a/pkg/chartutil/testdata/subpop/charts/subchart2/Chart.yaml b/pkg/chartutil/testdata/subpop/charts/subchart2/Chart.yaml index 2c76b078661..f936528a739 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart2/Chart.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart2/Chart.yaml @@ -2,3 +2,18 @@ apiVersion: v1 description: A Helm chart for Kubernetes name: subchart2 version: 0.1.0 +dependencies: + - name: subchartb + repository: http://localhost:10191 + version: 0.1.0 + condition: subchartb.enabled + tags: + - back-end + - subchartb + - name: subchartc + repository: http://localhost:10191 + version: 0.1.0 + condition: subchartc.enabled + tags: + - back-end + - subchartc diff --git a/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml b/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml index 0935aadce3c..3f168bdbf14 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: name: subchart2-{{ .Chart.Name }} labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + helm.sh/hart: "{{ .Chart.Name }}-{{ .Chart.Version }}" spec: type: {{ .Values.service.type }} ports: @@ -12,4 +12,4 @@ spec: protocol: TCP name: subchart2-{{ .Values.service.name }} selector: - app: {{ .Chart.Name }} + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml b/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml index fdf75aa911a..27501e1e0b2 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: name: {{ .Chart.Name }} labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" spec: type: {{ .Values.service.type }} ports: @@ -12,4 +12,4 @@ spec: protocol: TCP name: {{ .Values.service.name }} selector: - app: {{ .Chart.Name }} + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/chartutil/testdata/subpop/charts/subchart2/requirements.yaml b/pkg/chartutil/testdata/subpop/charts/subchart2/requirements.yaml deleted file mode 100644 index 1f0023a08b3..00000000000 --- a/pkg/chartutil/testdata/subpop/charts/subchart2/requirements.yaml +++ /dev/null @@ -1,15 +0,0 @@ -dependencies: - - name: subchartb - repository: http://localhost:10191 - version: 0.1.0 - condition: subchartb.enabled,subchart2.subchartb.enabled - tags: - - back-end - - subchartb - - name: subchartc - repository: http://localhost:10191 - version: 0.1.0 - condition: subchartc.enabled - tags: - - back-end - - subchartc diff --git a/pkg/chartutil/testdata/subpop/charts/subchart2/templates/service.yaml b/pkg/chartutil/testdata/subpop/charts/subchart2/templates/service.yaml index fdf75aa911a..27501e1e0b2 100644 --- a/pkg/chartutil/testdata/subpop/charts/subchart2/templates/service.yaml +++ b/pkg/chartutil/testdata/subpop/charts/subchart2/templates/service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: name: {{ .Chart.Name }} labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" spec: type: {{ .Values.service.type }} ports: @@ -12,4 +12,4 @@ spec: protocol: TCP name: {{ .Values.service.name }} selector: - app: {{ .Chart.Name }} + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/chartutil/testdata/subpop/noreqs/templates/service.yaml b/pkg/chartutil/testdata/subpop/noreqs/templates/service.yaml index fdf75aa911a..27501e1e0b2 100644 --- a/pkg/chartutil/testdata/subpop/noreqs/templates/service.yaml +++ b/pkg/chartutil/testdata/subpop/noreqs/templates/service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: name: {{ .Chart.Name }} labels: - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" spec: type: {{ .Values.service.type }} ports: @@ -12,4 +12,4 @@ spec: protocol: TCP name: {{ .Values.service.name }} selector: - app: {{ .Chart.Name }} + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/chartutil/testdata/subpop/requirements.yaml b/pkg/chartutil/testdata/subpop/requirements.yaml deleted file mode 100644 index a8eb0aaceb3..00000000000 --- a/pkg/chartutil/testdata/subpop/requirements.yaml +++ /dev/null @@ -1,31 +0,0 @@ -dependencies: - - name: subchart1 - repository: http://localhost:10191 - version: 0.1.0 - condition: subchart1.enabled - tags: - - front-end - - subchart1 - import-values: - - child: SC1data - parent: imported-chart1 - - child: SC1data - parent: overridden-chart1 - - child: imported-chartA - parent: imported-chartA - - child: imported-chartA-B - parent: imported-chartA-B - - child: overridden-chartA-B - parent: overridden-chartA-B - - child: SCBexported1A - parent: . - - SCBexported2 - - SC1exported1 - - - name: subchart2 - repository: http://localhost:10191 - version: 0.1.0 - condition: subchart2.enabled - tags: - - back-end - - subchart2 diff --git a/pkg/chartutil/testdata/subpop/values.yaml b/pkg/chartutil/testdata/subpop/values.yaml index 55e872d416e..d611d6a89a3 100644 --- a/pkg/chartutil/testdata/subpop/values.yaml +++ b/pkg/chartutil/testdata/subpop/values.yaml @@ -18,7 +18,7 @@ overridden-chartA: SCAbool: true SCAfloat: 41.3 SCAint: 808 - SCAstring: "jaberwocky" + SCAstring: "jabberwocky" SPextra4: true imported-chartA-B: @@ -28,7 +28,7 @@ overridden-chartA-B: SCAbool: true SCAfloat: 41.3 SCAint: 808 - SCAstring: "jaberwocky" + SCAstring: "jabberwocky" SCBbool: false SCBfloat: 1.99 SCBint: 77 @@ -39,3 +39,5 @@ tags: front-end: true back-end: false +subchart2alias: + enabled: false diff --git a/pkg/chartutil/testdata/test-values-negative.yaml b/pkg/chartutil/testdata/test-values-negative.yaml new file mode 100644 index 00000000000..5a1250bff36 --- /dev/null +++ b/pkg/chartutil/testdata/test-values-negative.yaml @@ -0,0 +1,14 @@ +firstname: John +lastname: Doe +age: -5 +likesCoffee: true +addresses: + - city: Springfield + street: Main + number: 12345 + - city: New York + street: Broadway + number: 67890 +phoneNumbers: + - "(888) 888-8888" + - "(555) 555-5555" diff --git a/pkg/chartutil/testdata/test-values.schema.json b/pkg/chartutil/testdata/test-values.schema.json new file mode 100644 index 00000000000..4df89bbe89f --- /dev/null +++ b/pkg/chartutil/testdata/test-values.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "addresses": { + "description": "List of addresses", + "items": { + "properties": { + "city": { + "type": "string" + }, + "number": { + "type": "number" + }, + "street": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "age": { + "description": "Age", + "minimum": 0, + "type": "integer" + }, + "employmentInfo": { + "properties": { + "salary": { + "minimum": 0, + "type": "number" + }, + "title": { + "type": "string" + } + }, + "required": [ + "salary" + ], + "type": "object" + }, + "firstname": { + "description": "First name", + "type": "string" + }, + "lastname": { + "type": "string" + }, + "likesCoffee": { + "type": "boolean" + }, + "phoneNumbers": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "firstname", + "lastname", + "addresses", + "employmentInfo" + ], + "title": "Values", + "type": "object" +} diff --git a/pkg/chartutil/testdata/test-values.yaml b/pkg/chartutil/testdata/test-values.yaml new file mode 100644 index 00000000000..042dea664b8 --- /dev/null +++ b/pkg/chartutil/testdata/test-values.yaml @@ -0,0 +1,17 @@ +firstname: John +lastname: Doe +age: 25 +likesCoffee: true +employmentInfo: + title: Software Developer + salary: 100000 +addresses: + - city: Springfield + street: Main + number: 12345 + - city: New York + street: Broadway + number: 67890 +phoneNumbers: + - "(888) 888-8888" + - "(555) 555-5555" diff --git a/pkg/chartutil/transform.go b/pkg/chartutil/transform.go deleted file mode 100644 index 7cbb754fb32..00000000000 --- a/pkg/chartutil/transform.go +++ /dev/null @@ -1,25 +0,0 @@ -/* -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 chartutil - -import "strings" - -// Transform performs a string replacement of the specified source for -// a given key with the replacement string -func Transform(src string, key string, replacement string) []byte { - return []byte(strings.Replace(src, key, replacement, -1)) -} diff --git a/pkg/chartutil/validate_name.go b/pkg/chartutil/validate_name.go new file mode 100644 index 00000000000..d253731ec63 --- /dev/null +++ b/pkg/chartutil/validate_name.go @@ -0,0 +1,107 @@ +/* +Copyright The Helm Authors. + +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 chartutil + +import ( + "fmt" + "regexp" + + "github.com/pkg/errors" +) + +// validName is a regular expression for resource names. +// +// According to the Kubernetes help text, the regular expression it uses is: +// +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) + +var ( + // errMissingName indicates that a release (name) was not provided. + errMissingName = errors.New("no name provided") + + // errInvalidName indicates that an invalid release name was provided + errInvalidName = errors.New(fmt.Sprintf( + "invalid release name, must match regex %s and the length must not be longer than 53", + validName.String())) + + // errInvalidKubernetesName indicates that the name does not meet the Kubernetes + // restrictions on metadata names. + errInvalidKubernetesName = errors.New(fmt.Sprintf( + "invalid metadata name, must match regex %s and the length must not be longer than 253", + validName.String())) +) + +const ( + // maxNameLen is the maximum length Helm allows for a release name + maxReleaseNameLen = 53 + // maxMetadataNameLen is the maximum length Kubernetes allows for any name. + maxMetadataNameLen = 253 +) + +// ValidateReleaseName performs checks for an entry for a Helm release name +// +// For Helm to allow a name, it must be below a certain character count (53) and also match +// a regular expression. +// +// According to the Kubernetes help text, the regular expression it uses is: +// +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +func ValidateReleaseName(name string) error { + // This case is preserved for backwards compatibility + if name == "" { + return errMissingName + + } + if len(name) > maxReleaseNameLen || !validName.MatchString(name) { + return errInvalidName + } + return nil +} + +// ValidateMetadataName validates the name field of a Kubernetes metadata object. +// +// Empty strings, strings longer than 253 chars, or strings that don't match the regexp +// will fail. +// +// According to the Kubernetes help text, the regular expression it uses is: +// +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +// +// Deprecated: remove in Helm 4. Name validation now uses rules defined in +// pkg/lint/rules.validateMetadataNameFunc() +func ValidateMetadataName(name string) error { + if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) { + return errInvalidKubernetesName + } + return nil +} diff --git a/pkg/chartutil/validate_name_test.go b/pkg/chartutil/validate_name_test.go new file mode 100644 index 00000000000..5f0792f9452 --- /dev/null +++ b/pkg/chartutil/validate_name_test.go @@ -0,0 +1,91 @@ +/* +Copyright The Helm Authors. + +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 chartutil + +import "testing" + +// TestValidateName is a regression test for ValidateName +// +// Kubernetes has strict naming conventions for resource names. This test represents +// those conventions. +// +// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +// +// NOTE: At the time of this writing, the docs above say that names cannot begin with +// digits. However, `kubectl`'s regular expression explicit allows this, and +// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits. +func TestValidateReleaseName(t *testing.T) { + names := map[string]bool{ + "": false, + "foo": true, + "foo.bar1234baz.seventyone": true, + "FOO": false, + "123baz": true, + "foo.BAR.baz": false, + "one-two": true, + "-two": false, + "one_two": false, + "a..b": false, + "%^&#$%*@^*@&#^": false, + "example:com": false, + "example%%com": false, + "a1111111111111111111111111111111111111111111111111111111111z": false, + } + for input, expectPass := range names { + if err := ValidateReleaseName(input); (err == nil) != expectPass { + st := "fail" + if expectPass { + st = "succeed" + } + t.Errorf("Expected %q to %s", input, st) + } + } +} + +func TestValidateMetadataName(t *testing.T) { + names := map[string]bool{ + "": false, + "foo": true, + "foo.bar1234baz.seventyone": true, + "FOO": false, + "123baz": true, + "foo.BAR.baz": false, + "one-two": true, + "-two": false, + "one_two": false, + "a..b": false, + "%^&#$%*@^*@&#^": false, + "example:com": false, + "example%%com": false, + "a1111111111111111111111111111111111111111111111111111111111z": true, + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z": false, + } + for input, expectPass := range names { + if err := ValidateMetadataName(input); (err == nil) != expectPass { + st := "fail" + if expectPass { + st = "succeed" + } + t.Errorf("Expected %q to %s", input, st) + } + } +} diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index 66a2658d511..e1cdf464228 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,23 +17,16 @@ limitations under the License. package chartutil import ( - "errors" "fmt" "io" "io/ioutil" - "log" "strings" - "github.com/ghodss/yaml" - "github.com/golang/protobuf/ptypes/timestamp" - "k8s.io/helm/pkg/proto/hapi/chart" -) - -// ErrNoTable indicates that a chart does not have a matching table. -type ErrNoTable error + "github.com/pkg/errors" + "sigs.k8s.io/yaml" -// ErrNoValue indicates that Values does not contain a key with a value -type ErrNoValue error + "helm.sh/helm/v3/pkg/chart" +) // GlobalKey is the name of the Values key that is used for storing global vars. const GlobalKey = "global" @@ -60,14 +53,12 @@ func (v Values) YAML() (string, error) { // // An ErrNoTable is returned if the table does not exist. func (v Values) Table(name string) (Values, error) { - names := strings.Split(name, ".") table := v var err error - for _, n := range names { - table, err = tableLookup(table, n) - if err != nil { - return table, err + for _, n := range parsePath(name) { + if table, err = tableLookup(table, n); err != nil { + break } } return table, err @@ -85,7 +76,6 @@ func (v Values) AsMap() map[string]interface{} { // Encode writes serialized Values information to the given io.Writer. func (v Values) Encode(w io.Writer) error { - //return yaml.NewEncoder(w).Encode(v) out, err := yaml.Marshal(v) if err != nil { return err @@ -97,7 +87,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(fmt.Errorf("no table named %q (%v)", simple, v)) + return v, ErrNoTable{simple} } if vv, ok := v2.(map[string]interface{}); ok { return vv, nil @@ -110,8 +100,7 @@ func tableLookup(v Values, simple string) (Values, error) { return vv, nil } - var e ErrNoTable = fmt.Errorf("no table named %q", simple) - return map[string]interface{}{}, e + return Values{}, ErrNoTable{simple} } // ReadValues will parse YAML byte data into a Values. @@ -120,7 +109,7 @@ func ReadValues(data []byte) (vals Values, err error) { if len(vals) == 0 { vals = Values{} } - return + return vals, err } // ReadValuesFile will parse a YAML file into a map of values. @@ -132,245 +121,34 @@ func ReadValuesFile(filename string) (Values, error) { return ReadValues(data) } -// CoalesceValues coalesces all of the values in a chart (and its subcharts). -// -// Values are coalesced together using the following rules: -// -// - Values in a higher level chart always override values in a lower-level -// dependency chart -// - Scalar values and arrays are replaced, maps are merged -// - 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) (Values, error) { - 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 { - evals, err := ReadValues([]byte(vals.Raw)) - if err != nil { - return cvals, err - } - cvals, err = coalesce(chrt, evals) - if err != nil { - return cvals, err - } - } - - var err error - cvals, err = coalesceDeps(chrt, cvals) - return cvals, err -} - -// coalesce coalesces the dest values and the chart values, giving priority to the dest values. -// -// This is a helper function for CoalesceValues. -func coalesce(ch *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) { - var err error - dest, err = coalesceValues(ch, dest) - if err != nil { - return dest, err - } - coalesceDeps(ch, dest) - return dest, nil -} - -// coalesceDeps coalesces the dependencies of the given chart. -func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) { - for _, subchart := range chrt.Dependencies { - if c, ok := dest[subchart.Metadata.Name]; !ok { - // If dest doesn't already have the key, create it. - dest[subchart.Metadata.Name] = map[string]interface{}{} - } else if !istable(c) { - return dest, fmt.Errorf("type mismatch on %s: %t", subchart.Metadata.Name, c) - } - if dv, ok := dest[subchart.Metadata.Name]; ok { - dvmap := dv.(map[string]interface{}) - - // Get globals out of dest and merge them into dvmap. - coalesceGlobals(dvmap, dest) - - var err error - // Now coalesce the rest of the values. - dest[subchart.Metadata.Name], err = coalesce(subchart, dvmap) - if err != nil { - return dest, err - } - } - } - return dest, nil -} - -// coalesceGlobals copies the globals out of src and merges them into dest. -// -// For convenience, returns dest. -func coalesceGlobals(dest, src map[string]interface{}) map[string]interface{} { - var dg, sg map[string]interface{} - - if destglob, ok := dest[GlobalKey]; !ok { - dg = map[string]interface{}{} - } else if dg, ok = destglob.(map[string]interface{}); !ok { - log.Printf("warning: skipping globals because destination %s is not a table.", GlobalKey) - return dg - } - - if srcglob, ok := src[GlobalKey]; !ok { - sg = map[string]interface{}{} - } else if sg, ok = srcglob.(map[string]interface{}); !ok { - log.Printf("warning: skipping globals because source %s is not a table.", GlobalKey) - return dg - } - - // EXPERIMENTAL: In the past, we have disallowed globals to test tables. This - // reverses that decision. It may somehow be possible to introduce a loop - // here, but I haven't found a way. So for the time being, let's allow - // tables in globals. - for key, val := range sg { - if istable(val) { - vv := copyMap(val.(map[string]interface{})) - if destv, ok := dg[key]; ok { - if destvmap, ok := destv.(map[string]interface{}); ok { - // Basically, we reverse order of coalesce here to merge - // top-down. - coalesceTables(vv, destvmap) - dg[key] = vv - continue - } else { - log.Printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key) - } - } else { - // Here there is no merge. We're just adding. - dg[key] = vv - } - } else if dv, ok := dg[key]; ok && istable(dv) { - // It's not clear if this condition can actually ever trigger. - log.Printf("key %s is table. Skipping", key) - continue - } - // TODO: Do we need to do any additional checking on the value? - dg[key] = val - } - dest[GlobalKey] = dg - return dest -} - -func copyMap(src map[string]interface{}) map[string]interface{} { - dest := make(map[string]interface{}, len(src)) - for k, v := range src { - dest[k] = v - } - return dest -} - -// coalesceValues builds up a values map for a particular chart. -// -// Values in v will override the values in the chart. -func coalesceValues(c *chart.Chart, v map[string]interface{}) (map[string]interface{}, error) { - // If there are no values in the chart, we just return the given values - if c.Values == nil || c.Values.Raw == "" { - return v, nil - } - - nv, err := ReadValues([]byte(c.Values.Raw)) - if err != nil { - // On error, we return just the overridden values. - // FIXME: We should log this error. It indicates that the YAML data - // did not parse. - return v, fmt.Errorf("error reading default values (%s): %s", c.Values.Raw, err) - } - - for key, val := range nv { - if value, ok := v[key]; ok { - if value == nil { - // When the YAML value is null, we remove the value's key. - // This allows Helm's various sources of values (value files or --set) to - // remove incompatible keys from any previous chart, file, or set values. - delete(v, key) - } else if dest, ok := value.(map[string]interface{}); ok { - // if v[key] is a table, merge nv's val table into v[key]. - src, ok := val.(map[string]interface{}) - if !ok { - log.Printf("warning: skipped value for %s: Not a table.", key) - continue - } - // Because v has higher precedence than nv, dest values override src - // values. - coalesceTables(dest, src) - } - } else { - // If the key is not in v, copy it from nv. - v[key] = val - } - } - return v, nil -} - -// coalesceTables merges a source map into a destination map. -// -// dest is considered authoritative. -func coalesceTables(dst, src map[string]interface{}) map[string]interface{} { - // Because dest has higher precedence than src, dest values override src - // values. - for key, val := range src { - if istable(val) { - if innerdst, ok := dst[key]; !ok { - dst[key] = val - } else if istable(innerdst) { - coalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{})) - } else { - log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val) - } - continue - } else if dv, ok := dst[key]; ok && istable(dv) { - log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val) - continue - } else if !ok { // <- ok is still in scope from preceding conditional. - dst[key] = val - continue - } - } - 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 + Revision int IsUpgrade bool IsInstall bool - Revision int } // ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files // -// WARNING: This function is deprecated for Helm > 2.1.99 Use ToRenderValuesCaps() instead. It will -// remain in the codebase to stay SemVer compliant. -// -// In Helm 3.0, this will be changed to accept Capabilities as a fourth parameter. -func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions) (Values, error) { - caps := &Capabilities{APIVersions: DefaultVersionSet} - return ToRenderValuesCaps(chrt, chrtVals, options, caps) -} - -// ToRenderValuesCaps composes the struct from the data coming from the Releases, Charts and Values files -// // This takes both ReleaseOptions and Capabilities to merge into the render values. -func ToRenderValuesCaps(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions, caps *Capabilities) (Values, error) { - +func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) { + if caps == nil { + caps = DefaultCapabilities + } top := map[string]interface{}{ + "Chart": chrt.Metadata, + "Capabilities": caps, "Release": map[string]interface{}{ "Name": options.Name, - "Time": options.Time, "Namespace": options.Namespace, "IsUpgrade": options.IsUpgrade, "IsInstall": options.IsInstall, "Revision": options.Revision, - "Service": "Tiller", + "Service": "Helm", }, - "Chart": chrt.Metadata, - "Files": NewFiles(chrt.Files), - "Capabilities": caps, } vals, err := CoalesceValues(chrt, chrtVals) @@ -378,6 +156,11 @@ func ToRenderValuesCaps(chrt *chart.Chart, chrtVals *chart.Config, options Relea return top, err } + if err := ValidateAgainstSchema(chrt, vals); err != nil { + errFmt := "values don't meet the specifications of the schema(s) in the following chart(s):\n%s" + return top, fmt.Errorf(errFmt, err.Error()) + } + top["Values"] = vals return top, nil } @@ -395,41 +178,35 @@ func istable(v interface{}) bool { // chapter: // one: // title: "Loomings" -func (v Values) PathValue(ypath string) (interface{}, error) { - if len(ypath) == 0 { - return nil, errors.New("YAML path string cannot be zero length") +func (v Values) PathValue(path string) (interface{}, error) { + if path == "" { + return nil, errors.New("YAML path cannot be empty") } - yps := strings.Split(ypath, ".") - if len(yps) == 1 { + return v.pathValue(parsePath(path)) +} + +func (v Values) pathValue(path []string) (interface{}, error) { + if len(path) == 1 { // if exists must be root key not table - vals := v.AsMap() - k := yps[0] - if _, ok := vals[k]; ok && !istable(vals[k]) { - // key found - return vals[yps[0]], nil + if _, ok := v[path[0]]; ok && !istable(v[path[0]]) { + return v[path[0]], nil } - // key not found - return nil, ErrNoValue(fmt.Errorf("%v is not a value", k)) + return nil, ErrNoValue{path[0]} } - // join all elements of YAML path except last to get string table path - ypsLen := len(yps) - table := yps[:ypsLen-1] - st := strings.Join(table, ".") - // get the last element as a string key - key := yps[ypsLen-1:] - sk := string(key[0]) + + key, path := path[len(path)-1], path[:len(path)-1] // get our table for table path - t, err := v.Table(st) + t, err := v.Table(joinPath(path...)) if err != nil { - //no table - return nil, ErrNoValue(fmt.Errorf("%v is not a value", sk)) + return nil, ErrNoValue{key} } // check table for key and ensure value is not a table - if k, ok := t[sk]; ok && !istable(k) { - // key found + if k, ok := t[key]; ok && !istable(k) { return k, nil } - - // key not found - return nil, ErrNoValue(fmt.Errorf("key not found: %s", sk)) + return nil, ErrNoValue{key} } + +func parsePath(key string) []string { return strings.Split(key, ".") } + +func joinPath(path ...string) string { return strings.Join(path, ".") } diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go index d9b03c21a1e..c95fa503a49 100644 --- a/pkg/chartutil/values_test.go +++ b/pkg/chartutil/values_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,17 +18,11 @@ package chartutil import ( "bytes" - "encoding/json" "fmt" "testing" "text/template" - "github.com/golang/protobuf/ptypes/any" - - kversion "k8s.io/apimachinery/pkg/version" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/timeconv" - "k8s.io/helm/pkg/version" + "helm.sh/helm/v3/pkg/chart" ) func TestReadValues(t *testing.T) { @@ -72,52 +66,44 @@ water: } } -func TestToRenderValuesCaps(t *testing.T) { +func TestToRenderValues(t *testing.T) { - chartValues := ` -name: al Rashid -where: - city: Basrah - title: caliph -` - overideValues := ` -name: Haroun -where: - city: Baghdad - date: 809 CE -` + chartValues := map[string]interface{}{ + "name": "al Rashid", + "where": map[string]interface{}{ + "city": "Basrah", + "title": "caliph", + }, + } + + overrideValues := map[string]interface{}{ + "name": "Haroun", + "where": map[string]interface{}{ + "city": "Baghdad", + "date": "809 CE", + }, + } c := &chart.Chart{ Metadata: &chart.Metadata{Name: "test"}, - Templates: []*chart.Template{}, - Values: &chart.Config{Raw: chartValues}, - Dependencies: []*chart.Chart{ - { - Metadata: &chart.Metadata{Name: "where"}, - Values: &chart.Config{Raw: ""}, - }, - }, - Files: []*any.Any{ - {TypeUrl: "scheherazade/shahryar.txt", Value: []byte("1,001 Nights")}, + Templates: []*chart.File{}, + Values: chartValues, + Files: []*chart.File{ + {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, } - v := &chart.Config{Raw: overideValues} + c.AddDependency(&chart.Chart{ + Metadata: &chart.Metadata{Name: "where"}, + }) o := ReleaseOptions{ Name: "Seven Voyages", - Time: timeconv.Now(), - Namespace: "al Basrah", + Namespace: "default", + Revision: 1, IsInstall: true, - Revision: 5, - } - - caps := &Capabilities{ - APIVersions: DefaultVersionSet, - TillerVersion: version.GetVersionProto(), - KubeVersion: &kversion.Info{Major: "1"}, } - res, err := ToRenderValuesCaps(c, v, o, caps) + res, err := ToRenderValues(c, overrideValues, o, nil) if err != nil { t.Fatal(err) } @@ -130,8 +116,11 @@ where: if name := relmap["Name"]; name.(string) != "Seven Voyages" { t.Errorf("Expected release name 'Seven Voyages', got %q", name) } - if rev := relmap["Revision"]; rev.(int) != 5 { - t.Errorf("Expected release revision %d, got %q", 5, rev) + if namespace := relmap["Namespace"]; namespace.(string) != "default" { + t.Errorf("Expected namespace 'default', got %q", namespace) + } + if revision := relmap["Revision"]; revision.(int) != 1 { + t.Errorf("Expected revision '1', got %d", revision) } if relmap["IsUpgrade"].(bool) { t.Error("Expected upgrade to be false.") @@ -139,22 +128,14 @@ where: if !relmap["IsInstall"].(bool) { t.Errorf("Expected install to be true.") } - if data := res["Files"].(Files)["scheherazade/shahryar.txt"]; string(data) != "1,001 Nights" { - t.Errorf("Expected file '1,001 Nights', got %q", string(data)) - } if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") { t.Error("Expected Capabilities to have v1 as an API") } - if res["Capabilities"].(*Capabilities).TillerVersion.SemVer == "" { - t.Error("Expected Capabilities to have a Tiller version") - } if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" { t.Error("Expected Capabilities to have a Kube version") } - var vals Values - vals = res["Values"].(Values) - + vals := res["Values"].(Values) if vals["name"] != "Haroun" { t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals) } @@ -269,158 +250,10 @@ func matchValues(t *testing.T, data map[string]interface{}) { func ttpl(tpl string, v map[string]interface{}) (string, error) { var b bytes.Buffer tt := template.Must(template.New("t").Parse(tpl)) - if err := tt.Execute(&b, v); err != nil { - return "", err - } - return b.String(), nil + err := tt.Execute(&b, v) + return b.String(), err } -// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362 -var testCoalesceValuesYaml = ` -top: yup -bottom: null -right: Null -left: NULL -front: ~ -back: "" - -global: - name: Ishmael - subject: Queequeg - nested: - boat: true - -pequod: - global: - name: Stinky - harpooner: Tashtego - nested: - boat: false - sail: true - ahab: - scope: whale -` - -func TestCoalesceValues(t *testing.T) { - tchart := "testdata/moby" - c, err := LoadDir(tchart) - if err != nil { - t.Fatal(err) - } - - tvals := &chart.Config{Raw: testCoalesceValuesYaml} - - v, err := CoalesceValues(c, tvals) - if err != nil { - t.Fatal(err) - } - j, _ := json.MarshalIndent(v, "", " ") - t.Logf("Coalesced Values: %s", string(j)) - - tests := []struct { - tpl string - expect string - }{ - {"{{.top}}", "yup"}, - {"{{.back}}", ""}, - {"{{.name}}", "moby"}, - {"{{.global.name}}", "Ishmael"}, - {"{{.global.subject}}", "Queequeg"}, - {"{{.global.harpooner}}", ""}, - {"{{.pequod.name}}", "pequod"}, - {"{{.pequod.ahab.name}}", "ahab"}, - {"{{.pequod.ahab.scope}}", "whale"}, - {"{{.pequod.ahab.global.name}}", "Ishmael"}, - {"{{.pequod.ahab.global.subject}}", "Queequeg"}, - {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, - {"{{.pequod.global.name}}", "Ishmael"}, - {"{{.pequod.global.subject}}", "Queequeg"}, - {"{{.spouter.global.name}}", "Ishmael"}, - {"{{.spouter.global.harpooner}}", ""}, - - {"{{.global.nested.boat}}", "true"}, - {"{{.pequod.global.nested.boat}}", "true"}, - {"{{.spouter.global.nested.boat}}", "true"}, - {"{{.pequod.global.nested.sail}}", "true"}, - {"{{.spouter.global.nested.sail}}", ""}, - } - - for _, tt := range tests { - if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect { - t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o) - } - } - - nullKeys := []string{"bottom", "right", "left", "front"} - for _, nullKey := range nullKeys { - if _, ok := v[nullKey]; ok { - t.Errorf("Expected key %q to be removed, still present", nullKey) - } - } -} - -func TestCoalesceTables(t *testing.T) { - dst := map[string]interface{}{ - "name": "Ishmael", - "address": map[string]interface{}{ - "street": "123 Spouter Inn Ct.", - "city": "Nantucket", - }, - "details": map[string]interface{}{ - "friends": []string{"Tashtego"}, - }, - "boat": "pequod", - } - src := map[string]interface{}{ - "occupation": "whaler", - "address": map[string]interface{}{ - "state": "MA", - "street": "234 Spouter Inn Ct.", - }, - "details": "empty", - "boat": map[string]interface{}{ - "mast": true, - }, - } - - // What we expect is that anything in dst overrides anything in src, but that - // otherwise the values are coalesced. - coalesceTables(dst, src) - - if dst["name"] != "Ishmael" { - t.Errorf("Unexpected name: %s", dst["name"]) - } - if dst["occupation"] != "whaler" { - t.Errorf("Unexpected occupation: %s", dst["occupation"]) - } - - addr, ok := dst["address"].(map[string]interface{}) - if !ok { - t.Fatal("Address went away.") - } - - if addr["street"].(string) != "123 Spouter Inn Ct." { - t.Errorf("Unexpected address: %v", addr["street"]) - } - - if addr["city"].(string) != "Nantucket" { - t.Errorf("Unexpected city: %v", addr["city"]) - } - - if addr["state"].(string) != "MA" { - t.Errorf("Unexpected state: %v", addr["state"]) - } - - if det, ok := dst["details"].(map[string]interface{}); !ok { - t.Fatalf("Details is the wrong type: %v", dst["details"]) - } else if _, ok := det["friends"]; !ok { - t.Error("Could not find your friends. Maybe you don't have any. :-(") - } - - if dst["boat"].(string) != "pequod" { - t.Errorf("Expected boat string, got %v", dst["boat"]) - } -} func TestPathValue(t *testing.T) { doc := ` title: "Moby Dick" @@ -442,10 +275,10 @@ chapter: } else if v != "Loomings" { t.Errorf("No error but got wrong value for title: %s\n%v", err, d) } - if _, err := d.PathValue("chapter.one.doesntexist"); err == nil { + if _, err := d.PathValue("chapter.one.doesnotexist"); err == nil { t.Errorf("Non-existent key should return error: %s\n%v", err, d) } - if _, err := d.PathValue("chapter.doesntexist.one"); err == nil { + if _, err := d.PathValue("chapter.doesnotexist.one"); err == nil { t.Errorf("Non-existent key in middle of path should return error: %s\n%v", err, d) } if _, err := d.PathValue(""); err == nil { diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go new file mode 100644 index 00000000000..ee60d981f60 --- /dev/null +++ b/pkg/cli/environment.go @@ -0,0 +1,186 @@ +/* +Copyright The Helm Authors. + +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 cli describes the operating environment for the Helm CLI. + +Helm's environment encapsulates all of the service dependencies Helm has. +These dependencies are expressed as interfaces so that alternate implementations +(mocks, etc.) can be easily generated. +*/ +package cli + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/spf13/pflag" + "k8s.io/cli-runtime/pkg/genericclioptions" + + "helm.sh/helm/v3/pkg/helmpath" +) + +// defaultMaxHistory sets the maximum number of releases to 0: unlimited +const defaultMaxHistory = 10 + +// EnvSettings describes all of the environment settings. +type EnvSettings struct { + namespace string + config *genericclioptions.ConfigFlags + + // KubeConfig is the path to the kubeconfig file + KubeConfig string + // KubeContext is the name of the kubeconfig context. + KubeContext string + // Bearer KubeToken used for authentication + KubeToken string + // Username to impersonate for the operation + KubeAsUser string + // Groups to impersonate for the operation, multiple groups parsed from a comma delimited list + KubeAsGroups []string + // Kubernetes API Server Endpoint for authentication + KubeAPIServer string + // Custom certificate authority file. + KubeCaFile string + // Debug indicates whether or not Helm is running in Debug mode. + Debug bool + // RegistryConfig is the path to the registry config file. + RegistryConfig string + // RepositoryConfig is the path to the repositories file. + RepositoryConfig string + // RepositoryCache is the path to the repository cache directory. + RepositoryCache string + // PluginsDirectory is the path to the plugins directory. + PluginsDirectory string + // MaxHistory is the max release history maintained. + MaxHistory int +} + +func New() *EnvSettings { + env := &EnvSettings{ + namespace: os.Getenv("HELM_NAMESPACE"), + MaxHistory: envIntOr("HELM_MAX_HISTORY", defaultMaxHistory), + KubeContext: os.Getenv("HELM_KUBECONTEXT"), + KubeToken: os.Getenv("HELM_KUBETOKEN"), + KubeAsUser: os.Getenv("HELM_KUBEASUSER"), + KubeAsGroups: envCSV("HELM_KUBEASGROUPS"), + KubeAPIServer: os.Getenv("HELM_KUBEAPISERVER"), + KubeCaFile: os.Getenv("HELM_KUBECAFILE"), + PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")), + RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry.json")), + RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), + RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), + } + env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) + + // bind to kubernetes config flags + env.config = &genericclioptions.ConfigFlags{ + Namespace: &env.namespace, + Context: &env.KubeContext, + BearerToken: &env.KubeToken, + APIServer: &env.KubeAPIServer, + CAFile: &env.KubeCaFile, + KubeConfig: &env.KubeConfig, + Impersonate: &env.KubeAsUser, + ImpersonateGroup: &env.KubeAsGroups, + } + return env +} + +// AddFlags binds flags to the given flagset. +func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { + fs.StringVarP(&s.namespace, "namespace", "n", s.namespace, "namespace scope for this request") + fs.StringVar(&s.KubeConfig, "kubeconfig", "", "path to the kubeconfig file") + fs.StringVar(&s.KubeContext, "kube-context", s.KubeContext, "name of the kubeconfig context to use") + fs.StringVar(&s.KubeToken, "kube-token", s.KubeToken, "bearer token used for authentication") + fs.StringVar(&s.KubeAsUser, "kube-as-user", s.KubeAsUser, "username to impersonate for the operation") + fs.StringArrayVar(&s.KubeAsGroups, "kube-as-group", s.KubeAsGroups, "group to impersonate for the operation, this flag can be repeated to specify multiple groups.") + fs.StringVar(&s.KubeAPIServer, "kube-apiserver", s.KubeAPIServer, "the address and the port for the Kubernetes API server") + fs.StringVar(&s.KubeCaFile, "kube-ca-file", s.KubeCaFile, "the certificate authority file for the Kubernetes API server connection") + fs.BoolVar(&s.Debug, "debug", s.Debug, "enable verbose output") + fs.StringVar(&s.RegistryConfig, "registry-config", s.RegistryConfig, "path to the registry config file") + fs.StringVar(&s.RepositoryConfig, "repository-config", s.RepositoryConfig, "path to the file containing repository names and URLs") + fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the file containing cached repository indexes") +} + +func envOr(name, def string) string { + if v, ok := os.LookupEnv(name); ok { + return v + } + return def +} + +func envIntOr(name string, def int) int { + if name == "" { + return def + } + envVal := envOr(name, strconv.Itoa(def)) + ret, err := strconv.Atoi(envVal) + if err != nil { + return def + } + return ret +} + +func envCSV(name string) (ls []string) { + trimmed := strings.Trim(os.Getenv(name), ", ") + if trimmed != "" { + ls = strings.Split(trimmed, ",") + } + return +} + +func (s *EnvSettings) EnvVars() map[string]string { + envvars := map[string]string{ + "HELM_BIN": os.Args[0], + "HELM_CACHE_HOME": helmpath.CachePath(""), + "HELM_CONFIG_HOME": helmpath.ConfigPath(""), + "HELM_DATA_HOME": helmpath.DataPath(""), + "HELM_DEBUG": fmt.Sprint(s.Debug), + "HELM_PLUGINS": s.PluginsDirectory, + "HELM_REGISTRY_CONFIG": s.RegistryConfig, + "HELM_REPOSITORY_CACHE": s.RepositoryCache, + "HELM_REPOSITORY_CONFIG": s.RepositoryConfig, + "HELM_NAMESPACE": s.Namespace(), + "HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory), + + // broken, these are populated from helm flags and not kubeconfig. + "HELM_KUBECONTEXT": s.KubeContext, + "HELM_KUBETOKEN": s.KubeToken, + "HELM_KUBEASUSER": s.KubeAsUser, + "HELM_KUBEASGROUPS": strings.Join(s.KubeAsGroups, ","), + "HELM_KUBEAPISERVER": s.KubeAPIServer, + "HELM_KUBECAFILE": s.KubeCaFile, + } + if s.KubeConfig != "" { + envvars["KUBECONFIG"] = s.KubeConfig + } + return envvars +} + +// Namespace gets the namespace from the configuration +func (s *EnvSettings) Namespace() string { + if ns, _, err := s.config.ToRawKubeConfigLoader().Namespace(); err == nil { + return ns + } + return "default" +} + +// RESTClientGetter gets the kubeconfig from EnvSettings +func (s *EnvSettings) RESTClientGetter() genericclioptions.RESTClientGetter { + return s.config +} diff --git a/pkg/cli/environment_test.go b/pkg/cli/environment_test.go new file mode 100644 index 00000000000..31ba7a2379b --- /dev/null +++ b/pkg/cli/environment_test.go @@ -0,0 +1,135 @@ +/* +Copyright The Helm Authors. + +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 cli + +import ( + "os" + "reflect" + "strings" + "testing" + + "github.com/spf13/pflag" +) + +func TestEnvSettings(t *testing.T) { + tests := []struct { + name string + + // input + args string + envvars map[string]string + + // expected values + ns, kcontext string + debug bool + maxhistory int + kAsUser string + kAsGroups []string + kCaFile string + }{ + { + name: "defaults", + ns: "default", + maxhistory: defaultMaxHistory, + }, + { + name: "with flags set", + args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/tmp/ca.crt", + ns: "myns", + debug: true, + maxhistory: defaultMaxHistory, + kAsUser: "poro", + kAsGroups: []string{"admins", "teatime", "snackeaters"}, + kCaFile: "/tmp/ca.crt", + }, + { + name: "with envvars set", + envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt"}, + ns: "yourns", + maxhistory: 5, + debug: true, + kAsUser: "pikachu", + kAsGroups: []string{"operators", "snackeaters", "partyanimals"}, + kCaFile: "/tmp/ca.crt", + }, + { + name: "with flags and envvars set", + args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/my/ca.crt", + envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt"}, + ns: "myns", + debug: true, + maxhistory: 5, + kAsUser: "poro", + kAsGroups: []string{"admins", "teatime", "snackeaters"}, + kCaFile: "/my/ca.crt", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer resetEnv()() + + for k, v := range tt.envvars { + os.Setenv(k, v) + } + + flags := pflag.NewFlagSet("testing", pflag.ContinueOnError) + + settings := New() + settings.AddFlags(flags) + flags.Parse(strings.Split(tt.args, " ")) + + if settings.Debug != tt.debug { + t.Errorf("expected debug %t, got %t", tt.debug, settings.Debug) + } + if settings.Namespace() != tt.ns { + t.Errorf("expected namespace %q, got %q", tt.ns, settings.Namespace()) + } + if settings.KubeContext != tt.kcontext { + t.Errorf("expected kube-context %q, got %q", tt.kcontext, settings.KubeContext) + } + if settings.MaxHistory != tt.maxhistory { + t.Errorf("expected maxHistory %d, got %d", tt.maxhistory, settings.MaxHistory) + } + if tt.kAsUser != settings.KubeAsUser { + t.Errorf("expected kAsUser %q, got %q", tt.kAsUser, settings.KubeAsUser) + } + if !reflect.DeepEqual(tt.kAsGroups, settings.KubeAsGroups) { + t.Errorf("expected kAsGroups %+v, got %+v", len(tt.kAsGroups), len(settings.KubeAsGroups)) + } + if tt.kCaFile != settings.KubeCaFile { + t.Errorf("expected kCaFile %q, got %q", tt.kCaFile, settings.KubeCaFile) + } + }) + } +} + +func resetEnv() func() { + origEnv := os.Environ() + + // ensure any local envvars do not hose us + for e := range New().EnvVars() { + os.Unsetenv(e) + } + + return func() { + for _, pair := range origEnv { + kv := strings.SplitN(pair, "=", 2) + os.Setenv(kv[0], kv[1]) + } + } +} diff --git a/pkg/cli/output/output.go b/pkg/cli/output/output.go new file mode 100644 index 00000000000..a46c977ad97 --- /dev/null +++ b/pkg/cli/output/output.go @@ -0,0 +1,140 @@ +/* +Copyright The Helm Authors. + +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 output + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/gosuri/uitable" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" +) + +// Format is a type for capturing supported output formats +type Format string + +const ( + Table Format = "table" + JSON Format = "json" + YAML Format = "yaml" +) + +// Formats returns a list of the string representation of the supported formats +func Formats() []string { + return []string{Table.String(), JSON.String(), YAML.String()} +} + +// FormatsWithDesc returns a list of the string representation of the supported formats +// including a description +func FormatsWithDesc() map[string]string { + return map[string]string{ + Table.String(): "Output result in human-readable format", + JSON.String(): "Output result in JSON format", + YAML.String(): "Output result in YAML format", + } +} + +// ErrInvalidFormatType is returned when an unsupported format type is used +var ErrInvalidFormatType = fmt.Errorf("invalid format type") + +// String returns the string representation of the Format +func (o Format) String() string { + return string(o) +} + +// Write the output in the given format to the io.Writer. Unsupported formats +// will return an error +func (o Format) Write(out io.Writer, w Writer) error { + switch o { + case Table: + return w.WriteTable(out) + case JSON: + return w.WriteJSON(out) + case YAML: + return w.WriteYAML(out) + } + return ErrInvalidFormatType +} + +// ParseFormat takes a raw string and returns the matching Format. +// If the format does not exists, ErrInvalidFormatType is returned +func ParseFormat(s string) (out Format, err error) { + switch s { + case Table.String(): + out, err = Table, nil + case JSON.String(): + out, err = JSON, nil + case YAML.String(): + out, err = YAML, nil + default: + out, err = "", ErrInvalidFormatType + } + return +} + +// Writer is an interface that any type can implement to write supported formats +type Writer interface { + // WriteTable will write tabular output into the given io.Writer, returning + // an error if any occur + WriteTable(out io.Writer) error + // WriteJSON will write JSON formatted output into the given io.Writer, + // returning an error if any occur + WriteJSON(out io.Writer) error + // WriteYAML will write YAML formatted output into the given io.Writer, + // returning an error if any occur + WriteYAML(out io.Writer) error +} + +// EncodeJSON is a helper function to decorate any error message with a bit more +// context and avoid writing the same code over and over for printers. +func EncodeJSON(out io.Writer, obj interface{}) error { + enc := json.NewEncoder(out) + err := enc.Encode(obj) + if err != nil { + return errors.Wrap(err, "unable to write JSON output") + } + return nil +} + +// EncodeYAML is a helper function to decorate any error message with a bit more +// context and avoid writing the same code over and over for printers +func EncodeYAML(out io.Writer, obj interface{}) error { + raw, err := yaml.Marshal(obj) + if err != nil { + return errors.Wrap(err, "unable to write YAML output") + } + + _, err = out.Write(raw) + if err != nil { + return errors.Wrap(err, "unable to write YAML output") + } + return nil +} + +// EncodeTable is a helper function to decorate any error message with a bit +// more context and avoid writing the same code over and over for printers +func EncodeTable(out io.Writer, table *uitable.Table) error { + raw := table.Bytes() + raw = append(raw, []byte("\n")...) + _, err := out.Write(raw) + if err != nil { + return errors.Wrap(err, "unable to write table output") + } + return nil +} diff --git a/pkg/cli/values/options.go b/pkg/cli/values/options.go new file mode 100644 index 00000000000..e6ad7176732 --- /dev/null +++ b/pkg/cli/values/options.go @@ -0,0 +1,121 @@ +/* +Copyright The Helm Authors. + +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 values + +import ( + "io/ioutil" + "net/url" + "os" + "strings" + + "github.com/pkg/errors" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/strvals" +) + +type Options struct { + ValueFiles []string + StringValues []string + Values []string + FileValues []string +} + +// MergeValues merges values from files specified via -f/--values and directly +// via --set, --set-string, or --set-file, marshaling them to YAML +func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, error) { + base := map[string]interface{}{} + + // User specified a values files via -f/--values + for _, filePath := range opts.ValueFiles { + currentMap := map[string]interface{}{} + + bytes, err := readFile(filePath, p) + if err != nil { + return nil, err + } + + if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { + return nil, errors.Wrapf(err, "failed to parse %s", filePath) + } + // Merge with the previous map + base = mergeMaps(base, currentMap) + } + + // User specified a value via --set + for _, value := range opts.Values { + if err := strvals.ParseInto(value, base); err != nil { + return nil, errors.Wrap(err, "failed parsing --set data") + } + } + + // User specified a value via --set-string + for _, value := range opts.StringValues { + if err := strvals.ParseIntoString(value, base); err != nil { + return nil, errors.Wrap(err, "failed parsing --set-string data") + } + } + + // User specified a value via --set-file + for _, value := range opts.FileValues { + reader := func(rs []rune) (interface{}, error) { + bytes, err := readFile(string(rs), p) + return string(bytes), err + } + if err := strvals.ParseIntoFile(value, base, reader); err != nil { + return nil, errors.Wrap(err, "failed parsing --set-file data") + } + } + + return base, nil +} + +func mergeMaps(a, b map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}, len(a)) + for k, v := range a { + out[k] = v + } + for k, v := range b { + if v, ok := v.(map[string]interface{}); ok { + if bv, ok := out[k]; ok { + if bv, ok := bv.(map[string]interface{}); ok { + out[k] = mergeMaps(bv, v) + continue + } + } + } + out[k] = v + } + return out +} + +// readFile load a file from stdin, the local directory, or a remote file with a url. +func readFile(filePath string, p getter.Providers) ([]byte, error) { + if strings.TrimSpace(filePath) == "-" { + return ioutil.ReadAll(os.Stdin) + } + u, _ := url.Parse(filePath) + + // FIXME: maybe someone handle other protocols like ftp. + g, err := p.ByScheme(u.Scheme) + if err != nil { + return ioutil.ReadFile(filePath) + } + data, err := g.Get(filePath, getter.WithURL(filePath)) + return data.Bytes(), err +} diff --git a/pkg/cli/values/options_test.go b/pkg/cli/values/options_test.go new file mode 100644 index 00000000000..d988274bfaf --- /dev/null +++ b/pkg/cli/values/options_test.go @@ -0,0 +1,77 @@ +/* +Copyright The Helm Authors. + +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 values + +import ( + "reflect" + "testing" +) + +func TestMergeValues(t *testing.T) { + nestedMap := map[string]interface{}{ + "foo": "bar", + "baz": map[string]string{ + "cool": "stuff", + }, + } + anotherNestedMap := map[string]interface{}{ + "foo": "bar", + "baz": map[string]string{ + "cool": "things", + "awesome": "stuff", + }, + } + flatMap := map[string]interface{}{ + "foo": "bar", + "baz": "stuff", + } + anotherFlatMap := map[string]interface{}{ + "testing": "fun", + } + + testMap := mergeMaps(flatMap, nestedMap) + equal := reflect.DeepEqual(testMap, nestedMap) + if !equal { + t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap) + } + + testMap = mergeMaps(nestedMap, flatMap) + equal = reflect.DeepEqual(testMap, flatMap) + if !equal { + t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap) + } + + testMap = mergeMaps(nestedMap, anotherNestedMap) + equal = reflect.DeepEqual(testMap, anotherNestedMap) + if !equal { + t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap) + } + + testMap = mergeMaps(anotherFlatMap, anotherNestedMap) + expectedMap := map[string]interface{}{ + "testing": "fun", + "foo": "bar", + "baz": map[string]string{ + "cool": "things", + "awesome": "stuff", + }, + } + equal = reflect.DeepEqual(testMap, expectedMap) + if !equal { + t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) + } +} diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index fe2f3ce92d7..6c600bebb78 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,20 +16,22 @@ limitations under the License. package downloader import ( - "errors" "fmt" "io" - "io/ioutil" "net/url" "os" "path/filepath" "strings" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/provenance" - "k8s.io/helm/pkg/repo" - "k8s.io/helm/pkg/urlutil" + "github.com/pkg/errors" + + "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/internal/fileutil" + "helm.sh/helm/v3/internal/urlutil" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/provenance" + "helm.sh/helm/v3/pkg/repo" ) // VerificationStrategy describes a strategy for determining whether to verify a chart. @@ -63,14 +65,13 @@ type ChartDownloader struct { Verify VerificationStrategy // Keyring is the keyring file used for verification. Keyring string - // HelmHome is the $HELM_HOME. - HelmHome helmpath.Home // Getter collection for the operation Getters getter.Providers - // Chart repository username - Username string - // Chart repository password - Password string + // Options provide parameters to be passed along to the Getter being initialized. + Options []getter.Option + RegistryClient *registry.Client + RepositoryConfig string + RepositoryCache string } // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. @@ -85,35 +86,44 @@ type ChartDownloader struct { // Returns a string path to the location where the file was downloaded and a verification // (if provenance was verified), or an error if something bad happened. func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) { - u, r, g, err := c.ResolveChartVersionAndGetRepo(ref, version) + u, err := c.ResolveChartVersion(ref, version) + if err != nil { + return "", nil, err + } + + g, err := c.Getters.ByScheme(u.Scheme) if err != nil { return "", nil, err } - data, err := g.Get(u.String()) + data, err := g.Get(u.String(), c.Options...) if err != nil { return "", nil, err } name := filepath.Base(u.Path) + if u.Scheme == "oci" { + name = fmt.Sprintf("%s-%s.tgz", name, version) + } + destfile := filepath.Join(dest, name) - if err := ioutil.WriteFile(destfile, data.Bytes(), 0644); err != nil { + if err := fileutil.AtomicWriteFile(destfile, data, 0644); err != nil { return destfile, nil, err } // If provenance is requested, verify it. ver := &provenance.Verification{} if c.Verify > VerifyNever { - body, err := r.Client.Get(u.String() + ".prov") + body, err := g.Get(u.String() + ".prov") if err != nil { if c.Verify == VerifyAlways { - return destfile, ver, fmt.Errorf("Failed to fetch provenance %q", u.String()+".prov") + return destfile, ver, errors.Errorf("failed to fetch provenance %q", u.String()+".prov") } fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err) return destfile, ver, nil } provfile := destfile + ".prov" - if err := ioutil.WriteFile(provfile, body.Bytes(), 0644); err != nil { + if err := fileutil.AtomicWriteFile(provfile, body, 0644); err != nil { return destfile, nil, err } @@ -131,8 +141,8 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven // ResolveChartVersion resolves a chart reference to a URL. // -// It returns the URL as well as a preconfigured repo.Getter that can fetch -// the URL. +// It returns the URL and sets the ChartDownloader's Options that can fetch +// the URL using the appropriate Getter. // // A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path. // @@ -143,29 +153,16 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven // * If version is non-empty, this will return the URL for that version // * If version is empty, this will return the URL for the latest version // * If no version can be found, an error is returned -func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, getter.Getter, error) { - u, r, _, err := c.ResolveChartVersionAndGetRepo(ref, version) - if r != nil { - return u, r.Client, err - } - return u, nil, err -} - -// ResolveChartVersionAndGetRepo is the same as the ResolveChartVersion method, but returns the chart repositoryy. -func (c *ChartDownloader) ResolveChartVersionAndGetRepo(ref, version string) (*url.URL, *repo.ChartRepository, *getter.HttpGetter, error) { +func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) { u, err := url.Parse(ref) if err != nil { - return nil, nil, nil, fmt.Errorf("invalid chart URL format: %s", ref) - } - - rf, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile()) - if err != nil { - return u, nil, nil, err + return nil, errors.Errorf("invalid chart URL format: %s", ref) } + c.Options = append(c.Options, getter.WithURL(ref)) - g, err := getter.NewHTTPGetter(ref, "", "", "") + rf, err := loadRepoConfig(c.RepositoryConfig) if err != nil { - return u, nil, nil, err + return u, err } if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 { @@ -180,23 +177,33 @@ func (c *ChartDownloader) ResolveChartVersionAndGetRepo(ref, version string) (*u // If there is no special config, return the default HTTP client and // swallow the error. if err == ErrNoOwnerRepo { - r := &repo.ChartRepository{} - r.Client = g - g.SetCredentials(c.getRepoCredentials(r)) - return u, r, g, err + return u, nil } - return u, nil, nil, err + return u, err } - r, err := repo.NewChartRepository(rc, c.Getters) + // If we get here, we don't need to go through the next phase of looking - // up the URL. We have it already. So we just return. - return u, r, g, err + // up the URL. We have it already. So we just set the parameters and return. + c.Options = append( + c.Options, + getter.WithURL(rc.URL), + ) + if rc.CertFile != "" || rc.KeyFile != "" || rc.CAFile != "" { + c.Options = append(c.Options, getter.WithTLSClientConfig(rc.CertFile, rc.KeyFile, rc.CAFile)) + } + if rc.Username != "" && rc.Password != "" { + c.Options = append( + c.Options, + getter.WithBasicAuth(rc.Username, rc.Password), + ) + } + return u, nil } // See if it's of the form: repo/path_to_chart p := strings.SplitN(u.Path, "/", 2) if len(p) < 2 { - return u, nil, nil, fmt.Errorf("Non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u) + return u, errors.Errorf("non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u) } repoName := p[0] @@ -204,97 +211,90 @@ func (c *ChartDownloader) ResolveChartVersionAndGetRepo(ref, version string) (*u rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories) if err != nil { - return u, nil, nil, err + return u, err } r, err := repo.NewChartRepository(rc, c.Getters) if err != nil { - return u, nil, nil, err + return u, err + } + + if r != nil && r.Config != nil { + if r.Config.CertFile != "" || r.Config.KeyFile != "" || r.Config.CAFile != "" { + c.Options = append(c.Options, getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile)) + } + if r.Config.Username != "" && r.Config.Password != "" { + c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password)) + } } - g.SetCredentials(c.getRepoCredentials(r)) // Next, we need to load the index, and actually look up the chart. - i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name)) + idxFile := filepath.Join(c.RepositoryCache, helmpath.CacheIndexFile(r.Config.Name)) + i, err := repo.LoadIndexFile(idxFile) if err != nil { - return u, r, g, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) + return u, errors.Wrap(err, "no cached repo found. (try 'helm repo update')") } cv, err := i.Get(chartName, version) if err != nil { - return u, r, g, fmt.Errorf("chart %q matching %s not found in %s index. (try 'helm repo update'). %s", chartName, version, r.Config.Name, err) + return u, errors.Wrapf(err, "chart %q matching %s not found in %s index. (try 'helm repo update')", chartName, version, r.Config.Name) } if len(cv.URLs) == 0 { - return u, r, g, fmt.Errorf("chart %q has no downloadable URLs", ref) + return u, errors.Errorf("chart %q has no downloadable URLs", ref) } // TODO: Seems that picking first URL is not fully correct u, err = url.Parse(cv.URLs[0]) if err != nil { - return u, r, g, fmt.Errorf("invalid chart URL format: %s", ref) + return u, errors.Errorf("invalid chart URL format: %s", ref) } // If the URL is relative (no scheme), prepend the chart repo's base URL if !u.IsAbs() { repoURL, err := url.Parse(rc.URL) if err != nil { - return repoURL, r, nil, err + return repoURL, err } q := repoURL.Query() // We need a trailing slash for ResolveReference to work, but make sure there isn't already one repoURL.Path = strings.TrimSuffix(repoURL.Path, "/") + "/" u = repoURL.ResolveReference(u) u.RawQuery = q.Encode() - g, err := getter.NewHTTPGetter(rc.URL, "", "", "") - if err != nil { - return repoURL, r, nil, err + // TODO add user-agent + if _, err := getter.NewHTTPGetter(getter.WithURL(rc.URL)); err != nil { + return repoURL, err } - g.SetCredentials(c.getRepoCredentials(r)) - return u, r, g, err + return u, err } - return u, r, g, nil -} - -// If this ChartDownloader is not configured to use credentials, and the chart repository sent as an argument is, -// then the repository's configured credentials are returned. -// Else, this ChartDownloader's credentials are returned. -func (c *ChartDownloader) getRepoCredentials(r *repo.ChartRepository) (username, password string) { - username = c.Username - password = c.Password - if r != nil && r.Config != nil { - if username == "" { - username = r.Config.Username - } - if password == "" { - password = r.Config.Password - } - } - return + // TODO add user-agent + return u, nil } // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. // // It assumes that a chart archive file is accompanied by a provenance file whose // name is the archive file name plus the ".prov" extension. -func VerifyChart(path string, keyring string) (*provenance.Verification, error) { +func VerifyChart(path, keyring string) (*provenance.Verification, error) { // For now, error out if it's not a tar file. - if fi, err := os.Stat(path); err != nil { + switch fi, err := os.Stat(path); { + case err != nil: return nil, err - } else if fi.IsDir() { + case fi.IsDir(): return nil, errors.New("unpacked charts cannot be verified") - } else if !isTar(path) { + case !isTar(path): return nil, errors.New("chart must be a tgz file") } provfile := path + ".prov" if _, err := os.Stat(provfile); err != nil { - return nil, fmt.Errorf("could not load provenance file %s: %s", provfile, err) + return nil, errors.Wrapf(err, "could not load provenance file %s", provfile) } sig, err := provenance.NewFromKeyring(keyring, "") if err != nil { - return nil, fmt.Errorf("failed to load keyring: %s", err) + return nil, errors.Wrap(err, "failed to load keyring") } return sig.Verify(path, provfile) } @@ -304,19 +304,19 @@ func VerifyChart(path string, keyring string) (*provenance.Verification, error) // Currently, this simply checks extension, since a subsequent function will // untar the file and validate its binary format. func isTar(filename string) bool { - return strings.ToLower(filepath.Ext(filename)) == ".tgz" + return strings.EqualFold(filepath.Ext(filename), ".tgz") } func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Entry, error) { for _, rc := range cfgs { if rc.Name == name { if rc.URL == "" { - return nil, fmt.Errorf("no URL found for repository %s", name) + return nil, errors.Errorf("no URL found for repository %s", name) } return rc, nil } } - return nil, fmt.Errorf("repo %s not found", name) + return nil, errors.Errorf("repo %s not found", name) } // scanReposForURL scans all repos to find which repo contains the given URL. @@ -337,7 +337,7 @@ func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Ent // The same URL can technically exist in two or more repositories. This algorithm // will return the first one it finds. Order is determined by the order of repositories // in the repositories.yaml file. -func (c *ChartDownloader) scanReposForURL(u string, rf *repo.RepoFile) (*repo.Entry, error) { +func (c *ChartDownloader) scanReposForURL(u string, rf *repo.File) (*repo.Entry, error) { // FIXME: This is far from optimal. Larger installations and index files will // incur a performance hit for this type of scanning. for _, rc := range rf.Repositories { @@ -346,9 +346,10 @@ func (c *ChartDownloader) scanReposForURL(u string, rf *repo.RepoFile) (*repo.En return nil, err } - i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name)) + idxFile := filepath.Join(c.RepositoryCache, helmpath.CacheIndexFile(r.Config.Name)) + i, err := repo.LoadIndexFile(idxFile) if err != nil { - return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) + return nil, errors.Wrap(err, "no cached repo found. (try 'helm repo update')") } for _, entry := range i.Entries { @@ -364,3 +365,11 @@ func (c *ChartDownloader) scanReposForURL(u string, rf *repo.RepoFile) (*repo.En // This means that there is no repo file for the given URL. return nil, ErrNoOwnerRepo } + +func loadRepoConfig(file string) (*repo.File, error) { + r, err := repo.LoadFile(file) + if err != nil && !os.IsNotExist(errors.Cause(err)) { + return nil, err + } + return r, nil +} diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index 80efa77e8c9..334d7aaa1aa 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,20 +16,21 @@ limitations under the License. package downloader import ( - "fmt" - "io/ioutil" "net/http" - "net/http/httptest" - "net/url" "os" "path/filepath" "testing" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/repo" - "k8s.io/helm/pkg/repo/repotest" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo" + "helm.sh/helm/v3/pkg/repo/repotest" +) + +const ( + repoConfig = "testdata/repositories.yaml" + repoCache = "testdata/repository" ) func TestResolveChartRef(t *testing.T) { @@ -55,18 +56,22 @@ func TestResolveChartRef(t *testing.T) { } c := ChartDownloader{ - HelmHome: helmpath.Home("testdata/helmhome"), - Out: os.Stderr, - Getters: getter.All(environment.EnvSettings{}), + Out: os.Stderr, + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + Getters: getter.All(&cli.EnvSettings{ + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + }), } for _, tt := range tests { - u, _, err := c.ResolveChartVersion(tt.ref, tt.version) + u, err := c.ResolveChartVersion(tt.ref, tt.version) if err != nil { if tt.fail { continue } - t.Errorf("%s: failed with error %s", tt.name, err) + t.Errorf("%s: failed with error %q", tt.name, err) continue } if got := u.String(); got != tt.expect { @@ -75,67 +80,76 @@ func TestResolveChartRef(t *testing.T) { } } -func TestVerifyChart(t *testing.T) { - v, err := VerifyChart("testdata/signtest-0.1.0.tgz", "testdata/helm-test-key.pub") - if err != nil { - t.Fatal(err) - } - // The verification is tested at length in the provenance package. Here, - // we just want a quick sanity check that the v is not empty. - if len(v.FileHash) == 0 { - t.Error("Digest missing") +func TestResolveChartOpts(t *testing.T) { + tests := []struct { + name, ref, version string + expect []getter.Option + }{ + { + name: "repo with CA-file", + ref: "testing-ca-file/foo", + expect: []getter.Option{ + getter.WithURL("https://example.com/foo-1.2.3.tgz"), + getter.WithTLSClientConfig("cert", "key", "ca"), + }, + }, } -} - -func TestDownload(t *testing.T) { - expect := "Call me Ishmael" - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, expect) - })) - defer srv.Close() - provider, err := getter.ByScheme("http", environment.EnvSettings{}) - if err != nil { - t.Fatal("No http provider found") + c := ChartDownloader{ + Out: os.Stderr, + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + Getters: getter.All(&cli.EnvSettings{ + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + }), } - g, err := provider.New(srv.URL, "", "", "") - if err != nil { - t.Fatal(err) - } - got, err := g.Get(srv.URL) - if err != nil { - t.Fatal(err) - } + // snapshot options + snapshotOpts := c.Options - if got.String() != expect { - t.Errorf("Expected %q, got %q", expect, got.String()) - } + for _, tt := range tests { + // reset chart downloader options for each test case + c.Options = snapshotOpts - // test with server backed by basic auth - basicAuthSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - username, password, ok := r.BasicAuth() - if !ok || username != "username" || password != "password" { - t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) + expect, err := getter.NewHTTPGetter(tt.expect...) + if err != nil { + t.Errorf("%s: failed to setup http client: %s", tt.name, err) + continue } - fmt.Fprint(w, expect) - })) - defer basicAuthSrv.Close() + u, err := c.ResolveChartVersion(tt.ref, tt.version) + if err != nil { + t.Errorf("%s: failed with error %s", tt.name, err) + continue + } - u, _ := url.ParseRequestURI(basicAuthSrv.URL) - httpgetter, err := getter.NewHTTPGetter(u.String(), "", "", "") - if err != nil { - t.Fatal(err) + got, err := getter.NewHTTPGetter( + append( + c.Options, + getter.WithURL(u.String()), + )..., + ) + if err != nil { + t.Errorf("%s: failed to create http client: %s", tt.name, err) + continue + } + + if *(got.(*getter.HTTPGetter)) != *(expect.(*getter.HTTPGetter)) { + t.Errorf("%s: expected %s, got %s", tt.name, expect, got) + } } - httpgetter.SetCredentials("username", "password") - got, err = httpgetter.Get(u.String()) +} + +func TestVerifyChart(t *testing.T) { + v, err := VerifyChart("testdata/signtest-0.1.0.tgz", "testdata/helm-test-key.pub") if err != nil { t.Fatal(err) } - - if got.String() != expect { - t.Errorf("Expected %q, got %q", expect, got.String()) + // The verification is tested at length in the provenance package. Here, + // we just want a quick sanity check that the v is not empty. + if len(v.FileHash) == 0 { + t.Error("Digest missing") } } @@ -157,53 +171,47 @@ func TestIsTar(t *testing.T) { } func TestDownloadTo(t *testing.T) { - tmp, err := ioutil.TempDir("", "helm-downloadto-") + // Set up a fake repo with basic auth enabled + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") + srv.Stop() if err != nil { t.Fatal(err) } - defer os.RemoveAll(tmp) - - hh := helmpath.Home(tmp) - dest := filepath.Join(hh.String(), "dest") - configDirectories := []string{ - hh.String(), - hh.Repository(), - hh.Cache(), - dest, - } - for _, p := range configDirectories { - if fi, err := os.Stat(p); err != nil { - if err := os.MkdirAll(p, 0755); err != nil { - t.Fatalf("Could not create %s: %s", p, err) - } - } else if !fi.IsDir() { - t.Fatalf("%s must be a directory", p) + srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok || username != "username" || password != "password" { + t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) } - } - - // Set up a fake repo - srv := repotest.NewServer(tmp) + })) + srv.Start() defer srv.Stop() - if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil { - t.Error(err) - return + if err := srv.CreateIndex(); err != nil { + t.Fatal(err) } + if err := srv.LinkIndices(); err != nil { t.Fatal(err) } c := ChartDownloader{ - HelmHome: hh, - Out: os.Stderr, - Verify: VerifyAlways, - Keyring: "testdata/helm-test-key.pub", - Getters: getter.All(environment.EnvSettings{}), + Out: os.Stderr, + Verify: VerifyAlways, + Keyring: "testdata/helm-test-key.pub", + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + Getters: getter.All(&cli.EnvSettings{ + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + }), + Options: []getter.Option{ + getter.WithBasicAuth("username", "password"), + }, } cname := "/signtest-0.1.0.tgz" + dest := srv.Root() where, v, err := c.DownloadTo(srv.URL()+cname, "", dest) if err != nil { - t.Error(err) - return + t.Fatal(err) } if expect := filepath.Join(dest, cname); where != expect { @@ -216,57 +224,91 @@ func TestDownloadTo(t *testing.T) { if _, err := os.Stat(filepath.Join(dest, cname)); err != nil { t.Error(err) - return } } -func TestDownloadTo_VerifyLater(t *testing.T) { - tmp, err := ioutil.TempDir("", "helm-downloadto-") +func TestDownloadTo_TLS(t *testing.T) { + // Set up mock server w/ tls enabled + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") + srv.Stop() + if err != nil { + t.Fatal(err) + } + srv.StartTLS() + defer srv.Stop() + if err := srv.CreateIndex(); err != nil { + t.Fatal(err) + } + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + repoConfig := filepath.Join(srv.Root(), "repositories.yaml") + repoCache := srv.Root() + + c := ChartDownloader{ + Out: os.Stderr, + Verify: VerifyAlways, + Keyring: "testdata/helm-test-key.pub", + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + Getters: getter.All(&cli.EnvSettings{ + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + }), + Options: []getter.Option{}, + } + cname := "test/signtest" + dest := srv.Root() + where, v, err := c.DownloadTo(cname, "", dest) if err != nil { t.Fatal(err) } - defer os.RemoveAll(tmp) - hh := helmpath.Home(tmp) - dest := filepath.Join(hh.String(), "dest") - configDirectories := []string{ - hh.String(), - hh.Repository(), - hh.Cache(), - dest, + target := filepath.Join(dest, "signtest-0.1.0.tgz") + if expect := target; where != expect { + t.Errorf("Expected download to %s, got %s", expect, where) } - for _, p := range configDirectories { - if fi, err := os.Stat(p); err != nil { - if err := os.MkdirAll(p, 0755); err != nil { - t.Fatalf("Could not create %s: %s", p, err) - } - } else if !fi.IsDir() { - t.Fatalf("%s must be a directory", p) - } + + if v.FileHash == "" { + t.Error("File hash was empty, but verification is required.") } - // Set up a fake repo - srv := repotest.NewServer(tmp) - defer srv.Stop() - if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil { + if _, err := os.Stat(target); err != nil { t.Error(err) - return } +} + +func TestDownloadTo_VerifyLater(t *testing.T) { + defer ensure.HelmHome(t)() + + dest := ensure.TempDir(t) + defer os.RemoveAll(dest) + + // Set up a fake repo + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() if err := srv.LinkIndices(); err != nil { t.Fatal(err) } c := ChartDownloader{ - HelmHome: hh, - Out: os.Stderr, - Verify: VerifyLater, - Getters: getter.All(environment.EnvSettings{}), + Out: os.Stderr, + Verify: VerifyLater, + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + Getters: getter.All(&cli.EnvSettings{ + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + }), } cname := "/signtest-0.1.0.tgz" where, _, err := c.DownloadTo(srv.URL()+cname, "", dest) if err != nil { - t.Error(err) - return + t.Fatal(err) } if expect := filepath.Join(dest, cname); where != expect { @@ -274,26 +316,27 @@ func TestDownloadTo_VerifyLater(t *testing.T) { } if _, err := os.Stat(filepath.Join(dest, cname)); err != nil { - t.Error(err) - return + t.Fatal(err) } if _, err := os.Stat(filepath.Join(dest, cname+".prov")); err != nil { - t.Error(err) - return + t.Fatal(err) } } func TestScanReposForURL(t *testing.T) { - hh := helmpath.Home("testdata/helmhome") c := ChartDownloader{ - HelmHome: hh, - Out: os.Stderr, - Verify: VerifyLater, - Getters: getter.All(environment.EnvSettings{}), + Out: os.Stderr, + Verify: VerifyLater, + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + Getters: getter.All(&cli.EnvSettings{ + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + }), } u := "http://example.com/alpine-0.2.0.tgz" - rf, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile()) + rf, err := repo.LoadFile(repoConfig) if err != nil { t.Fatal(err) } diff --git a/pkg/downloader/doc.go b/pkg/downloader/doc.go index fb54936b867..9588a7dfe1d 100644 --- a/pkg/downloader/doc.go +++ b/pkg/downloader/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,8 +16,8 @@ limitations under the License. /*Package downloader provides a library for downloading charts. This package contains various tools for downloading charts from repository -servers, and then storing them in Helm-specific directory structures (like -HELM_HOME). This library contains many functions that depend on a specific +servers, and then storing them in Helm-specific directory structures. This +library contains many functions that depend on a specific filesystem layout. */ package downloader diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 89a839b54aa..e89ac7c0242 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,37 +16,53 @@ limitations under the License. package downloader import ( - "errors" + "crypto" + "encoding/hex" "fmt" "io" "io/ioutil" + "log" "net/url" "os" "path" "path/filepath" + "regexp" "strings" "sync" - "github.com/Masterminds/semver" - "github.com/ghodss/yaml" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/repo" - "k8s.io/helm/pkg/resolver" - "k8s.io/helm/pkg/urlutil" + "github.com/Masterminds/semver/v3" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/internal/resolver" + "helm.sh/helm/v3/internal/third_party/dep/fs" + "helm.sh/helm/v3/internal/urlutil" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/repo" ) +// ErrRepoNotFound indicates that chart repositories can't be found in local repo cache. +// The value of Repos is missing repos. +type ErrRepoNotFound struct { + Repos []string +} + +// Error implements the error interface. +func (e ErrRepoNotFound) Error() string { + return fmt.Sprintf("no repository definition for %s", strings.Join(e.Repos, ", ")) +} + // Manager handles the lifecycle of fetching, resolving, and storing dependencies. type Manager struct { // Out is used to print warnings and notifications. Out io.Writer // ChartPath is the path to the unpacked base chart upon which this operates. ChartPath string - // HelmHome is the $HELM_HOME directory - HelmHome helmpath.Home // Verification indicates whether the chart should be verified. Verify VerificationStrategy // Debug is the global "--debug" flag @@ -56,7 +72,10 @@ type Manager struct { // SkipUpdate indicates that the repository should not be updated first. SkipUpdate bool // Getter collection for the operation - Getters []getter.Provider + Getters []getter.Provider + RegistryClient *registry.Client + RepositoryConfig string + RepositoryCache string } // Build rebuilds a local charts directory from a lockfile. @@ -72,18 +91,42 @@ func (m *Manager) Build() error { // If a lock file is found, run a build from that. Otherwise, just do // an update. - lock, err := chartutil.LoadRequirementsLock(c) - if err != nil { + lock := c.Lock + if lock == nil { return m.Update() } - // A lock must accompany a requirements.yaml file. - req, err := chartutil.LoadRequirements(c) - if err != nil { - return fmt.Errorf("requirements.yaml cannot be opened: %s", err) + // Check that all of the repos we're dependent on actually exist. + req := c.Metadata.Dependencies + + // If using apiVersion v1, calculate the hash before resolve repo names + // because resolveRepoNames will change req if req uses repo alias + // and Helm 2 calculate the digest from the original req + // Fix for: https://github.com/helm/helm/issues/7619 + var v2Sum string + if c.Metadata.APIVersion == chart.APIVersionV1 { + v2Sum, err = resolver.HashV2Req(req) + if err != nil { + return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies") + } } - if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest { - return fmt.Errorf("requirements.lock is out of sync with requirements.yaml") + + if _, err := m.resolveRepoNames(req); err != nil { + return err + } + + if sum, err := resolver.HashReq(req, lock.Dependencies); err != nil || sum != lock.Digest { + // If lock digest differs and chart is apiVersion v1, it maybe because the lock was built + // with Helm 2 and therefore should be checked with Helm v2 hash + // Fix for: https://github.com/helm/helm/issues/7233 + if c.Metadata.APIVersion == chart.APIVersionV1 { + log.Println("warning: a valid Helm v3 hash was not found. Checking against Helm v2 hash...") + if v2Sum != lock.Digest { + return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies") + } + } else { + return errors.New("the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies") + } } // Check that all of the repos we're dependent on actually exist. @@ -99,16 +142,12 @@ func (m *Manager) Build() error { } // Now we need to fetch every package here into charts/ - if err := m.downloadAll(lock.Dependencies); err != nil { - return err - } - - return nil + return m.downloadAll(lock.Dependencies) } // Update updates a local charts directory. // -// It first reads the requirements.yaml file, and then attempts to +// It first reads the Chart.yaml file, and then attempts to // negotiate versions based on that. It will download the versions // from remote chart repositories unless SkipUpdate is true. func (m *Manager) Update() error { @@ -117,31 +156,34 @@ func (m *Manager) Update() error { return err } - // If no requirements file is found, we consider this a successful + // If no dependencies are found, we consider this a successful // completion. - req, err := chartutil.LoadRequirements(c) - if err != nil { - if err == chartutil.ErrRequirementsNotFound { - fmt.Fprintf(m.Out, "No requirements found in %s/charts.\n", m.ChartPath) - return nil - } - return err + req := c.Metadata.Dependencies + if req == nil { + return nil } - // Hash requirements.yaml - hash, err := resolver.HashReq(req) + // Get the names of the repositories the dependencies need that Helm is + // configured to know about. + repoNames, err := m.resolveRepoNames(req) if err != nil { return err } - // Check that all of the repos we're dependent on actually exist and - // the repo index names. - repoNames, err := m.getRepoNames(req.Dependencies) + // For the repositories Helm is not configured to know about, ensure Helm + // has some information about them and, when possible, the index files + // locally. + // TODO(mattfarina): Repositories should be explicitly added by end users + // rather than automattic. In Helm v4 require users to add repositories. They + // should have to add them in order to make sure they are aware of the + // repositories and opt-in to any locations, for security. + repoNames, err = m.ensureMissingRepos(repoNames, req) if err != nil { return err } - // For each repo in the file, update the cached copy of that repo + // For each of the repositories Helm is configured to know about, update + // the index information locally. if !m.SkipUpdate { if err := m.UpdateRepositories(); err != nil { return err @@ -149,8 +191,8 @@ func (m *Manager) Update() error { } // Now we need to find out which version of a chart best satisfies the - // requirements the requirements.yaml - lock, err := m.resolve(req, repoNames, hash) + // dependencies in the Chart.yaml + lock, err := m.resolve(req, repoNames) if err != nil { return err } @@ -160,38 +202,45 @@ func (m *Manager) Update() error { return err } + // downloadAll might overwrite dependency version, recalculate lock digest + newDigest, err := resolver.HashReq(req, lock.Dependencies) + if err != nil { + return err + } + lock.Digest = newDigest + // If the lock file hasn't changed, don't write a new one. - oldLock, err := chartutil.LoadRequirementsLock(c) - if err == nil && oldLock.Digest == lock.Digest { + oldLock := c.Lock + if oldLock != nil && oldLock.Digest == lock.Digest { return nil } // Finally, we need to write the lockfile. - return writeLock(m.ChartPath, lock) + return writeLock(m.ChartPath, lock, c.Metadata.APIVersion == chart.APIVersionV1) } func (m *Manager) loadChartDir() (*chart.Chart, error) { if fi, err := os.Stat(m.ChartPath); err != nil { - return nil, fmt.Errorf("could not find %s: %s", m.ChartPath, err) + return nil, errors.Wrapf(err, "could not find %s", m.ChartPath) } else if !fi.IsDir() { return nil, errors.New("only unpacked charts can be updated") } - return chartutil.LoadDir(m.ChartPath) + return loader.LoadDir(m.ChartPath) } -// resolve takes a list of requirements and translates them into an exact version to download. +// resolve takes a list of dependencies and translates them into an exact version to download. // -// This returns a lock file, which has all of the requirements normalized to a specific version. -func (m *Manager) resolve(req *chartutil.Requirements, repoNames map[string]string, hash string) (*chartutil.RequirementsLock, error) { - res := resolver.New(m.ChartPath, m.HelmHome) - return res.Resolve(req, repoNames, hash) +// This returns a lock file, which has all of the dependencies normalized to a specific version. +func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) { + res := resolver.New(m.ChartPath, m.RepositoryCache) + return res.Resolve(req, repoNames) } // downloadAll takes a list of dependencies and downloads them into charts/ // // It will delete versions of the chart that exist on disk and might cause // a conflict. -func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { +func (m *Manager) downloadAll(deps []*chart.Dependency) error { repos, err := m.loadChartRepositories() if err != nil { return err @@ -206,11 +255,11 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { return err } } else if !fi.IsDir() { - return fmt.Errorf("%q is not a directory", destPath) + return errors.Errorf("%q is not a directory", destPath) } - if err := os.Rename(destPath, tmpPath); err != nil { - return fmt.Errorf("Unable to move current charts to tmp dir: %v", err) + if err := fs.RenameWithFallback(destPath, tmpPath); err != nil { + return errors.Wrap(err, "unable to move current charts to tmp dir") } if err := os.MkdirAll(destPath, 0755); err != nil { @@ -219,7 +268,33 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) var saveError error + churls := make(map[string]struct{}) for _, dep := range deps { + // No repository means the chart is in charts directory + if dep.Repository == "" { + fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name) + chartPath := filepath.Join(tmpPath, dep.Name) + ch, err := loader.LoadDir(chartPath) + if err != nil { + return fmt.Errorf("Unable to load chart: %v", err) + } + + constraint, err := semver.NewConstraint(dep.Version) + if err != nil { + return fmt.Errorf("Dependency %s has an invalid version/constraint format: %s", dep.Name, err) + } + + v, err := semver.NewVersion(ch.Metadata.Version) + if err != nil { + return fmt.Errorf("Invalid version %s for dependency %s: %s", dep.Version, dep.Name, err) + } + + if !constraint.Check(v) { + saveError = fmt.Errorf("Dependency %s at version %s does not satisfy the constraint %s", dep.Name, ch.Metadata.Version, dep.Version) + break + } + continue + } if strings.HasPrefix(dep.Repository, "file://") { if m.Debug { fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) @@ -233,44 +308,73 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { continue } - fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) - // Any failure to resolve/download a chart should fail: - // https://github.com/kubernetes/helm/issues/1439 - churl, username, password, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos) + // https://github.com/helm/helm/issues/1439 + churl, username, password, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) if err != nil { - saveError = fmt.Errorf("could not find %s: %s", churl, err) + saveError = errors.Wrapf(err, "could not find %s", churl) break } + if _, ok := churls[churl]; ok { + fmt.Fprintf(m.Out, "Already downloaded %s from repo %s\n", dep.Name, dep.Repository) + continue + } + + fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) + dl := ChartDownloader{ - Out: m.Out, - Verify: m.Verify, - Keyring: m.Keyring, - HelmHome: m.HelmHome, - Getters: m.Getters, - Username: username, - Password: password, + Out: m.Out, + Verify: m.Verify, + Keyring: m.Keyring, + RepositoryConfig: m.RepositoryConfig, + RepositoryCache: m.RepositoryCache, + Getters: m.Getters, + Options: []getter.Option{ + getter.WithBasicAuth(username, password), + }, + } + + version := "" + if strings.HasPrefix(churl, "oci://") { + if !resolver.FeatureGateOCI.IsEnabled() { + return errors.Wrapf(resolver.FeatureGateOCI.Error(), + "the repository %s is an OCI registry", churl) + } + + churl, version, err = parseOCIRef(churl) + if err != nil { + return errors.Wrapf(err, "could not parse OCI reference") + } + dl.Options = append(dl.Options, + getter.WithRegistryClient(m.RegistryClient), + getter.WithTagName(version)) } - if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil { - saveError = fmt.Errorf("could not download %s: %s", churl, err) + _, _, err = dl.DownloadTo(churl, version, destPath) + if err != nil { + saveError = errors.Wrapf(err, "could not download %s", churl) break } + + churls[churl] = struct{}{} } if saveError == nil { fmt.Fprintln(m.Out, "Deleting outdated charts") for _, dep := range deps { - if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil { - return err + // Chart from local charts directory stays in place + if dep.Repository != "" { + if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil { + 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) + return errors.Wrapf(err, "failed to remove %v", tmpPath) } } else { fmt.Fprintln(m.Out, "Save error occurred: ", saveError) @@ -281,16 +385,28 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { } } if err := os.RemoveAll(destPath); err != nil { - return fmt.Errorf("Failed to remove %v: %v", destPath, err) + return errors.Wrapf(err, "failed to remove %v", destPath) } - if err := os.Rename(tmpPath, destPath); err != nil { - return fmt.Errorf("Unable to move current charts to tmp dir: %v", err) + if err := fs.RenameWithFallback(tmpPath, destPath); err != nil { + return errors.Wrap(err, "unable to move current charts to tmp dir") } return saveError } return nil } +func parseOCIRef(chartRef string) (string, string, error) { + refTagRegexp := regexp.MustCompile(`^(oci://[^:]+(:[0-9]{1,5})?[^:]+):(.*)$`) + caps := refTagRegexp.FindStringSubmatch(chartRef) + if len(caps) != 4 { + return "", "", errors.Errorf("improperly formatted oci chart reference: %s", chartRef) + } + chartRef = caps[1] + tag := caps[3] + + return chartRef, tag, nil +} + // safeDeleteDep deletes any versions of the given dependency in the given directory. // // It does this by first matching the file name to an expected pattern, then loading @@ -307,12 +423,12 @@ func (m *Manager) safeDeleteDep(name, dir string) error { return err } for _, fname := range files { - ch, err := chartutil.LoadFile(fname) + ch, err := loader.LoadFile(fname) if err != nil { fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err) continue } - if ch.Metadata.Name != name { + if ch.Name() != name { // This is not the file you are looking for. continue } @@ -325,8 +441,8 @@ func (m *Manager) safeDeleteDep(name, dir string) error { } // hasAllRepos ensures that all of the referenced deps are in the local repo cache. -func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error { - rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) +func (m *Manager) hasAllRepos(deps []*chart.Dependency) error { + rf, err := loadRepoConfig(m.RepositoryConfig) if err != nil { return err } @@ -335,36 +451,99 @@ func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error { // Verify that all repositories referenced in the deps are actually known // by Helm. missing := []string{} +Loop: for _, dd := range deps { - // If repo is from local path, continue - if strings.HasPrefix(dd.Repository, "file://") { + // If repo is from local path or OCI, continue + if strings.HasPrefix(dd.Repository, "file://") || strings.HasPrefix(dd.Repository, "oci://") { continue } - found := false if dd.Repository == "" { - found = true - } else { - for _, repo := range repos { - if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { - found = true - } - } + continue } - if !found { - missing = append(missing, dd.Repository) + for _, repo := range repos { + if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { + continue Loop + } } + missing = append(missing, dd.Repository) } if len(missing) > 0 { - return fmt.Errorf("no repository definition for %s. Please add the missing repos via 'helm repo add'", strings.Join(missing, ", ")) + return ErrRepoNotFound{missing} } return nil } -// getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file. -func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, error) { - rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) +// ensureMissingRepos attempts to ensure the repository information for repos +// not managed by Helm is present. This takes in the repoNames Helm is configured +// to work with along with the chart dependencies. It will find the deps not +// in a known repo and attempt to ensure the data is present for steps like +// version resolution. +func (m *Manager) ensureMissingRepos(repoNames map[string]string, deps []*chart.Dependency) (map[string]string, error) { + + var ru []*repo.Entry + + for _, dd := range deps { + + // If the chart is in the local charts directory no repository needs + // to be specified. + if dd.Repository == "" { + continue + } + + // When the repoName for a dependency is known we can skip ensuring + if _, ok := repoNames[dd.Name]; ok { + continue + } + + // The generated repository name, which will result in an index being + // locally cached, has a name pattern of "helm-manager-" followed by a + // sha256 of the repo name. This assumes end users will never create + // repositories with these names pointing to other repositories. Using + // this method of naming allows the existing repository pulling and + // resolution code to do most of the work. + rn, err := key(dd.Repository) + if err != nil { + return repoNames, err + } + rn = managerKeyPrefix + rn + + repoNames[dd.Name] = rn + + // Assuming the repository is generally available. For Helm managed + // access controls the repository needs to be added through the user + // managed system. This path will work for public charts, like those + // supplied by Bitnami, but not for protected charts, like corp ones + // behind a username and pass. + ri := &repo.Entry{ + Name: rn, + URL: dd.Repository, + } + ru = append(ru, ri) + } + + // Calls to UpdateRepositories (a public function) will only update + // repositories configured by the user. Here we update repos found in + // the dependencies that are not known to the user if update skipping + // is not configured. + if !m.SkipUpdate && len(ru) > 0 { + fmt.Fprintln(m.Out, "Getting updates for unmanaged Helm repositories...") + if err := m.parallelRepoUpdate(ru); err != nil { + return repoNames, err + } + } + + return repoNames, nil +} + +// resolveRepoNames returns the repo names of the referenced deps which can be used to fetch the cached index file +// and replaces aliased repository URLs into resolved URLs in dependencies. +func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string, error) { + rf, err := loadRepoConfig(m.RepositoryConfig) if err != nil { + if os.IsNotExist(err) { + return make(map[string]string), nil + } return nil, err } repos := rf.Repositories @@ -375,6 +554,11 @@ func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, // by Helm. missing := []string{} for _, dd := range deps { + // Don't map the repository, we don't need to download chart from charts directory + // When OCI is used there is no Helm repository + if dd.Repository == "" || strings.HasPrefix(dd.Repository, "oci://") { + continue + } // if dep chart is from local path, verify the path is valid if strings.HasPrefix(dd.Repository, "file://") { if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil { @@ -388,6 +572,11 @@ func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, continue } + if strings.HasPrefix(dd.Repository, "oci://") { + reposMap[dd.Name] = dd.Repository + continue + } + found := false for _, repo := range repos { @@ -404,51 +593,57 @@ func (m *Manager) getRepoNames(deps []*chartutil.Dependency) (map[string]string, } } if !found { - missing = append(missing, dd.Repository) + repository := dd.Repository + // Add if URL + _, err := url.ParseRequestURI(repository) + if err == nil { + reposMap[repository] = repository + continue + } + missing = append(missing, repository) } } if len(missing) > 0 { - if len(missing) > 0 { - errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", ")) - // It is common for people to try to enter "stable" as a repository instead of the actual URL. - // For this case, let's give them a suggestion. - containsNonURL := false - for _, repo := range missing { - if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") { - containsNonURL = true - } - } - if containsNonURL { - errorMessage += ` -Note that repositories must be URLs or aliases. For example, to refer to the stable -repository, use "https://kubernetes-charts.storage.googleapis.com/" or "@stable" instead of -"stable". Don't forget to add the repo, too ('helm repo add').` + errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", ")) + // It is common for people to try to enter "stable" as a repository instead of the actual URL. + // For this case, let's give them a suggestion. + containsNonURL := false + for _, repo := range missing { + if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") { + containsNonURL = true } - return nil, errors.New(errorMessage) } + if containsNonURL { + errorMessage += ` +Note that repositories must be URLs or aliases. For example, to refer to the "example" +repository, use "https://charts.example.com/" or "@example" instead of +"example". Don't forget to add the repo, too ('helm repo add').` + } + return nil, errors.New(errorMessage) } return reposMap, nil } // UpdateRepositories updates all of the local repos to the latest. func (m *Manager) UpdateRepositories() error { - rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) + rf, err := loadRepoConfig(m.RepositoryConfig) if err != nil { return err } repos := rf.Repositories if len(repos) > 0 { + fmt.Fprintln(m.Out, "Hang tight while we grab the latest from your chart repositories...") // This prints warnings straight to out. if err := m.parallelRepoUpdate(repos); err != nil { return err } + fmt.Fprintln(m.Out, "Update Complete. ⎈Happy Helming!⎈") } return nil } func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { - out := m.Out - fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") + var wg sync.WaitGroup for _, c := range repos { r, err := repo.NewChartRepository(c, m.Getters) @@ -457,16 +652,28 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { } wg.Add(1) go func(r *repo.ChartRepository) { - if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil { - fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", r.Config.Name, r.Config.URL, err) + if _, err := r.DownloadIndexFile(); err != nil { + // For those dependencies that are not known to helm and using a + // generated key name we display the repo url. + if strings.HasPrefix(r.Config.Name, managerKeyPrefix) { + fmt.Fprintf(m.Out, "...Unable to get an update from the %q chart repository:\n\t%s\n", r.Config.URL, err) + } else { + fmt.Fprintf(m.Out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", r.Config.Name, r.Config.URL, err) + } } else { - fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name) + // For those dependencies that are not known to helm and using a + // generated key name we display the repo url. + if strings.HasPrefix(r.Config.Name, managerKeyPrefix) { + fmt.Fprintf(m.Out, "...Successfully got an update from the %q chart repository\n", r.Config.URL) + } else { + fmt.Fprintf(m.Out, "...Successfully got an update from the %q chart repository\n", r.Config.Name) + } } wg.Done() }(r) } wg.Wait() - fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") + return nil } @@ -478,8 +685,13 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { // repoURL is the repository to search // // If it finds a URL that is "relative", it will prepend the repoURL. -func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) { +func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) { + if strings.HasPrefix(repoURL, "oci://") { + return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", nil + } + for _, cr := range repos { + if urlutil.Equal(repoURL, cr.Config.URL) { var entry repo.ChartVersions entry, err = findEntryByName(name, cr) @@ -500,8 +712,12 @@ func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRep return } } - err = fmt.Errorf("chart %s not found in %s", name, repoURL) - return + url, err = repo.FindChartInRepoURL(repoURL, name, version, "", "", "", m.Getters) + if err == nil { + return url, username, password, err + } + err = errors.Errorf("chart %s not found in %s: %s", name, repoURL, err) + return url, username, password, err } // findEntryByName finds an entry in the chart repository whose name matches the given name. @@ -556,7 +772,7 @@ func normalizeURL(baseURL, urlOrPath string) (string, error) { } u2, err := url.Parse(baseURL) if err != nil { - return urlOrPath, fmt.Errorf("Base URL failed to parse: %s", err) + return urlOrPath, errors.Wrap(err, "base URL failed to parse") } u2.Path = path.Join(u2.Path, urlOrPath) @@ -569,18 +785,17 @@ func normalizeURL(baseURL, urlOrPath string) (string, error) { // The key is the local name (which is only present in the repositories.yaml). func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) { indices := map[string]*repo.ChartRepository{} - repoyaml := m.HelmHome.RepositoryFile() // Load repositories.yaml file - rf, err := repo.LoadRepositoriesFile(repoyaml) + rf, err := loadRepoConfig(m.RepositoryConfig) if err != nil { - return indices, fmt.Errorf("failed to load %s: %s", repoyaml, err) + return indices, errors.Wrapf(err, "failed to load %s", m.RepositoryConfig) } for _, re := range rf.Repositories { lname := re.Name - cacheindex := m.HelmHome.CacheIndex(lname) - index, err := repo.LoadIndexFile(cacheindex) + idxFile := filepath.Join(m.RepositoryCache, helmpath.CacheIndexFile(lname)) + index, err := repo.LoadIndexFile(idxFile) if err != nil { return indices, err } @@ -596,21 +811,25 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err } // writeLock writes a lockfile to disk -func writeLock(chartpath string, lock *chartutil.RequirementsLock) error { +func writeLock(chartpath string, lock *chart.Lock, legacyLockfile bool) error { data, err := yaml.Marshal(lock) if err != nil { return err } - dest := filepath.Join(chartpath, "requirements.lock") + lockfileName := "Chart.lock" + if legacyLockfile { + lockfileName = "requirements.lock" + } + dest := filepath.Join(chartpath, lockfileName) return ioutil.WriteFile(dest, data, 0644) } // archive a dep chart from local directory and save it into charts/ -func tarFromLocalDir(chartpath string, name string, repo string, version string) (string, error) { +func tarFromLocalDir(chartpath, name, repo, version string) (string, error) { destPath := filepath.Join(chartpath, "charts") if !strings.HasPrefix(repo, "file://") { - return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo) + return "", errors.Errorf("wrong format: chart %s repository %s", name, repo) } origPath, err := resolver.GetLocalPath(repo, chartpath) @@ -618,14 +837,14 @@ func tarFromLocalDir(chartpath string, name string, repo string, version string) return "", err } - ch, err := chartutil.LoadDir(origPath) + ch, err := loader.LoadDir(origPath) if err != nil { return "", err } constraint, err := semver.NewConstraint(version) if err != nil { - return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %s", name, err) + return "", errors.Wrapf(err, "dependency %s has an invalid version/constraint format", name) } v, err := semver.NewVersion(ch.Metadata.Version) @@ -638,7 +857,7 @@ func tarFromLocalDir(chartpath string, name string, repo string, version string) return ch.Metadata.Version, err } - return "", fmt.Errorf("can't get a valid version for dependency %s", name) + return "", errors.Errorf("can't get a valid version for dependency %s", name) } // move files from tmppath to destpath @@ -648,9 +867,24 @@ func move(tmpPath, destPath string) error { 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) + if err := fs.RenameWithFallback(tmpfile, destfile); err != nil { + return errors.Wrap(err, "unable to move local charts to charts dir") } } return nil } + +// The prefix to use for cache keys created by the manager for repo names +const managerKeyPrefix = "helm-manager-" + +// key is used to turn a name, such as a repository url, into a filesystem +// safe name that is unique for querying. To accomplish this a unique hash of +// the string is used. +func key(name string) (string, error) { + in := strings.NewReader(name) + hash := crypto.SHA256.New() + if _, err := io.Copy(hash, in); err != nil { + return "", nil + } + return hex.EncodeToString(hash.Sum(nil)), nil +} diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go index 1ff2a9c173c..fc8d9abb2e3 100644 --- a/pkg/downloader/manager_test.go +++ b/pkg/downloader/manager_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -17,11 +17,14 @@ package downloader import ( "bytes" + "path/filepath" "reflect" "testing" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/helm/helmpath" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/repo/repotest" ) func TestVersionEquals(t *testing.T) { @@ -63,10 +66,11 @@ func TestNormalizeURL(t *testing.T) { } func TestFindChartURL(t *testing.T) { - b := bytes.NewBuffer(nil) + var b bytes.Buffer m := &Manager{ - Out: b, - HelmHome: helmpath.Home("testdata/helmhome"), + Out: &b, + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, } repos, err := m.loadChartRepositories() if err != nil { @@ -77,11 +81,11 @@ func TestFindChartURL(t *testing.T) { version := "0.1.0" repoURL := "http://example.com/charts" - churl, username, password, err := findChartURL(name, version, repoURL, repos) + churl, username, password, err := m.findChartURL(name, version, repoURL, repos) if err != nil { t.Fatal(err) } - if churl != "https://kubernetes-charts.storage.googleapis.com/alpine-0.1.0.tgz" { + if churl != "https://charts.helm.sh/stable/alpine-0.1.0.tgz" { t.Errorf("Unexpected URL %q", churl) } if username != "" { @@ -95,61 +99,69 @@ func TestFindChartURL(t *testing.T) { func TestGetRepoNames(t *testing.T) { b := bytes.NewBuffer(nil) m := &Manager{ - Out: b, - HelmHome: helmpath.Home("testdata/helmhome"), + Out: b, + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, } tests := []struct { name string - req []*chartutil.Dependency + req []*chart.Dependency expect map[string]string err bool }{ { - name: "no repo definition failure", - req: []*chartutil.Dependency{ + name: "no repo definition, but references a url", + req: []*chart.Dependency{ {Name: "oedipus-rex", Repository: "http://example.com/test"}, }, - err: true, + expect: map[string]string{"http://example.com/test": "http://example.com/test"}, }, { name: "no repo definition failure -- stable repo", - req: []*chartutil.Dependency{ + req: []*chart.Dependency{ {Name: "oedipus-rex", Repository: "stable"}, }, err: true, }, { name: "no repo definition failure", - req: []*chartutil.Dependency{ + req: []*chart.Dependency{ {Name: "oedipus-rex", Repository: "http://example.com"}, }, expect: map[string]string{"oedipus-rex": "testing"}, }, { name: "repo from local path", - req: []*chartutil.Dependency{ + req: []*chart.Dependency{ {Name: "local-dep", Repository: "file://./testdata/signtest"}, }, expect: map[string]string{"local-dep": "file://./testdata/signtest"}, }, { name: "repo alias (alias:)", - req: []*chartutil.Dependency{ + req: []*chart.Dependency{ {Name: "oedipus-rex", Repository: "alias:testing"}, }, expect: map[string]string{"oedipus-rex": "testing"}, }, { name: "repo alias (@)", - req: []*chartutil.Dependency{ + req: []*chart.Dependency{ {Name: "oedipus-rex", Repository: "@testing"}, }, expect: map[string]string{"oedipus-rex": "testing"}, }, + { + name: "repo from local chart under charts path", + req: []*chart.Dependency{ + {Name: "local-subchart", Repository: ""}, + }, + expect: map[string]string{}, + }, } for _, tt := range tests { - l, err := m.getRepoNames(tt.req) + l, err := m.resolveRepoNames(tt.req) if err != nil { if tt.err { continue @@ -168,3 +180,312 @@ func TestGetRepoNames(t *testing.T) { } } } + +func TestUpdateBeforeBuild(t *testing.T) { + // Set up a fake repo + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + dir := func(p ...string) string { + return filepath.Join(append([]string{srv.Root()}, p...)...) + } + + // Save dep + d := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "dep-chart", + Version: "0.1.0", + APIVersion: "v1", + }, + } + if err := chartutil.SaveDir(d, dir()); err != nil { + t.Fatal(err) + } + // Save a chart + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "with-dependency", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{{ + Name: d.Metadata.Name, + Version: ">=0.1.0", + Repository: "file://../dep-chart", + }}, + }, + } + if err := chartutil.SaveDir(c, dir()); err != nil { + t.Fatal(err) + } + + // Set-up a manager + b := bytes.NewBuffer(nil) + g := getter.Providers{getter.Provider{ + Schemes: []string{"http", "https"}, + New: getter.NewHTTPGetter, + }} + m := &Manager{ + ChartPath: dir(c.Metadata.Name), + Out: b, + Getters: g, + RepositoryConfig: dir("repositories.yaml"), + RepositoryCache: dir(), + } + + // Update before Build. see issue: https://github.com/helm/helm/issues/7101 + err = m.Update() + if err != nil { + t.Fatal(err) + } + + err = m.Build() + if err != nil { + t.Fatal(err) + } +} + +// TestUpdateWithNoRepo is for the case of a dependency that has no repo listed. +// This happens when the dependency is in the charts directory and does not need +// to be fetched. +func TestUpdateWithNoRepo(t *testing.T) { + // Set up a fake repo + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + dir := func(p ...string) string { + return filepath.Join(append([]string{srv.Root()}, p...)...) + } + + // Setup the dependent chart + d := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "dep-chart", + Version: "0.1.0", + APIVersion: "v1", + }, + } + + // Save a chart with the dependency + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "with-dependency", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{{ + Name: d.Metadata.Name, + Version: "0.1.0", + }}, + }, + } + if err := chartutil.SaveDir(c, dir()); err != nil { + t.Fatal(err) + } + + // Save dependent chart into the parents charts directory. If the chart is + // not in the charts directory Helm will return an error that it is not + // found. + if err := chartutil.SaveDir(d, dir(c.Metadata.Name, "charts")); err != nil { + t.Fatal(err) + } + + // Set-up a manager + b := bytes.NewBuffer(nil) + g := getter.Providers{getter.Provider{ + Schemes: []string{"http", "https"}, + New: getter.NewHTTPGetter, + }} + m := &Manager{ + ChartPath: dir(c.Metadata.Name), + Out: b, + Getters: g, + RepositoryConfig: dir("repositories.yaml"), + RepositoryCache: dir(), + } + + // Test the update + err = m.Update() + if err != nil { + t.Fatal(err) + } +} + +// This function is the skeleton test code of failing tests for #6416 and #6871 and bugs due to #5874. +// +// This function is used by below tests that ensures success of build operation +// with optional fields, alias, condition, tags, and even with ranged version. +// Parent chart includes local-subchart 0.1.0 subchart from a fake repository, by default. +// If each of these main fields (name, version, repository) is not supplied by dep param, default value will be used. +func checkBuildWithOptionalFields(t *testing.T, chartName string, dep chart.Dependency) { + // Set up a fake repo + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + dir := func(p ...string) string { + return filepath.Join(append([]string{srv.Root()}, p...)...) + } + + // Set main fields if not exist + if dep.Name == "" { + dep.Name = "local-subchart" + } + if dep.Version == "" { + dep.Version = "0.1.0" + } + if dep.Repository == "" { + dep.Repository = srv.URL() + } + + // Save a chart + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: chartName, + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{&dep}, + }, + } + if err := chartutil.SaveDir(c, dir()); err != nil { + t.Fatal(err) + } + + // Set-up a manager + b := bytes.NewBuffer(nil) + g := getter.Providers{getter.Provider{ + Schemes: []string{"http", "https"}, + New: getter.NewHTTPGetter, + }} + m := &Manager{ + ChartPath: dir(chartName), + Out: b, + Getters: g, + RepositoryConfig: dir("repositories.yaml"), + RepositoryCache: dir(), + } + + // First build will update dependencies and create Chart.lock file. + err = m.Build() + if err != nil { + t.Fatal(err) + } + + // Second build should be passed. See PR #6655. + err = m.Build() + if err != nil { + t.Fatal(err) + } +} + +func TestBuild_WithoutOptionalFields(t *testing.T) { + // Dependency has main fields only (name/version/repository) + checkBuildWithOptionalFields(t, "without-optional-fields", chart.Dependency{}) +} + +func TestBuild_WithSemVerRange(t *testing.T) { + // Dependency version is the form of SemVer range + checkBuildWithOptionalFields(t, "with-semver-range", chart.Dependency{ + Version: ">=0.1.0", + }) +} + +func TestBuild_WithAlias(t *testing.T) { + // Dependency has an alias + checkBuildWithOptionalFields(t, "with-alias", chart.Dependency{ + Alias: "local-subchart-alias", + }) +} + +func TestBuild_WithCondition(t *testing.T) { + // Dependency has a condition + checkBuildWithOptionalFields(t, "with-condition", chart.Dependency{ + Condition: "some.condition", + }) +} + +func TestBuild_WithTags(t *testing.T) { + // Dependency has several tags + checkBuildWithOptionalFields(t, "with-tags", chart.Dependency{ + Tags: []string{"tag1", "tag2"}, + }) +} + +// Failing test for #6871 +func TestBuild_WithRepositoryAlias(t *testing.T) { + // Dependency repository is aliased in Chart.yaml + checkBuildWithOptionalFields(t, "with-repository-alias", chart.Dependency{ + Repository: "@test", + }) +} + +func TestErrRepoNotFound_Error(t *testing.T) { + type fields struct { + Repos []string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "OK", + fields: fields{ + Repos: []string{"https://charts1.example.com", "https://charts2.example.com"}, + }, + want: "no repository definition for https://charts1.example.com, https://charts2.example.com", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := ErrRepoNotFound{ + Repos: tt.fields.Repos, + } + if got := e.Error(); got != tt.want { + t.Errorf("Error() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestKey(t *testing.T) { + tests := []struct { + name string + expect string + }{ + { + name: "file:////tmp", + expect: "afeed3459e92a874f6373aca264ce1459bfa91f9c1d6612f10ae3dc2ee955df3", + }, + { + name: "https://example.com/charts", + expect: "7065c57c94b2411ad774638d76823c7ccb56415441f5ab2f5ece2f3845728e5d", + }, + { + name: "foo/bar/baz", + expect: "15c46a4f8a189ae22f36f201048881d6c090c93583bedcf71f5443fdef224c82", + }, + } + + for _, tt := range tests { + o, err := key(tt.name) + if err != nil { + t.Fatalf("unable to generate key for %q with error: %s", tt.name, err) + } + if o != tt.expect { + t.Errorf("wrong key name generated for %q, expected %q but got %q", tt.name, tt.expect, o) + } + } +} diff --git a/pkg/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml b/pkg/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml deleted file mode 100644 index 28d272ae24a..00000000000 --- a/pkg/downloader/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: v1 -entries: - alpine: - - name: alpine - urls: - - https://kubernetes-charts.storage.googleapis.com/alpine-0.1.0.tgz - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 0.1.0 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" - - name: alpine - urls: - - https://kubernetes-charts.storage.googleapis.com/alpine-0.2.0.tgz - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 0.2.0 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" - mariadb: - - name: mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.3.0.tgz - checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 - home: https://mariadb.org - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - version: 0.3.0 - description: Chart for MariaDB - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - name: Bitnami - email: containers@bitnami.com - engine: gotpl - icon: "" diff --git a/pkg/downloader/testdata/helmhome/repository/cache/local-index.yaml b/pkg/downloader/testdata/helmhome/repository/cache/local-index.yaml deleted file mode 120000 index ed068e99e4d..00000000000 --- a/pkg/downloader/testdata/helmhome/repository/cache/local-index.yaml +++ /dev/null @@ -1 +0,0 @@ -repository/local/index.yaml \ No newline at end of file diff --git a/pkg/downloader/testdata/helmhome/repository/cache/testing-basicauth-index.yaml b/pkg/downloader/testdata/helmhome/repository/cache/testing-basicauth-index.yaml deleted file mode 100644 index 47bb1b77c87..00000000000 --- a/pkg/downloader/testdata/helmhome/repository/cache/testing-basicauth-index.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -entries: - foo: - - name: foo - description: Foo Chart - engine: gotpl - home: https://k8s.io/helm - keywords: [] - maintainers: [] - sources: - - https://github.com/kubernetes/charts - urls: - - http://username:password@example.com/foo-1.2.3.tgz - version: 1.2.3 - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d diff --git a/pkg/downloader/testdata/helmhome/repository/cache/testing-https-index.yaml b/pkg/downloader/testdata/helmhome/repository/cache/testing-https-index.yaml deleted file mode 100644 index 872478c3f9d..00000000000 --- a/pkg/downloader/testdata/helmhome/repository/cache/testing-https-index.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -entries: - foo: - - name: foo - description: Foo Chart - engine: gotpl - home: https://k8s.io/helm - keywords: [] - maintainers: [] - sources: - - https://github.com/kubernetes/charts - urls: - - https://example.com/foo-1.2.3.tgz - version: 1.2.3 - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d diff --git a/pkg/downloader/testdata/helmhome/repository/cache/testing-index.yaml b/pkg/downloader/testdata/helmhome/repository/cache/testing-index.yaml deleted file mode 100644 index 14cdffecee6..00000000000 --- a/pkg/downloader/testdata/helmhome/repository/cache/testing-index.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: v1 -entries: - alpine: - - name: alpine - urls: - - http://example.com/alpine-1.2.3.tgz - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 1.2.3 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" - - name: alpine - urls: - - http://example.com/alpine-0.2.0.tgz - - https://kubernetes-charts.storage.googleapis.com/alpine-0.2.0.tgz - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 0.2.0 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" - foo: - - name: foo - description: Foo Chart - engine: gotpl - home: https://k8s.io/helm - keywords: [] - maintainers: [] - sources: - - https://github.com/kubernetes/charts - urls: - - http://example.com/foo-1.2.3.tgz - version: 1.2.3 - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d diff --git a/pkg/downloader/testdata/helmhome/repository/cache/testing-relative-index.yaml b/pkg/downloader/testdata/helmhome/repository/cache/testing-relative-index.yaml deleted file mode 100644 index 210f92e45fa..00000000000 --- a/pkg/downloader/testdata/helmhome/repository/cache/testing-relative-index.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: v1 -entries: - foo: - - name: foo - description: Foo Chart With Relative Path - engine: gotpl - home: https://k8s.io/helm - keywords: [] - maintainers: [] - sources: - - https://github.com/kubernetes/charts - urls: - - charts/foo-1.2.3.tgz - version: 1.2.3 - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - bar: - - name: bar - description: Bar Chart With Relative Path - engine: gotpl - home: https://k8s.io/helm - keywords: [] - maintainers: [] - sources: - - https://github.com/kubernetes/charts - urls: - - bar-1.2.3.tgz - version: 1.2.3 - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d diff --git a/pkg/downloader/testdata/helmhome/repository/cache/testing-relative-trailing-slash-index.yaml b/pkg/downloader/testdata/helmhome/repository/cache/testing-relative-trailing-slash-index.yaml deleted file mode 100644 index 210f92e45fa..00000000000 --- a/pkg/downloader/testdata/helmhome/repository/cache/testing-relative-trailing-slash-index.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: v1 -entries: - foo: - - name: foo - description: Foo Chart With Relative Path - engine: gotpl - home: https://k8s.io/helm - keywords: [] - maintainers: [] - sources: - - https://github.com/kubernetes/charts - urls: - - charts/foo-1.2.3.tgz - version: 1.2.3 - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - bar: - - name: bar - description: Bar Chart With Relative Path - engine: gotpl - home: https://k8s.io/helm - keywords: [] - maintainers: [] - sources: - - https://github.com/kubernetes/charts - urls: - - bar-1.2.3.tgz - version: 1.2.3 - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d diff --git a/pkg/downloader/testdata/local-subchart-0.1.0.tgz b/pkg/downloader/testdata/local-subchart-0.1.0.tgz new file mode 100644 index 00000000000..4853121056a Binary files /dev/null and b/pkg/downloader/testdata/local-subchart-0.1.0.tgz differ diff --git a/pkg/downloader/testdata/local-subchart/Chart.yaml b/pkg/downloader/testdata/local-subchart/Chart.yaml new file mode 100644 index 00000000000..1e17203e580 --- /dev/null +++ b/pkg/downloader/testdata/local-subchart/Chart.yaml @@ -0,0 +1,3 @@ +description: A Helm chart for Kubernetes +name: local-subchart +version: 0.1.0 diff --git a/pkg/downloader/testdata/helmhome/repository/repositories.yaml b/pkg/downloader/testdata/repositories.yaml similarity index 76% rename from pkg/downloader/testdata/helmhome/repository/repositories.yaml rename to pkg/downloader/testdata/repositories.yaml index 374d95c8abf..43086526987 100644 --- a/pkg/downloader/testdata/helmhome/repository/repositories.yaml +++ b/pkg/downloader/testdata/repositories.yaml @@ -15,4 +15,9 @@ repositories: - name: testing-relative url: "http://example.com/helm" - name: testing-relative-trailing-slash - url: "http://example.com/helm/" \ No newline at end of file + url: "http://example.com/helm/" + - name: testing-ca-file + url: "https://example.com" + certFile: "cert" + keyFile: "key" + caFile: "ca" diff --git a/pkg/downloader/testdata/repository/kubernetes-charts-index.yaml b/pkg/downloader/testdata/repository/kubernetes-charts-index.yaml new file mode 100644 index 00000000000..52dcf930b62 --- /dev/null +++ b/pkg/downloader/testdata/repository/kubernetes-charts-index.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +entries: + alpine: + - name: alpine + urls: + - https://charts.helm.sh/stable/alpine-0.1.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://helm.sh/helm + sources: + - https://github.com/helm/helm + version: 0.1.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + icon: "" + apiVersion: v2 + - name: alpine + urls: + - https://charts.helm.sh/stable/alpine-0.2.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://helm.sh/helm + sources: + - https://github.com/helm/helm + version: 0.2.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + icon: "" + apiVersion: v2 + mariadb: + - name: mariadb + urls: + - https://charts.helm.sh/stable/mariadb-0.3.0.tgz + checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 + home: https://mariadb.org + sources: + - https://github.com/bitnami/bitnami-docker-mariadb + version: 0.3.0 + description: Chart for MariaDB + keywords: + - mariadb + - mysql + - database + - sql + maintainers: + - name: Bitnami + email: containers@bitnami.com + icon: "" + apiVersion: v2 diff --git a/pkg/downloader/testdata/helmhome/repository/cache/malformed-index.yaml b/pkg/downloader/testdata/repository/malformed-index.yaml similarity index 75% rename from pkg/downloader/testdata/helmhome/repository/cache/malformed-index.yaml rename to pkg/downloader/testdata/repository/malformed-index.yaml index 1956e9f8349..fa319abddf3 100644 --- a/pkg/downloader/testdata/helmhome/repository/cache/malformed-index.yaml +++ b/pkg/downloader/testdata/repository/malformed-index.yaml @@ -5,12 +5,12 @@ entries: urls: - alpine-1.2.3.tgz checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - home: https://k8s.io/helm + home: https://helm.sh/helm sources: - - https://github.com/kubernetes/helm + - https://github.com/helm/helm version: 1.2.3 description: Deploy a basic Alpine Linux pod keywords: [] maintainers: [] - engine: "" icon: "" + apiVersion: v2 diff --git a/pkg/downloader/testdata/repository/testing-basicauth-index.yaml b/pkg/downloader/testdata/repository/testing-basicauth-index.yaml new file mode 100644 index 00000000000..ed092ef4169 --- /dev/null +++ b/pkg/downloader/testdata/repository/testing-basicauth-index.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +entries: + foo: + - name: foo + description: Foo Chart + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - http://username:password@example.com/foo-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + apiVersion: v2 diff --git a/pkg/downloader/testdata/repository/testing-ca-file-index.yaml b/pkg/downloader/testdata/repository/testing-ca-file-index.yaml new file mode 100644 index 00000000000..81901efc720 --- /dev/null +++ b/pkg/downloader/testdata/repository/testing-ca-file-index.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +entries: + foo: + - name: foo + description: Foo Chart + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - https://example.com/foo-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + apiVersion: v2 diff --git a/pkg/downloader/testdata/repository/testing-https-index.yaml b/pkg/downloader/testdata/repository/testing-https-index.yaml new file mode 100644 index 00000000000..81901efc720 --- /dev/null +++ b/pkg/downloader/testdata/repository/testing-https-index.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +entries: + foo: + - name: foo + description: Foo Chart + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - https://example.com/foo-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + apiVersion: v2 diff --git a/pkg/downloader/testdata/repository/testing-index.yaml b/pkg/downloader/testdata/repository/testing-index.yaml new file mode 100644 index 00000000000..f588bf1fb16 --- /dev/null +++ b/pkg/downloader/testdata/repository/testing-index.yaml @@ -0,0 +1,43 @@ +apiVersion: v1 +entries: + alpine: + - name: alpine + urls: + - http://example.com/alpine-1.2.3.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://helm.sh/helm + sources: + - https://github.com/helm/helm + version: 1.2.3 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + icon: "" + apiVersion: v2 + - name: alpine + urls: + - http://example.com/alpine-0.2.0.tgz + - https://charts.helm.sh/stable/alpine-0.2.0.tgz + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + home: https://helm.sh/helm + sources: + - https://github.com/helm/helm + version: 0.2.0 + description: Deploy a basic Alpine Linux pod + keywords: [] + maintainers: [] + icon: "" + apiVersion: v2 + foo: + - name: foo + description: Foo Chart + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - http://example.com/foo-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + apiVersion: v2 diff --git a/pkg/downloader/testdata/helmhome/repository/cache/testing-querystring-index.yaml b/pkg/downloader/testdata/repository/testing-querystring-index.yaml similarity index 75% rename from pkg/downloader/testdata/helmhome/repository/cache/testing-querystring-index.yaml rename to pkg/downloader/testdata/repository/testing-querystring-index.yaml index 1956e9f8349..fa319abddf3 100644 --- a/pkg/downloader/testdata/helmhome/repository/cache/testing-querystring-index.yaml +++ b/pkg/downloader/testdata/repository/testing-querystring-index.yaml @@ -5,12 +5,12 @@ entries: urls: - alpine-1.2.3.tgz checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - home: https://k8s.io/helm + home: https://helm.sh/helm sources: - - https://github.com/kubernetes/helm + - https://github.com/helm/helm version: 1.2.3 description: Deploy a basic Alpine Linux pod keywords: [] maintainers: [] - engine: "" icon: "" + apiVersion: v2 diff --git a/pkg/downloader/testdata/repository/testing-relative-index.yaml b/pkg/downloader/testdata/repository/testing-relative-index.yaml new file mode 100644 index 00000000000..ba27ed2573f --- /dev/null +++ b/pkg/downloader/testdata/repository/testing-relative-index.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +entries: + foo: + - name: foo + description: Foo Chart With Relative Path + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - charts/foo-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + apiVersion: v2 + bar: + - name: bar + description: Bar Chart With Relative Path + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - bar-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + apiVersion: v2 diff --git a/pkg/downloader/testdata/repository/testing-relative-trailing-slash-index.yaml b/pkg/downloader/testdata/repository/testing-relative-trailing-slash-index.yaml new file mode 100644 index 00000000000..ba27ed2573f --- /dev/null +++ b/pkg/downloader/testdata/repository/testing-relative-trailing-slash-index.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +entries: + foo: + - name: foo + description: Foo Chart With Relative Path + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - charts/foo-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + apiVersion: v2 + bar: + - name: bar + description: Bar Chart With Relative Path + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - bar-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + apiVersion: v2 diff --git a/pkg/downloader/testdata/signtest-0.1.0.tgz b/pkg/downloader/testdata/signtest-0.1.0.tgz index 6de9d988d47..c74e5b0ef5c 100644 Binary files a/pkg/downloader/testdata/signtest-0.1.0.tgz and b/pkg/downloader/testdata/signtest-0.1.0.tgz differ diff --git a/pkg/downloader/testdata/signtest-0.1.0.tgz.prov b/pkg/downloader/testdata/signtest-0.1.0.tgz.prov index 94235399ae0..d325bb266e6 100644 --- a/pkg/downloader/testdata/signtest-0.1.0.tgz.prov +++ b/pkg/downloader/testdata/signtest-0.1.0.tgz.prov @@ -1,20 +1,21 @@ -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 +apiVersion: v1 description: A Helm chart for Kubernetes name: signtest version: 0.1.0 ... files: - signtest-0.1.0.tgz: sha256:dee72947753628425b82814516bdaa37aef49f25e8820dd2a6e15a33a007823b + signtest-0.1.0.tgz: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55 -----BEGIN PGP SIGNATURE----- -wsBcBAEBCgAQBQJXomNHCRCEO7+YH8GHYgAALywIAG1Me852Fpn1GYu8Q1GCcw4g -l2k7vOFchdDwDhdSVbkh4YyvTaIO3iE2Jtk1rxw+RIJiUr0eLO/rnIJuxZS8WKki -DR1LI9J1VD4dxN3uDETtWDWq7ScoPsRY5mJvYZXC8whrWEt/H2kfqmoA9LloRPWp -flOE0iktA4UciZOblTj6nAk3iDyjh/4HYL4a6tT0LjjKI7OTw4YyHfjHad1ywVCz -9dMUc1rPgTnl+fnRiSPSrlZIWKOt1mcQ4fVrU3nwtRUwTId2k8FtygL0G6M+Y6t0 -S6yaU7qfk9uTxkdkUF7Bf1X3ukxfe+cNBC32vf4m8LY4NkcYfSqK2fGtQsnVr6s= -=NyOM +wsBcBAEBCgAQBQJcoosfCRCEO7+YH8GHYgAA220IALAs8T8NPgkcLvHu+5109cAN +BOCNPSZDNsqLZW/2Dc9cKoBG7Jen4Qad+i5l9351kqn3D9Gm6eRfAWcjfggRobV/ +9daZ19h0nl4O1muQNAkjvdgZt8MOP3+PB3I3/Tu2QCYjI579SLUmuXlcZR5BCFPR +PJy+e3QpV2PcdeU2KZLG4tjtlrq+3QC9ZHHEJLs+BVN9d46Dwo6CxJdHJrrrAkTw +M8MhA92vbiTTPRSCZI9x5qDAwJYhoq0oxLflpuL2tIlo3qVoCsaTSURwMESEHO32 +XwYG7BaVDMELWhAorBAGBGBwWFbJ1677qQ2gd9CN0COiVhekWlFRcnn60800r84= +=k9Y9 -----END PGP SIGNATURE----- \ No newline at end of file diff --git a/pkg/downloader/testdata/signtest/Chart.yaml b/pkg/downloader/testdata/signtest/Chart.yaml index 90964b44a60..f1f73723a86 100644 --- a/pkg/downloader/testdata/signtest/Chart.yaml +++ b/pkg/downloader/testdata/signtest/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: signtest version: 0.1.0 diff --git a/pkg/downloader/testdata/signtest/alpine/Chart.yaml b/pkg/downloader/testdata/signtest/alpine/Chart.yaml index 6fbb27f1811..eec261220ec 100644 --- a/pkg/downloader/testdata/signtest/alpine/Chart.yaml +++ b/pkg/downloader/testdata/signtest/alpine/Chart.yaml @@ -1,6 +1,7 @@ +apiVersion: v1 description: Deploy a basic Alpine Linux pod -home: https://k8s.io/helm +home: https://helm.sh/helm name: alpine sources: -- https://github.com/kubernetes/helm +- https://github.com/helm/helm version: 0.1.0 diff --git a/pkg/downloader/testdata/signtest/alpine/README.md b/pkg/downloader/testdata/signtest/alpine/README.md index 5bd595747e4..28bebae070e 100644 --- a/pkg/downloader/testdata/signtest/alpine/README.md +++ b/pkg/downloader/testdata/signtest/alpine/README.md @@ -6,4 +6,4 @@ couple of parameters. The `values.yaml` file contains the default values for the `alpine-pod.yaml` template. -You can install this example using `helm install docs/examples/alpine`. +You can install this example using `helm install ./alpine`. diff --git a/pkg/downloader/testdata/signtest/alpine/templates/alpine-pod.yaml b/pkg/downloader/testdata/signtest/alpine/templates/alpine-pod.yaml index 08cf3c2c1f5..5bbae10afb3 100644 --- a/pkg/downloader/testdata/signtest/alpine/templates/alpine-pod.yaml +++ b/pkg/downloader/testdata/signtest/alpine/templates/alpine-pod.yaml @@ -3,11 +3,9 @@ kind: Pod metadata: name: {{.Release.Name}}-{{.Chart.Name}} labels: - heritage: {{.Release.Service}} + app.kubernetes.io/managed-by: {{.Release.Service}} chartName: {{.Chart.Name}} chartVersion: {{.Chart.Version | quote}} - annotations: - "helm.sh/created": "{{.Release.Time.Seconds}}" spec: restartPolicy: {{default "Never" .restart_policy}} containers: diff --git a/pkg/engine/doc.go b/pkg/engine/doc.go index 53c4084b005..6ff875c46b7 100644 --- a/pkg/engine/doc.go +++ b/pkg/engine/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package engine implements the Go template engine as a Tiller Engine. +/*Package engine implements the Go text template engine as needed for Helm. -Tiller provides a simple interface for taking a Chart and rendering its templates. -The 'engine' package implements this interface using Go's built-in 'text/template' -package. +When Helm renders templates it does so with additional functions and different +modes (e.g., strict, lint mode). This package handles the helm specific +implementation. */ -package engine // import "k8s.io/helm/pkg/engine" +package engine // import "helm.sh/helm/v3/pkg/engine" diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 7a940fc84bc..155d50a3853 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,84 +17,31 @@ limitations under the License. package engine import ( - "bytes" "fmt" + "log" "path" + "path/filepath" + "regexp" "sort" "strings" "text/template" - "github.com/Masterminds/sprig" + "github.com/pkg/errors" + "k8s.io/client-go/rest" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/proto/hapi/chart" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" ) -// Engine is an implementation of 'cmd/tiller/environment'.Engine that uses Go templates. +// Engine is an implementation of the Helm rendering implementation for templates. 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 - CurrentTemplates map[string]renderable -} - -// New creates a new Go template Engine instance. -// -// The FuncMap is initialized here. You may modify the FuncMap _prior to_ the -// first invocation of Render. -// -// The FuncMap sets all of the Sprig functions except for those that provide -// access to the underlying OS (env, expandenv). -func New() *Engine { - f := FuncMap() - return &Engine{ - FuncMap: f, - } -} - -// FuncMap returns a mapping of all of the functions that Engine has. -// -// Because some functions are late-bound (e.g. contain context-sensitive -// data), the functions may not all perform identically outside of an -// Engine as they will inside of an Engine. -// -// Known late-bound functions: -// -// - "include": This is late-bound in Engine.Render(). The version -// included in the FuncMap is a placeholder. -// - "required": This is late-bound in Engine.Render(). The version -// included in the FuncMap is a placeholder. -// - "tpl": This is late-bound in Engine.Render(). The version -// included in the FuncMap is a placeholder. -func FuncMap() template.FuncMap { - f := sprig.TxtFuncMap() - delete(f, "env") - delete(f, "expandenv") - - // Add some extra functionality - extra := template.FuncMap{ - "toToml": chartutil.ToToml, - "toYaml": chartutil.ToYaml, - "fromYaml": chartutil.FromYaml, - "toJson": chartutil.ToJson, - "fromJson": chartutil.FromJson, - - // This is a placeholder for the "include" function, which is - // late-bound to a template. By declaring it here, we preserve the - // integrity of the linter. - "include": func(string, interface{}) string { return "not implemented" }, - "required": func(string, interface{}) interface{} { return "not implemented" }, - "tpl": func(string, interface{}) interface{} { return "not implemented" }, - } - - for k, v := range extra { - f[k] = v - } - - return f + Strict bool + // In LintMode, some 'required' template values may be missing, so don't fail + LintMode bool + // the rest config to connect to the kubernetes api + config *rest.Config } // Render takes a chart, optional values, and value overrides, and attempts to render the Go templates. @@ -116,13 +63,26 @@ func FuncMap() template.FuncMap { // that section of the values will be passed into the "foo" chart. And if that // section contains a value named "bar", that value will be passed on to the // bar chart during render time. -func (e *Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { - // Render the charts +func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { tmap := allTemplates(chrt, values) - e.CurrentTemplates = tmap return e.render(tmap) } +// Render takes a chart, optional values, and value overrides, and attempts to +// render the Go templates using the default options. +func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) { + return new(Engine).Render(chrt, values) +} + +// RenderWithClient takes a chart, optional values, and value overrides, and attempts to +// render the Go templates using the default options. This engine is client aware and so can have template +// functions that interact with the client +func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) { + return Engine{ + config: config, + }.Render(chrt, values) +} + // renderable is an object that can be rendered. type renderable struct { // tpl is the current template. @@ -133,70 +93,103 @@ type renderable struct { basePath string } -// alterFuncMap takes the Engine's FuncMap and adds context-specific functions. -// -// The resulting FuncMap is only valid for the passed-in template. -func (e *Engine) alterFuncMap(t *template.Template) template.FuncMap { - // Clone the func map because we are adding context-specific functions. - var funcMap template.FuncMap = map[string]interface{}{} - for k, v := range e.FuncMap { - funcMap[k] = v - } +const warnStartDelim = "HELM_ERR_START" +const warnEndDelim = "HELM_ERR_END" +const recursionMaxNums = 1000 + +var warnRegex = regexp.MustCompile(warnStartDelim + `(.*)` + warnEndDelim) + +func warnWrap(warn string) string { + return warnStartDelim + warn + warnEndDelim +} + +// initFunMap creates the Engine's FuncMap and adds context-specific functions. +func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]renderable) { + funcMap := funcMap() + includedNames := make(map[string]int) // Add the 'include' function here so we can close over t. funcMap["include"] = func(name string, data interface{}) (string, error) { - buf := bytes.NewBuffer(nil) - if err := t.ExecuteTemplate(buf, name, data); err != nil { - return "", err - } - return buf.String(), nil - } - - // Add the 'required' function here - funcMap["required"] = func(warn string, val interface{}) (interface{}, error) { - if val == nil { - return val, fmt.Errorf(warn) - } else if _, ok := val.(string); ok { - if val == "" { - return val, fmt.Errorf(warn) + var buf strings.Builder + if v, ok := includedNames[name]; ok { + if v > recursionMaxNums { + return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name) } + includedNames[name]++ + } else { + includedNames[name] = 1 } - return val, nil + err := t.ExecuteTemplate(&buf, name, data) + includedNames[name]-- + return buf.String(), err } // Add the 'tpl' function here funcMap["tpl"] = func(tpl string, vals chartutil.Values) (string, error) { basePath, err := vals.PathValue("Template.BasePath") if err != nil { - return "", fmt.Errorf("Cannot retrieve Template.Basepath from values inside tpl function: %s (%s)", tpl, err.Error()) + return "", errors.Wrapf(err, "cannot retrieve Template.Basepath from values inside tpl function: %s", tpl) } - r := renderable{ - tpl: tpl, - vals: vals, - basePath: basePath.(string), - } - - templates := map[string]renderable{} templateName, err := vals.PathValue("Template.Name") if err != nil { - return "", fmt.Errorf("Cannot retrieve Template.Name from values inside tpl function: %s (%s)", tpl, err.Error()) + return "", errors.Wrapf(err, "cannot retrieve Template.Name from values inside tpl function: %s", tpl) } - templates[templateName.(string)] = r + templates := map[string]renderable{ + templateName.(string): { + tpl: tpl, + vals: vals, + basePath: basePath.(string), + }, + } - result, err := e.render(templates) + result, err := e.renderWithReferences(templates, referenceTpls) if err != nil { - return "", fmt.Errorf("Error during tpl function execution for %q: %s", tpl, err.Error()) + return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl) } return result[templateName.(string)], nil } - return funcMap + // Add the `required` function here so we can use lintMode + funcMap["required"] = func(warn string, val interface{}) (interface{}, error) { + if val == nil { + if e.LintMode { + // Don't fail on missing required values when linting + log.Printf("[INFO] Missing required value: %s", warn) + return "", nil + } + return val, errors.Errorf(warnWrap(warn)) + } else if _, ok := val.(string); ok { + if val == "" { + if e.LintMode { + // Don't fail on missing required values when linting + log.Printf("[INFO] Missing required value: %s", warn) + return "", nil + } + return val, errors.Errorf(warnWrap(warn)) + } + } + return val, nil + } + + // If we are not linting and have a cluster connection, provide a Kubernetes-backed + // implementation. + if !e.LintMode && e.config != nil { + funcMap["lookup"] = NewLookupFunction(e.config) + } + + t.Funcs(funcMap) } // render takes a map of templates/values and renders them. -func (e *Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) { +func (e Engine) render(tpls map[string]renderable) (map[string]string, error) { + return e.renderWithReferences(tpls, tpls) +} + +// renderWithReferences takes a map of templates/values to render, and a map of +// templates which can be referenced within them. +func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable) (rendered map[string]string, err error) { // Basically, what we do here is start with an empty parent template and then // build up a list of templates -- one for each file. Once all of the templates // have been parsed, we loop through again and execute every template. @@ -206,7 +199,7 @@ func (e *Engine) render(tpls map[string]renderable) (rendered map[string]string, // template engine. defer func() { if r := recover(); r != nil { - err = fmt.Errorf("rendering template failed: %v", r) + err = errors.Errorf("rendering template failed: %v", r) } }() t := template.New("gotpl") @@ -218,59 +211,92 @@ func (e *Engine) render(tpls map[string]renderable) (rendered map[string]string, t.Option("missingkey=zero") } - funcMap := e.alterFuncMap(t) + e.initFunMap(t, referenceTpls) // We want to parse the templates in a predictable order. The order favors // higher-level (in file system) templates over deeply nested templates. keys := sortTemplates(tpls) + referenceKeys := sortTemplates(referenceTpls) - files := []string{} - - for _, fname := range keys { - r := tpls[fname] - t = t.New(fname).Funcs(funcMap) - if _, err := t.Parse(r.tpl); err != nil { - return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err) + for _, filename := range keys { + r := tpls[filename] + if _, err := t.New(filename).Parse(r.tpl); err != nil { + return map[string]string{}, cleanupParseError(filename, err) } - files = append(files, fname) } - // Adding the engine's currentTemplates to the template context + // Adding the reference templates to the template context // so they can be referenced in the tpl function - for fname, r := range e.CurrentTemplates { - if t.Lookup(fname) == nil { - t = t.New(fname).Funcs(funcMap) - if _, err := t.Parse(r.tpl); err != nil { - return map[string]string{}, fmt.Errorf("parse error in %q: %s", fname, err) + for _, filename := range referenceKeys { + if t.Lookup(filename) == nil { + r := referenceTpls[filename] + if _, err := t.New(filename).Parse(r.tpl); err != nil { + return map[string]string{}, cleanupParseError(filename, err) } } } - rendered = make(map[string]string, len(files)) - var buf bytes.Buffer - for _, file := range files { + rendered = make(map[string]string, len(keys)) + for _, filename := range keys { // Don't render partials. We don't care out the direct output of partials. // They are only included from other templates. - if strings.HasPrefix(path.Base(file), "_") { + if strings.HasPrefix(path.Base(filename), "_") { continue } // At render time, add information about the template that is being rendered. - vals := tpls[file].vals - vals["Template"] = map[string]interface{}{"Name": file, "BasePath": tpls[file].basePath} - if err := t.ExecuteTemplate(&buf, file, vals); err != nil { - return map[string]string{}, fmt.Errorf("render error in %q: %s", file, err) + vals := tpls[filename].vals + vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath} + var buf strings.Builder + if err := t.ExecuteTemplate(&buf, filename, vals); err != nil { + return map[string]string{}, cleanupExecError(filename, 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. - rendered[file] = strings.Replace(buf.String(), "", "", -1) - buf.Reset() + rendered[filename] = strings.ReplaceAll(buf.String(), "", "") } return rendered, nil } +func cleanupParseError(filename string, err error) error { + tokens := strings.Split(err.Error(), ": ") + if len(tokens) == 1 { + // This might happen if a non-templating error occurs + return fmt.Errorf("parse error in (%s): %s", filename, err) + } + // The first token is "template" + // The second token is either "filename:lineno" or "filename:lineNo:columnNo" + location := tokens[1] + // The remaining tokens make up a stacktrace-like chain, ending with the relevant error + errMsg := tokens[len(tokens)-1] + return fmt.Errorf("parse error at (%s): %s", string(location), errMsg) +} + +func cleanupExecError(filename string, err error) error { + if _, isExecError := err.(template.ExecError); !isExecError { + return err + } + + tokens := strings.SplitN(err.Error(), ": ", 3) + if len(tokens) != 3 { + // This might happen if a non-templating error occurs + return fmt.Errorf("execution error in (%s): %s", filename, err) + } + + // The first token is "template" + // The second token is either "filename:lineno" or "filename:lineNo:columnNo" + location := tokens[1] + + parts := warnRegex.FindStringSubmatch(tokens[2]) + if len(parts) >= 2 { + return fmt.Errorf("execution error at (%s): %s", string(location), parts[1]) + } + + return err +} + func sortTemplates(tpls map[string]renderable) []string { keys := make([]string, len(tpls)) i := 0 @@ -299,8 +325,8 @@ func (p byPathLen) Less(i, j int) bool { // // 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, "") + templates := make(map[string]renderable) + recAllTpls(c, templates, vals) return templates } @@ -308,49 +334,49 @@ 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, parentID string) { - // This should never evaluate to a nil map. That will cause problems when - // values are appended later. - cvals := chartutil.Values{} - if top { - // If this is the top of the rendering tree, assume that parentVals - // is already resolved to the authoritative values. - cvals = parentVals - } else if c.Metadata != nil && c.Metadata.Name != "" { - // 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 - } - } - - cvals = map[string]interface{}{ - "Values": newVals, - "Release": parentVals["Release"], - "Chart": c.Metadata, - "Files": chartutil.NewFiles(c.Files), - "Capabilities": parentVals["Capabilities"], - } +func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) { + next := map[string]interface{}{ + "Chart": c.Metadata, + "Files": newFiles(c.Files), + "Release": vals["Release"], + "Capabilities": vals["Capabilities"], + "Values": make(chartutil.Values), } - 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) + // If there is a {{.Values.ThisChart}} in the parent metadata, + // copy that into the {{.Values}} for this template. + if c.IsRoot() { + next["Values"] = vals["Values"] + } else if vs, err := vals.Table("Values." + c.Name()); err == nil { + next["Values"] = vs } - for _, child := range c.Dependencies { - recAllTpls(child, templates, cvals, false, newParentID) + for _, child := range c.Dependencies() { + recAllTpls(child, templates, next) } + + newParentID := c.ChartFullPath() for _, t := range c.Templates { + if !isTemplateValid(c, t.Name) { + continue + } templates[path.Join(newParentID, t.Name)] = renderable{ tpl: string(t.Data), - vals: cvals, + vals: next, basePath: path.Join(newParentID, "templates"), } } } + +// isTemplateValid returns true if the template is valid for the chart type +func isTemplateValid(ch *chart.Chart, templateName string) bool { + if isLibraryChart(ch) { + return strings.HasPrefix(filepath.Base(templateName), "_") + } + return true +} + +// isLibraryChart returns true if the chart is a library chart +func isLibraryChart(c *chart.Chart) bool { + return strings.EqualFold(c.Metadata.Type, "library") +} diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 8ffb3d87c9d..d2da7a77a12 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,13 +18,12 @@ package engine import ( "fmt" + "strings" "sync" "testing" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/proto/hapi/chart" - - "github.com/golang/protobuf/ptypes/any" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" ) func TestSortTemplates(t *testing.T) { @@ -53,25 +52,16 @@ func TestSortTemplates(t *testing.T) { } for i, e := range expect { if got[i] != e { - t.Errorf("expected %q, got %q at index %d\n\tExp: %#v\n\tGot: %#v", e, got[i], i, expect, got) - } - } -} - -func TestEngine(t *testing.T) { - e := New() - - // Forbidden because they allow access to the host OS. - forbidden := []string{"env", "expandenv"} - for _, f := range forbidden { - if _, ok := e.FuncMap[f]; ok { - t.Errorf("Forbidden function %s exists in FuncMap.", f) + t.Fatalf("\n\tExp:\n%s\n\tGot:\n%s", + strings.Join(expect, "\n"), + strings.Join(got, "\n"), + ) } } } func TestFuncMap(t *testing.T) { - fns := FuncMap() + fns := funcMap() forbidden := []string{"env", "expandenv"} for _, f := range forbidden { if _, ok := fns[f]; ok { @@ -80,7 +70,7 @@ func TestFuncMap(t *testing.T) { } // Test for Engine-specific template functions. - expect := []string{"include", "required", "tpl", "toYaml", "fromYaml", "toToml", "toJson", "fromJson"} + expect := []string{"include", "required", "tpl", "toYaml", "fromYaml", "toToml", "toJson", "fromJson", "lookup"} for _, f := range expect { if _, ok := fns[f]; !ok { t.Errorf("Expected add-on function %q", f) @@ -94,56 +84,90 @@ func TestRender(t *testing.T) { Name: "moby", Version: "1.2.3", }, - Templates: []*chart.Template{ - {Name: "templates/test1", Data: []byte("{{.outer | title }} {{.inner | title}}")}, - {Name: "templates/test2", Data: []byte("{{.global.callme | lower }}")}, + Templates: []*chart.File{ + {Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, + {Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")}, {Name: "templates/test3", Data: []byte("{{.noValue}}")}, + {Name: "templates/test4", Data: []byte("{{toJson .Values}}")}, }, - Values: &chart.Config{ - Raw: "outer: DEFAULT\ninner: DEFAULT", - }, + Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"}, } - vals := &chart.Config{ - Raw: ` -outer: spouter -inner: inn -global: - callme: Ishmael -`} + vals := map[string]interface{}{ + "Values": map[string]interface{}{ + "outer": "spouter", + "inner": "inn", + "global": map[string]interface{}{ + "callme": "Ishmael", + }, + }, + } - e := New() v, err := chartutil.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } - out, err := e.Render(c, v) + out, err := Render(c, v) if err != nil { t.Errorf("Failed to render templates: %s", err) } - expect := "Spouter Inn" - if out["moby/templates/test1"] != expect { - t.Errorf("Expected %q, got %q", expect, out["test1"]) + expect := map[string]string{ + "moby/templates/test1": "Spouter Inn", + "moby/templates/test2": "ishmael", + "moby/templates/test3": "", + "moby/templates/test4": `{"global":{"callme":"Ishmael"},"inner":"inn","outer":"spouter"}`, } - expect = "ishmael" - if out["moby/templates/test2"] != expect { - t.Errorf("Expected %q, got %q", expect, out["test2"]) + for name, data := range expect { + if out[name] != data { + t.Errorf("Expected %q, got %q", data, out[name]) + } } - expect = "" - if out["moby/templates/test3"] != expect { - t.Errorf("Expected %q, got %q", expect, out["test3"]) +} + +func TestRenderRefsOrdering(t *testing.T) { + parentChart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "parent", + Version: "1.2.3", + }, + Templates: []*chart.File{ + {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, + {Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, + }, + } + childChart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "child", + Version: "1.2.3", + }, + Templates: []*chart.File{ + {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, + }, } + parentChart.AddDependency(childChart) - if _, err := e.Render(c, v); err != nil { - t.Errorf("Unexpected error: %s", err) + expect := map[string]string{ + "parent/templates/test.yaml": "parent value", + } + + for i := 0; i < 100; i++ { + out, err := Render(parentChart, chartutil.Values{}) + if err != nil { + t.Fatalf("Failed to render templates: %s", err) + } + + for name, data := range expect { + if out[name] != data { + t.Fatalf("Expected %q, got %q (iteration %d)", data, out[name], i+1) + } + } } } func TestRenderInternals(t *testing.T) { // Test the internals of the rendering tool. - e := New() vals := chartutil.Values{"Name": "one", "Value": "two"} tpls := map[string]renderable{ @@ -154,7 +178,7 @@ func TestRenderInternals(t *testing.T) { "three": {tpl: `{{template "two" dict "Value" "three"}}`, vals: vals}, } - out, err := e.render(tpls) + out, err := new(Engine).render(tpls) if err != nil { t.Fatalf("Failed template rendering: %s", err) } @@ -178,21 +202,24 @@ func TestRenderInternals(t *testing.T) { func TestParallelRenderInternals(t *testing.T) { // Make sure that we can use one Engine to run parallel template renders. - e := New() + e := new(Engine) var wg sync.WaitGroup for i := 0; i < 20; i++ { wg.Add(1) go func(i int) { - fname := "my/file/name" tt := fmt.Sprintf("expect-%d", i) - v := chartutil.Values{"val": tt} - tpls := map[string]renderable{fname: {tpl: `{{.val}}`, vals: v}} + tpls := map[string]renderable{ + "t": { + tpl: `{{.val}}`, + vals: map[string]interface{}{"val": tt}, + }, + } out, err := e.render(tpls) if err != nil { t.Errorf("Failed to render %s: %s", tt, err) } - if out[fname] != tt { - t.Errorf("Expected %q, got %q", tt, out[fname]) + if out["t"] != tt { + t.Errorf("Expected %q, got %q", tt, out["t"]) } wg.Done() }(i) @@ -200,58 +227,113 @@ func TestParallelRenderInternals(t *testing.T) { wg.Wait() } +func TestParseErrors(t *testing.T) { + vals := chartutil.Values{"Values": map[string]interface{}{}} + + tplsUndefinedFunction := map[string]renderable{ + "undefined_function": {tpl: `{{foo}}`, vals: vals}, + } + _, err := new(Engine).render(tplsUndefinedFunction) + if err == nil { + t.Fatalf("Expected failures while rendering: %s", err) + } + expected := `parse error at (undefined_function:1): function "foo" not defined` + if err.Error() != expected { + t.Errorf("Expected '%s', got %q", expected, err.Error()) + } +} + +func TestExecErrors(t *testing.T) { + vals := chartutil.Values{"Values": map[string]interface{}{}} + + tplsMissingRequired := map[string]renderable{ + "missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals}, + } + _, err := new(Engine).render(tplsMissingRequired) + if err == nil { + t.Fatalf("Expected failures while rendering: %s", err) + } + expected := `execution error at (missing_required:1:2): foo is required` + if err.Error() != expected { + t.Errorf("Expected '%s', got %q", expected, err.Error()) + } + + tplsMissingRequired = map[string]renderable{ + "missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals}, + } + _, err = new(Engine).render(tplsMissingRequired) + if err == nil { + t.Fatalf("Expected failures while rendering: %s", err) + } + expected = `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:` + if err.Error() != expected { + t.Errorf("Expected '%s', got %q", expected, err.Error()) + } + + issue6044tpl := `{{ $someEmptyValue := "" }} +{{ $myvar := "abc" }} +{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}` + tplsMissingRequired = map[string]renderable{ + "issue6044": {tpl: issue6044tpl, vals: vals}, + } + _, err = new(Engine).render(tplsMissingRequired) + if err == nil { + t.Fatalf("Expected failures while rendering: %s", err) + } + expected = `execution error at (issue6044:3:4): abc: something is missing` + if err.Error() != expected { + t.Errorf("Expected '%s', got %q", expected, err.Error()) + } +} + func TestAllTemplates(t *testing.T) { ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "ch1"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: "templates/foo", Data: []byte("foo")}, {Name: "templates/bar", Data: []byte("bar")}, }, - Dependencies: []*chart.Chart{ - { - Metadata: &chart.Metadata{Name: "laboratory mice"}, - Templates: []*chart.Template{ - {Name: "templates/pinky", Data: []byte("pinky")}, - {Name: "templates/brain", Data: []byte("brain")}, - }, - Dependencies: []*chart.Chart{{ - Metadata: &chart.Metadata{Name: "same thing we do every night"}, - Templates: []*chart.Template{ - {Name: "templates/innermost", Data: []byte("innermost")}, - }}, - }, - }, + } + dep1 := &chart.Chart{ + Metadata: &chart.Metadata{Name: "laboratory mice"}, + Templates: []*chart.File{ + {Name: "templates/pinky", Data: []byte("pinky")}, + {Name: "templates/brain", Data: []byte("brain")}, + }, + } + ch1.AddDependency(dep1) + + dep2 := &chart.Chart{ + Metadata: &chart.Metadata{Name: "same thing we do every night"}, + Templates: []*chart.File{ + {Name: "templates/innermost", Data: []byte("innermost")}, }, } + dep1.AddDependency(dep2) - var v chartutil.Values - tpls := allTemplates(ch1, v) + tpls := allTemplates(ch1, chartutil.Values{}) if len(tpls) != 5 { t.Errorf("Expected 5 charts, got %d", len(tpls)) } } func TestRenderDependency(t *testing.T) { - e := New() deptpl := `{{define "myblock"}}World{{end}}` toptpl := `Hello {{template "myblock"}}` ch := &chart.Chart{ Metadata: &chart.Metadata{Name: "outerchart"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: "templates/outer", Data: []byte(toptpl)}, }, - Dependencies: []*chart.Chart{ - { - Metadata: &chart.Metadata{Name: "innerchart"}, - Templates: []*chart.Template{ - {Name: "templates/inner", Data: []byte(deptpl)}, - }, - }, - }, } + ch.AddDependency(&chart.Chart{ + Metadata: &chart.Metadata{Name: "innerchart"}, + Templates: []*chart.File{ + {Name: "templates/inner", Data: []byte(deptpl)}, + }, + }) - out, err := e.Render(ch, map[string]interface{}{}) - + out, err := Render(ch, map[string]interface{}{}) if err != nil { t.Fatalf("failed to render chart: %s", err) } @@ -268,8 +350,6 @@ func TestRenderDependency(t *testing.T) { } func TestRenderNestedValues(t *testing.T) { - e := New() - innerpath := "templates/inner.tpl" outerpath := "templates/outer.tpl" // Ensure namespacing rules are working. @@ -278,48 +358,50 @@ func TestRenderNestedValues(t *testing.T) { deepest := &chart.Chart{ Metadata: &chart.Metadata{Name: "deepest"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, {Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, }, - Values: &chart.Config{Raw: `what: "milkshake"`}, + Values: map[string]interface{}{"what": "milkshake"}, } inner := &chart.Chart{ Metadata: &chart.Metadata{Name: "herrick"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, }, - Values: &chart.Config{Raw: `who: "Robert"`}, - Dependencies: []*chart.Chart{deepest}, + Values: map[string]interface{}{"who": "Robert"}, } + inner.AddDependency(deepest) outer := &chart.Chart{ Metadata: &chart.Metadata{Name: "top"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, }, - Values: &chart.Config{ - Raw: ` -what: stinkweed -who: me -herrick: - who: time`, + Values: map[string]interface{}{ + "what": "stinkweed", + "who": "me", + "herrick": map[string]interface{}{ + "who": "time", + }, }, - Dependencies: []*chart.Chart{inner}, } + outer.AddDependency(inner) - injValues := chart.Config{ - Raw: ` -what: rosebuds -herrick: - deepest: - what: flower -global: - when: to-day`, + injValues := map[string]interface{}{ + "what": "rosebuds", + "herrick": map[string]interface{}{ + "deepest": map[string]interface{}{ + "what": "flower", + }, + }, + "global": map[string]interface{}{ + "when": "to-day", + }, } - tmp, err := chartutil.CoalesceValues(outer, &injValues) + tmp, err := chartutil.CoalesceValues(outer, injValues) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } @@ -334,7 +416,7 @@ global: t.Logf("Calculated values: %v", inject) - out, err := e.Render(outer, inject) + out, err := Render(outer, inject) if err != nil { t.Fatalf("failed to render templates: %s", err) } @@ -363,29 +445,26 @@ global: func TestRenderBuiltinValues(t *testing.T) { inner := &chart.Chart{ Metadata: &chart.Metadata{Name: "Latium"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: "templates/Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, {Name: "templates/From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, }, - Values: &chart.Config{Raw: ``}, - Dependencies: []*chart.Chart{}, - Files: []*any.Any{ - {TypeUrl: "author", Value: []byte("Virgil")}, - {TypeUrl: "book/title.txt", Value: []byte("Aeneid")}, + Files: []*chart.File{ + {Name: "author", Data: []byte("Virgil")}, + {Name: "book/title.txt", Data: []byte("Aeneid")}, }, } outer := &chart.Chart{ Metadata: &chart.Metadata{Name: "Troy"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, }, - Values: &chart.Config{Raw: ``}, - Dependencies: []*chart.Chart{inner}, } + outer.AddDependency(inner) inject := chartutil.Values{ - "Values": &chart.Config{Raw: ""}, + "Values": "", "Chart": outer.Metadata, "Release": chartutil.Values{ "Name": "Aeneid", @@ -394,7 +473,7 @@ func TestRenderBuiltinValues(t *testing.T) { t.Logf("Calculated values: %v", outer) - out, err := New().Render(outer, inject) + out, err := Render(outer, inject) if err != nil { t.Fatalf("failed to render templates: %s", err) } @@ -412,26 +491,33 @@ func TestRenderBuiltinValues(t *testing.T) { } -func TestAlterFuncMap(t *testing.T) { +func TestAlterFuncMap_include(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conrad"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: "templates/quote", Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, }, - Values: &chart.Config{Raw: ``}, - Dependencies: []*chart.Chart{}, + } + + // Check nested reference in include FuncMap + d := &chart.Chart{ + Metadata: &chart.Metadata{Name: "nested"}, + Templates: []*chart.File{ + {Name: "templates/quote", Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, + {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, + }, } v := chartutil.Values{ - "Values": &chart.Config{Raw: ""}, + "Values": "", "Chart": c.Metadata, "Release": chartutil.Values{ "Name": "Mistah Kurtz", }, } - out, err := New().Render(c, v) + out, err := Render(c, v) if err != nil { t.Fatal(err) } @@ -441,126 +527,214 @@ func TestAlterFuncMap(t *testing.T) { t.Errorf("Expected %q, got %q (%v)", expect, got, out) } - reqChart := &chart.Chart{ + _, err = Render(d, v) + expectErrName := "nested/templates/quote" + if err == nil { + t.Errorf("Expected err of nested reference name: %v", expectErrName) + } +} + +func TestAlterFuncMap_require(t *testing.T) { + c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conan"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: "templates/quote", Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, {Name: "templates/bases", Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, }, - Values: &chart.Config{Raw: ``}, - Dependencies: []*chart.Chart{}, } - reqValues := chartutil.Values{ + v := chartutil.Values{ "Values": chartutil.Values{ "who": "us", "bases": 2, }, - "Chart": reqChart.Metadata, + "Chart": c.Metadata, "Release": chartutil.Values{ "Name": "That 90s meme", }, } - outReq, err := New().Render(reqChart, reqValues) + out, err := Render(c, v) if err != nil { t.Fatal(err) } expectStr := "All your base are belong to us" - if gotStr := outReq["conan/templates/quote"]; gotStr != expectStr { - t.Errorf("Expected %q, got %q (%v)", expectStr, gotStr, outReq) + if gotStr := out["conan/templates/quote"]; gotStr != expectStr { + t.Errorf("Expected %q, got %q (%v)", expectStr, gotStr, out) } expectNum := "All 2 of them!" - if gotNum := outReq["conan/templates/bases"]; gotNum != expectNum { - t.Errorf("Expected %q, got %q (%v)", expectNum, gotNum, outReq) + if gotNum := out["conan/templates/bases"]; gotNum != expectNum { + t.Errorf("Expected %q, got %q (%v)", expectNum, gotNum, out) } - tplChart := &chart.Chart{ + // test required without passing in needed values with lint mode on + // verifies lint replaces required with an empty string (should not fail) + lintValues := chartutil.Values{ + "Values": chartutil.Values{ + "who": "us", + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "That 90s meme", + }, + } + var e Engine + e.LintMode = true + out, err = e.Render(c, lintValues) + if err != nil { + t.Fatal(err) + } + + expectStr = "All your base are belong to us" + if gotStr := out["conan/templates/quote"]; gotStr != expectStr { + t.Errorf("Expected %q, got %q (%v)", expectStr, gotStr, out) + } + expectNum = "All of them!" + if gotNum := out["conan/templates/bases"]; gotNum != expectNum { + t.Errorf("Expected %q, got %q (%v)", expectNum, gotNum, out) + } +} + +func TestAlterFuncMap_tpl(t *testing.T) { + c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)}, }, - Values: &chart.Config{Raw: ``}, - Dependencies: []*chart.Chart{}, } - tplValues := chartutil.Values{ + v := chartutil.Values{ "Values": chartutil.Values{ "value": "myvalue", }, - "Chart": tplChart.Metadata, + "Chart": c.Metadata, "Release": chartutil.Values{ "Name": "TestRelease", }, } - outTpl, err := New().Render(tplChart, tplValues) + out, err := Render(c, v) if err != nil { t.Fatal(err) } - expectTplStr := "Evaluate tpl Value: myvalue" - if gotStrTpl := outTpl["TplFunction/templates/base"]; gotStrTpl != expectTplStr { - t.Errorf("Expected %q, got %q (%v)", expectTplStr, gotStrTpl, outTpl) + expect := "Evaluate tpl Value: myvalue" + if got := out["TplFunction/templates/base"]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, out) } +} - tplChartWithFunction := &chart.Chart{ +func TestAlterFuncMap_tplfunc(t *testing.T) { + c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)}, }, - Values: &chart.Config{Raw: ``}, - Dependencies: []*chart.Chart{}, } - tplValuesWithFunction := chartutil.Values{ + v := chartutil.Values{ "Values": chartutil.Values{ "value": "myvalue", }, - "Chart": tplChartWithFunction.Metadata, + "Chart": c.Metadata, "Release": chartutil.Values{ "Name": "TestRelease", }, } - outTplWithFunction, err := New().Render(tplChartWithFunction, tplValuesWithFunction) + out, err := Render(c, v) if err != nil { t.Fatal(err) } - expectTplStrWithFunction := "Evaluate tpl Value: \"myvalue\"" - if gotStrTplWithFunction := outTplWithFunction["TplFunction/templates/base"]; gotStrTplWithFunction != expectTplStrWithFunction { - t.Errorf("Expected %q, got %q (%v)", expectTplStrWithFunction, gotStrTplWithFunction, outTplWithFunction) + expect := "Evaluate tpl Value: \"myvalue\"" + if got := out["TplFunction/templates/base"]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, out) } +} - tplChartWithInclude := &chart.Chart{ +func TestAlterFuncMap_tplinclude(t *testing.T) { + c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.Template{ + Templates: []*chart.File{ {Name: "templates/base", Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, {Name: "templates/_partial", Data: []byte(`{{.Template.Name}}`)}, }, - Values: &chart.Config{Raw: ``}, - Dependencies: []*chart.Chart{}, } - tplValueWithInclude := chartutil.Values{ + v := chartutil.Values{ "Values": chartutil.Values{ "value": "myvalue", }, - "Chart": tplChartWithInclude.Metadata, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expect := "\"TplFunction/templates/base\"" + if got := out["TplFunction/templates/base"]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, out) + } + +} + +func TestRenderRecursionLimit(t *testing.T) { + // endless recursion should produce an error + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "bad"}, + Templates: []*chart.File{ + {Name: "templates/base", Data: []byte(`{{include "recursion" . }}`)}, + {Name: "templates/recursion", Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, + }, + } + v := chartutil.Values{ + "Values": "", + "Chart": c.Metadata, "Release": chartutil.Values{ "Name": "TestRelease", }, } + expectErr := "rendering template has a nested reference name: recursion: unable to execute template" + + _, err := Render(c, v) + if err == nil || !strings.HasSuffix(err.Error(), expectErr) { + t.Errorf("Expected err with suffix: %s", expectErr) + } + + // calling the same function many times is ok + times := 4000 + phrase := "All work and no play makes Jack a dull boy" + printFunc := `{{define "overlook"}}{{printf "` + phrase + `\n"}}{{end}}` + var repeatedIncl string + for i := 0; i < times; i++ { + repeatedIncl += `{{include "overlook" . }}` + } + + d := &chart.Chart{ + Metadata: &chart.Metadata{Name: "overlook"}, + Templates: []*chart.File{ + {Name: "templates/quote", Data: []byte(repeatedIncl)}, + {Name: "templates/_function", Data: []byte(printFunc)}, + }, + } - outTplWithInclude, err := New().Render(tplChartWithInclude, tplValueWithInclude) + out, err := Render(d, v) if err != nil { t.Fatal(err) } - expectedTplStrWithInclude := "\"TplFunction/templates/base\"" - if gotStrTplWithInclude := outTplWithInclude["TplFunction/templates/base"]; gotStrTplWithInclude != expectedTplStrWithInclude { - t.Errorf("Expected %q, got %q (%v)", expectedTplStrWithInclude, gotStrTplWithInclude, outTplWithInclude) + var expect string + for i := 0; i < times; i++ { + expect += phrase + "\n" + } + if got := out["overlook/templates/quote"]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, out) } } diff --git a/pkg/engine/files.go b/pkg/engine/files.go new file mode 100644 index 00000000000..d7e62da5a2a --- /dev/null +++ b/pkg/engine/files.go @@ -0,0 +1,160 @@ +/* +Copyright The Helm Authors. + +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 engine + +import ( + "encoding/base64" + "path" + "strings" + + "github.com/gobwas/glob" + + "helm.sh/helm/v3/pkg/chart" +) + +// files is a map of files in a chart that can be accessed from a template. +type files map[string][]byte + +// NewFiles creates a new files from chart files. +// Given an []*chart.File (the format for files in a chart.Chart), extract a map of files. +func newFiles(from []*chart.File) files { + files := make(map[string][]byte) + for _, f := range from { + files[f.Name] = f.Data + } + return files +} + +// GetBytes gets a file by path. +// +// The returned data is raw. In a template context, this is identical to calling +// {{index .Files $path}}. +// +// This is intended to be accessed from within a template, so a missed key returns +// an empty []byte. +func (f files) GetBytes(name string) []byte { + if v, ok := f[name]; ok { + return v + } + return []byte{} +} + +// Get returns a string representation of the given file. +// +// Fetch the contents of a file as a string. It is designed to be called in a +// template. +// +// {{.Files.Get "foo"}} +func (f files) Get(name string) string { + return string(f.GetBytes(name)) +} + +// Glob takes a glob pattern and returns another files object only containing +// matched files. +// +// This is designed to be called from a template. +// +// {{ range $name, $content := .Files.Glob("foo/**") }} +// {{ $name }}: | +// {{ .Files.Get($name) | indent 4 }}{{ end }} +func (f files) Glob(pattern string) files { + g, err := glob.Compile(pattern, '/') + if err != nil { + g, _ = glob.Compile("**") + } + + nf := newFiles(nil) + for name, contents := range f { + if g.Match(name) { + nf[name] = contents + } + } + + return nf +} + +// AsConfig turns a Files group and flattens it to a YAML map suitable for +// including in the 'data' section of a Kubernetes ConfigMap definition. +// Duplicate keys will be overwritten, so be aware that your file names +// (regardless of path) should be unique. +// +// This is designed to be called from a template, and will return empty string +// (via toYAML function) if it cannot be serialized to YAML, or if the Files +// object is nil. +// +// The output will not be indented, so you will want to pipe this to the +// 'indent' template function. +// +// data: +// {{ .Files.Glob("config/**").AsConfig() | indent 4 }} +func (f files) AsConfig() string { + if f == nil { + return "" + } + + m := make(map[string]string) + + // Explicitly convert to strings, and file names + for k, v := range f { + m[path.Base(k)] = string(v) + } + + return toYAML(m) +} + +// AsSecrets returns the base64-encoded value of a Files object suitable for +// including in the 'data' section of a Kubernetes Secret definition. +// Duplicate keys will be overwritten, so be aware that your file names +// (regardless of path) should be unique. +// +// This is designed to be called from a template, and will return empty string +// (via toYAML function) if it cannot be serialized to YAML, or if the Files +// object is nil. +// +// The output will not be indented, so you will want to pipe this to the +// 'indent' template function. +// +// data: +// {{ .Files.Glob("secrets/*").AsSecrets() }} +func (f files) AsSecrets() string { + if f == nil { + return "" + } + + m := make(map[string]string) + + for k, v := range f { + m[path.Base(k)] = base64.StdEncoding.EncodeToString(v) + } + + return toYAML(m) +} + +// Lines returns each line of a named file (split by "\n") as a slice, so it can +// be ranged over in your templates. +// +// This is designed to be called from a template. +// +// {{ range .Files.Lines "foo/bar.html" }} +// {{ . }}{{ end }} +func (f files) Lines(path string) []string { + if f == nil || f[path] == nil { + return []string{} + } + + return strings.Split(string(f[path]), "\n") +} diff --git a/pkg/engine/files_test.go b/pkg/engine/files_test.go new file mode 100644 index 00000000000..4b37724f9a0 --- /dev/null +++ b/pkg/engine/files_test.go @@ -0,0 +1,98 @@ +/* +Copyright The Helm Authors. +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 engine + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var cases = []struct { + path, data string +}{ + {"ship/captain.txt", "The Captain"}, + {"ship/stowaway.txt", "Legatt"}, + {"story/name.txt", "The Secret Sharer"}, + {"story/author.txt", "Joseph Conrad"}, + {"multiline/test.txt", "bar\nfoo"}, +} + +func getTestFiles() files { + a := make(files, len(cases)) + for _, c := range cases { + a[c.path] = []byte(c.data) + } + return a +} + +func TestNewFiles(t *testing.T) { + files := getTestFiles() + if len(files) != len(cases) { + t.Errorf("Expected len() = %d, got %d", len(cases), len(files)) + } + + for i, f := range cases { + if got := string(files.GetBytes(f.path)); got != f.data { + t.Errorf("%d: expected %q, got %q", i, f.data, got) + } + if got := files.Get(f.path); got != f.data { + t.Errorf("%d: expected %q, got %q", i, f.data, got) + } + } +} + +func TestFileGlob(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + matched := f.Glob("story/**") + + as.Len(matched, 2, "Should be two files in glob story/**") + as.Equal("Joseph Conrad", matched.Get("story/author.txt")) +} + +func TestToConfig(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + out := f.Glob("**/captain.txt").AsConfig() + as.Equal("captain.txt: The Captain", out) + + out = f.Glob("ship/**").AsConfig() + as.Equal("captain.txt: The Captain\nstowaway.txt: Legatt", out) +} + +func TestToSecret(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + out := f.Glob("ship/**").AsSecrets() + as.Equal("captain.txt: VGhlIENhcHRhaW4=\nstowaway.txt: TGVnYXR0", out) +} + +func TestLines(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + out := f.Lines("multiline/test.txt") + as.Len(out, 2) + + as.Equal("bar", out[0]) +} diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go new file mode 100644 index 00000000000..92b4c3383e2 --- /dev/null +++ b/pkg/engine/funcs.go @@ -0,0 +1,177 @@ +/* +Copyright The Helm Authors. + +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 engine + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" + + "github.com/BurntSushi/toml" + "github.com/Masterminds/sprig/v3" + "sigs.k8s.io/yaml" +) + +// funcMap returns a mapping of all of the functions that Engine has. +// +// Because some functions are late-bound (e.g. contain context-sensitive +// data), the functions may not all perform identically outside of an Engine +// as they will inside of an Engine. +// +// Known late-bound functions: +// +// - "include" +// - "tpl" +// +// These are late-bound in Engine.Render(). The +// version included in the FuncMap is a placeholder. +// +func funcMap() template.FuncMap { + f := sprig.TxtFuncMap() + delete(f, "env") + delete(f, "expandenv") + + // Add some extra functionality + extra := template.FuncMap{ + "toToml": toTOML, + "toYaml": toYAML, + "fromYaml": fromYAML, + "fromYamlArray": fromYAMLArray, + "toJson": toJSON, + "fromJson": fromJSON, + "fromJsonArray": fromJSONArray, + + // This is a placeholder for the "include" function, which is + // late-bound to a template. By declaring it here, we preserve the + // integrity of the linter. + "include": func(string, interface{}) string { return "not implemented" }, + "tpl": func(string, interface{}) interface{} { return "not implemented" }, + "required": func(string, interface{}) (interface{}, error) { return "not implemented", nil }, + // Provide a placeholder for the "lookup" function, which requires a kubernetes + // connection. + "lookup": func(string, string, string, string) (map[string]interface{}, error) { + return map[string]interface{}{}, nil + }, + } + + for k, v := range extra { + f[k] = v + } + + return f +} + +// toYAML takes an interface, marshals it to yaml, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toYAML(v interface{}) string { + data, err := yaml.Marshal(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return strings.TrimSuffix(string(data), "\n") +} + +// fromYAML converts a YAML document into a map[string]interface{}. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromYAML(str string) map[string]interface{} { + m := map[string]interface{}{} + + if err := yaml.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + +// fromYAMLArray converts a YAML array into a []interface{}. +// +// This is not a general-purpose YAML parser, and will not parse all valid +// YAML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromYAMLArray(str string) []interface{} { + a := []interface{}{} + + if err := yaml.Unmarshal([]byte(str), &a); err != nil { + a = []interface{}{err.Error()} + } + return a +} + +// toTOML takes an interface, marshals it to toml, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toTOML(v interface{}) string { + b := bytes.NewBuffer(nil) + e := toml.NewEncoder(b) + err := e.Encode(v) + if err != nil { + return err.Error() + } + return b.String() +} + +// toJSON takes an interface, marshals it to json, and returns a string. It will +// always return a string, even on marshal error (empty string). +// +// This is designed to be called from a template. +func toJSON(v interface{}) string { + data, err := json.Marshal(v) + if err != nil { + // Swallow errors inside of a template. + return "" + } + return string(data) +} + +// fromJSON converts a JSON document into a map[string]interface{}. +// +// This is not a general-purpose JSON parser, and will not parse all valid +// JSON documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromJSON(str string) map[string]interface{} { + m := make(map[string]interface{}) + + if err := json.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + +// fromJSONArray converts a JSON array into a []interface{}. +// +// This is not a general-purpose JSON parser, and will not parse all valid +// JSON documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string as +// the first and only item in the returned array. +func fromJSONArray(str string) []interface{} { + a := []interface{}{} + + if err := json.Unmarshal([]byte(str), &a); err != nil { + a = []interface{}{err.Error()} + } + return a +} diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go new file mode 100644 index 00000000000..62c63ec2bcd --- /dev/null +++ b/pkg/engine/funcs_test.go @@ -0,0 +1,178 @@ +/* +Copyright The Helm Authors. + +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 engine + +import ( + "strings" + "testing" + "text/template" + + "github.com/stretchr/testify/assert" +) + +func TestFuncs(t *testing.T) { + //TODO write tests for failure cases + tests := []struct { + tpl, expect string + vars interface{} + }{{ + tpl: `{{ toYaml . }}`, + expect: `foo: bar`, + vars: map[string]interface{}{"foo": "bar"}, + }, { + tpl: `{{ toToml . }}`, + expect: "foo = \"bar\"\n", + vars: map[string]interface{}{"foo": "bar"}, + }, { + tpl: `{{ toJson . }}`, + expect: `{"foo":"bar"}`, + vars: map[string]interface{}{"foo": "bar"}, + }, { + tpl: `{{ fromYaml . }}`, + expect: "map[hello:world]", + vars: `hello: world`, + }, { + tpl: `{{ fromYamlArray . }}`, + expect: "[one 2 map[name:helm]]", + vars: "- one\n- 2\n- name: helm\n", + }, { + tpl: `{{ fromYamlArray . }}`, + expect: "[one 2 map[name:helm]]", + vars: `["one", 2, { "name": "helm" }]`, + }, { + // Regression for https://github.com/helm/helm/issues/2271 + tpl: `{{ toToml . }}`, + expect: "[mast]\n sail = \"white\"\n", + vars: map[string]map[string]string{"mast": {"sail": "white"}}, + }, { + tpl: `{{ fromYaml . }}`, + expect: "map[Error:error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type map[string]interface {}]", + vars: "- one\n- two\n", + }, { + tpl: `{{ fromJson .}}`, + expect: `map[hello:world]`, + vars: `{"hello":"world"}`, + }, { + tpl: `{{ fromJson . }}`, + expect: `map[Error:json: cannot unmarshal array into Go value of type map[string]interface {}]`, + vars: `["one", "two"]`, + }, { + tpl: `{{ fromJsonArray . }}`, + expect: `[one 2 map[name:helm]]`, + vars: `["one", 2, { "name": "helm" }]`, + }, { + tpl: `{{ fromJsonArray . }}`, + expect: `[json: cannot unmarshal object into Go value of type []interface {}]`, + vars: `{"hello": "world"}`, + }, { + tpl: `{{ merge .dict (fromYaml .yaml) }}`, + expect: `map[a:map[b:c]]`, + vars: map[string]interface{}{"dict": map[string]interface{}{"a": map[string]interface{}{"b": "c"}}, "yaml": `{"a":{"b":"d"}}`}, + }, { + tpl: `{{ merge (fromYaml .yaml) .dict }}`, + expect: `map[a:map[b:d]]`, + vars: map[string]interface{}{"dict": map[string]interface{}{"a": map[string]interface{}{"b": "c"}}, "yaml": `{"a":{"b":"d"}}`}, + }, { + tpl: `{{ fromYaml . }}`, + expect: `map[Error:error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go value of type map[string]interface {}]`, + vars: `["one", "two"]`, + }, { + tpl: `{{ fromYamlArray . }}`, + expect: `[error unmarshaling JSON: while decoding JSON: json: cannot unmarshal object into Go value of type []interface {}]`, + vars: `hello: world`, + }, { + // This should never result in a network lookup. Regression for #7955 + tpl: `{{ lookup "v1" "Namespace" "" "unlikelynamespace99999999" }}`, + expect: `map[]`, + vars: `["one", "two"]`, + }} + + for _, tt := range tests { + var b strings.Builder + err := template.Must(template.New("test").Funcs(funcMap()).Parse(tt.tpl)).Execute(&b, tt.vars) + assert.NoError(t, err) + assert.Equal(t, tt.expect, b.String(), tt.tpl) + } +} + +// This test to check a function provided by sprig is due to a change in a +// dependency of sprig. mergo in v0.3.9 changed the way it merges and only does +// public fields (i.e. those starting with a capital letter). This test, from +// sprig, fails in the new version. This is a behavior change for mergo that +// impacts sprig and Helm users. This test will help us to not update to a +// version of mergo (even accidentally) that causes a breaking change. See +// sprig changelog and notes for more details. +// Note, Go modules assume semver is never broken. So, there is no way to tell +// the tooling to not update to a minor or patch version. `go get -u` could be +// used to accidentally update mergo. This test and message should catch the +// problem and explain why it's happening. +func TestMerge(t *testing.T) { + dict := map[string]interface{}{ + "src2": map[string]interface{}{ + "h": 10, + "i": "i", + "j": "j", + }, + "src1": map[string]interface{}{ + "a": 1, + "b": 2, + "d": map[string]interface{}{ + "e": "four", + }, + "g": []int{6, 7}, + "i": "aye", + "j": "jay", + "k": map[string]interface{}{ + "l": false, + }, + }, + "dst": map[string]interface{}{ + "a": "one", + "c": 3, + "d": map[string]interface{}{ + "f": 5, + }, + "g": []int{8, 9}, + "i": "eye", + "k": map[string]interface{}{ + "l": true, + }, + }, + } + tpl := `{{merge .dst .src1 .src2}}` + var b strings.Builder + err := template.Must(template.New("test").Funcs(funcMap()).Parse(tpl)).Execute(&b, dict) + assert.NoError(t, err) + + expected := map[string]interface{}{ + "a": "one", // key overridden + "b": 2, // merged from src1 + "c": 3, // merged from dst + "d": map[string]interface{}{ // deep merge + "e": "four", + "f": 5, + }, + "g": []int{8, 9}, // overridden - arrays are not merged + "h": 10, // merged from src2 + "i": "eye", // overridden twice + "j": "jay", // overridden and merged + "k": map[string]interface{}{ + "l": true, // overridden + }, + } + assert.Equal(t, expected, dict["dst"]) +} diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go new file mode 100644 index 00000000000..cd3883c0f10 --- /dev/null +++ b/pkg/engine/lookup_func.go @@ -0,0 +1,124 @@ +/* +Copyright The Helm Authors. + +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 engine + +import ( + "context" + "log" + "strings" + + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" +) + +type lookupFunc = func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) + +// NewLookupFunction returns a function for looking up objects in the cluster. +// +// If the resource does not exist, no error is raised. +// +// This function is considered deprecated, and will be renamed in Helm 4. It will no +// longer be a public function. +func NewLookupFunction(config *rest.Config) lookupFunc { + return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) { + var client dynamic.ResourceInterface + c, namespaced, err := getDynamicClientOnKind(apiversion, resource, config) + if err != nil { + return map[string]interface{}{}, err + } + if namespaced && namespace != "" { + client = c.Namespace(namespace) + } else { + client = c + } + if name != "" { + // this will return a single object + obj, err := client.Get(context.Background(), name, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + // Just return an empty interface when the object was not found. + // That way, users can use `if not (lookup ...)` in their templates. + return map[string]interface{}{}, nil + } + return map[string]interface{}{}, err + } + return obj.UnstructuredContent(), nil + } + //this will return a list + obj, err := client.List(context.Background(), metav1.ListOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + // Just return an empty interface when the object was not found. + // That way, users can use `if not (lookup ...)` in their templates. + return map[string]interface{}{}, nil + } + return map[string]interface{}{}, err + } + return obj.UnstructuredContent(), nil + } +} + +// getDynamicClientOnUnstructured returns a dynamic client on an Unstructured type. This client can be further namespaced. +func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) { + gvk := schema.FromAPIVersionAndKind(apiversion, kind) + apiRes, err := getAPIResourceForGVK(gvk, config) + if err != nil { + log.Printf("[ERROR] unable to get apiresource from unstructured: %s , error %s", gvk.String(), err) + return nil, false, errors.Wrapf(err, "unable to get apiresource from unstructured: %s", gvk.String()) + } + gvr := schema.GroupVersionResource{ + Group: apiRes.Group, + Version: apiRes.Version, + Resource: apiRes.Name, + } + intf, err := dynamic.NewForConfig(config) + if err != nil { + log.Printf("[ERROR] unable to get dynamic client %s", err) + return nil, false, err + } + res := intf.Resource(gvr) + return res, apiRes.Namespaced, nil +} + +func getAPIResourceForGVK(gvk schema.GroupVersionKind, config *rest.Config) (metav1.APIResource, error) { + res := metav1.APIResource{} + discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) + if err != nil { + log.Printf("[ERROR] unable to create discovery client %s", err) + return res, err + } + resList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) + if err != nil { + log.Printf("[ERROR] unable to retrieve resource list for: %s , error: %s", gvk.GroupVersion().String(), err) + return res, err + } + for _, resource := range resList.APIResources { + //if a resource contains a "/" it's referencing a subresource. we don't support suberesource for now. + if resource.Kind == gvk.Kind && !strings.Contains(resource.Name, "/") { + res = resource + res.Group = gvk.Group + res.Version = gvk.Version + break + } + } + return res, nil +} diff --git a/pkg/gates/doc.go b/pkg/gates/doc.go new file mode 100644 index 00000000000..762fdb8c660 --- /dev/null +++ b/pkg/gates/doc.go @@ -0,0 +1,20 @@ +/* +Copyright The Helm Authors. +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 gates provides a general tool for working with experimental feature gates. + +This provides convenience methods where the user can determine if certain experimental features are enabled. +*/ +package gates diff --git a/pkg/gates/gates.go b/pkg/gates/gates.go new file mode 100644 index 00000000000..69559219e7f --- /dev/null +++ b/pkg/gates/gates.go @@ -0,0 +1,38 @@ +/* +Copyright The Helm Authors. +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 gates + +import ( + "fmt" + "os" +) + +// Gate is the name of the feature gate. +type Gate string + +// String returns the string representation of this feature gate. +func (g Gate) String() string { + return string(g) +} + +// IsEnabled determines whether a certain feature gate is enabled. +func (g Gate) IsEnabled() bool { + return os.Getenv(string(g)) != "" +} + +func (g Gate) Error() error { + return fmt.Errorf("this feature has been marked as experimental and is not enabled by default. Please set %s=1 in your environment to use this feature", g.String()) +} diff --git a/pkg/gates/gates_test.go b/pkg/gates/gates_test.go new file mode 100644 index 00000000000..6bdd17ed6d7 --- /dev/null +++ b/pkg/gates/gates_test.go @@ -0,0 +1,56 @@ +/* +Copyright The Helm Authors. +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 gates + +import ( + "os" + "testing" +) + +const name string = "HELM_EXPERIMENTAL_FEATURE" + +func TestIsEnabled(t *testing.T) { + os.Unsetenv(name) + g := Gate(name) + + if g.IsEnabled() { + t.Errorf("feature gate shows as available, but the environment variable %s was not set", name) + } + + os.Setenv(name, "1") + + if !g.IsEnabled() { + t.Errorf("feature gate shows as disabled, but the environment variable %s was set", name) + } +} + +func TestError(t *testing.T) { + os.Unsetenv(name) + g := Gate(name) + + if g.Error().Error() != "this feature has been marked as experimental and is not enabled by default. Please set HELM_EXPERIMENTAL_FEATURE=1 in your environment to use this feature" { + t.Errorf("incorrect error message. Received %s", g.Error().Error()) + } +} + +func TestString(t *testing.T) { + os.Unsetenv(name) + g := Gate(name) + + if g.String() != "HELM_EXPERIMENTAL_FEATURE" { + t.Errorf("incorrect string representation. Received %s", g.String()) + } +} diff --git a/pkg/getter/doc.go b/pkg/getter/doc.go index fe51e49670a..c53ef1ae02b 100644 --- a/pkg/getter/doc.go +++ b/pkg/getter/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index ca018884a01..46534845602 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,20 +18,109 @@ package getter import ( "bytes" - "fmt" + "time" - "k8s.io/helm/pkg/helm/environment" + "github.com/pkg/errors" + + "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/pkg/cli" ) +// options are generic parameters to be provided to the getter during instantiation. +// +// Getters may or may not ignore these parameters as they are passed in. +type options struct { + url string + certFile string + keyFile string + caFile string + unTar bool + insecureSkipVerifyTLS bool + username string + password string + userAgent string + version string + registryClient *registry.Client + timeout time.Duration +} + +// Option allows specifying various settings configurable by the user for overriding the defaults +// used when performing Get operations with the Getter. +type Option func(*options) + +// WithURL informs the getter the server name that will be used when fetching objects. Used in conjunction with +// WithTLSClientConfig to set the TLSClientConfig's server name. +func WithURL(url string) Option { + return func(opts *options) { + opts.url = url + } +} + +// WithBasicAuth sets the request's Authorization header to use the provided credentials +func WithBasicAuth(username, password string) Option { + return func(opts *options) { + opts.username = username + opts.password = password + } +} + +// WithUserAgent sets the request's User-Agent header to use the provided agent name. +func WithUserAgent(userAgent string) Option { + return func(opts *options) { + opts.userAgent = userAgent + } +} + +// WithInsecureSkipVerifyTLS determines if a TLS Certificate will be checked +func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) Option { + return func(opts *options) { + opts.insecureSkipVerifyTLS = insecureSkipVerifyTLS + } +} + +// WithTLSClientConfig sets the client auth with the provided credentials. +func WithTLSClientConfig(certFile, keyFile, caFile string) Option { + return func(opts *options) { + opts.certFile = certFile + opts.keyFile = keyFile + opts.caFile = caFile + } +} + +// WithTimeout sets the timeout for requests +func WithTimeout(timeout time.Duration) Option { + return func(opts *options) { + opts.timeout = timeout + } +} + +func WithTagName(tagname string) Option { + return func(opts *options) { + opts.version = tagname + } +} + +func WithRegistryClient(client *registry.Client) Option { + return func(opts *options) { + opts.registryClient = client + } +} + +func WithUntar() Option { + return func(opts *options) { + opts.unTar = true + } +} + // Getter is an interface to support GET to the specified URL. type Getter interface { - //Get file content by url string - Get(url string) (*bytes.Buffer, error) + // Get file content by url string + Get(url string, options ...Option) (*bytes.Buffer, error) } // Constructor is the function for every getter which creates a specific instance // according to the configuration -type Constructor func(URL, CertFile, KeyFile, CAFile string) (Getter, error) +type Constructor func(options ...Option) (Getter, error) // Provider represents any getter and the schemes that it supports. // @@ -58,41 +147,31 @@ type Providers []Provider // ByScheme returns a Provider that handles the given scheme. // // If no provider handles this scheme, this will return an error. -func (p Providers) ByScheme(scheme string) (Constructor, error) { +func (p Providers) ByScheme(scheme string) (Getter, error) { for _, pp := range p { if pp.Provides(scheme) { - return pp.New, nil + return pp.New() } } - return nil, fmt.Errorf("scheme %q not supported", scheme) + return nil, errors.Errorf("scheme %q not supported", scheme) +} + +var httpProvider = Provider{ + Schemes: []string{"http", "https"}, + New: NewHTTPGetter, +} + +var ociProvider = Provider{ + Schemes: []string{"oci"}, + New: NewOCIGetter, } // All finds all of the registered getters as a list of Provider instances. -// Currently the build-in http/https getter and the discovered -// plugins with downloader notations are collected. -func All(settings environment.EnvSettings) Providers { - result := Providers{ - { - Schemes: []string{"http", "https"}, - New: newHTTPGetter, - }, - } +// Currently, the built-in getters and the discovered plugins with downloader +// notations are collected. +func All(settings *cli.EnvSettings) Providers { + result := Providers{httpProvider, ociProvider} pluginDownloaders, _ := collectPlugins(settings) result = append(result, pluginDownloaders...) return result } - -// ByScheme returns a getter for the given scheme. -// -// If the scheme is not supported, this will return an error. -func ByScheme(scheme string, settings environment.EnvSettings) (Provider, error) { - // Q: What do you call a scheme string who's the boss? - // A: Bruce Schemestring, of course. - a := All(settings) - for _, p := range a { - if p.Provides(scheme) { - return p, nil - } - } - return Provider{}, fmt.Errorf("scheme %q not supported", scheme) -} diff --git a/pkg/getter/getter_test.go b/pkg/getter/getter_test.go index 6d38a0d28dd..ab14784abfb 100644 --- a/pkg/getter/getter_test.go +++ b/pkg/getter/getter_test.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,14 +16,17 @@ limitations under the License. package getter import ( - "os" "testing" + + "helm.sh/helm/v3/pkg/cli" ) +const pluginDir = "testdata/plugins" + func TestProvider(t *testing.T) { p := Provider{ []string{"one", "three"}, - func(h, e, l, m string) (Getter, error) { return nil, nil }, + func(_ ...Option) (Getter, error) { return nil, nil }, } if !p.Provides("three") { @@ -33,8 +36,8 @@ func TestProvider(t *testing.T) { func TestProviders(t *testing.T) { ps := Providers{ - {[]string{"one", "three"}, func(h, e, l, m string) (Getter, error) { return nil, nil }}, - {[]string{"two", "four"}, func(h, e, l, m string) (Getter, error) { return nil, nil }}, + {[]string{"one", "three"}, func(_ ...Option) (Getter, error) { return nil, nil }}, + {[]string{"two", "four"}, func(_ ...Option) (Getter, error) { return nil, nil }}, } if _, err := ps.ByScheme("one"); err != nil { @@ -50,15 +53,12 @@ func TestProviders(t *testing.T) { } func TestAll(t *testing.T) { - oldhh := os.Getenv("HELM_HOME") - defer os.Setenv("HELM_HOME", oldhh) - os.Setenv("HELM_HOME", "") - - env := hh(false) + env := cli.New() + env.PluginsDirectory = pluginDir all := All(env) - if len(all) != 3 { - t.Errorf("expected 3 providers (default plus two plugins), got %d", len(all)) + if len(all) != 4 { + t.Errorf("expected 4 providers (default plus three plugins), got %d", len(all)) } if _, err := all.ByScheme("test2"); err != nil { @@ -67,15 +67,14 @@ func TestAll(t *testing.T) { } func TestByScheme(t *testing.T) { - oldhh := os.Getenv("HELM_HOME") - defer os.Setenv("HELM_HOME", oldhh) - os.Setenv("HELM_HOME", "") + env := cli.New() + env.PluginsDirectory = pluginDir - env := hh(false) - if _, err := ByScheme("test", env); err != nil { + g := All(env) + if _, err := g.ByScheme("test"); err != nil { t.Error(err) } - if _, err := ByScheme("https", env); err != nil { + if _, err := g.ByScheme("https"); err != nil { t.Error(err) } } diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 3c20e35e199..bd60629ae87 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -17,36 +17,31 @@ package getter import ( "bytes" - "fmt" + "crypto/tls" "io" "net/http" - "strings" - "k8s.io/helm/pkg/tlsutil" - "k8s.io/helm/pkg/urlutil" - "k8s.io/helm/pkg/version" -) + "github.com/pkg/errors" -//HttpGetter is the efault HTTP(/S) backend handler -// TODO: change the name to HTTPGetter in Helm 3 -type HttpGetter struct { //nolint - client *http.Client - username string - password string -} + "helm.sh/helm/v3/internal/tlsutil" + "helm.sh/helm/v3/internal/urlutil" + "helm.sh/helm/v3/internal/version" +) -//SetCredentials sets the credentials for the getter -func (g *HttpGetter) SetCredentials(username, password string) { - g.username = username - g.password = password +// HTTPGetter is the default HTTP(/S) backend handler +type HTTPGetter struct { + opts options } //Get performs a Get from repo.Getter and returns the body. -func (g *HttpGetter) Get(href string) (*bytes.Buffer, error) { +func (g *HTTPGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { + for _, opt := range options { + opt(&g.opts) + } return g.get(href) } -func (g *HttpGetter) get(href string) (*bytes.Buffer, error) { +func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) { buf := bytes.NewBuffer(nil) // Set a helm specific user agent so that a repo server and metrics can @@ -55,18 +50,27 @@ func (g *HttpGetter) get(href string) (*bytes.Buffer, error) { if err != nil { return buf, err } - req.Header.Set("User-Agent", "Helm/"+strings.TrimPrefix(version.GetVersion(), "v")) - if g.username != "" && g.password != "" { - req.SetBasicAuth(g.username, g.password) + req.Header.Set("User-Agent", version.GetUserAgent()) + if g.opts.userAgent != "" { + req.Header.Set("User-Agent", g.opts.userAgent) + } + + if g.opts.username != "" && g.opts.password != "" { + req.SetBasicAuth(g.opts.username, g.opts.password) } - resp, err := g.client.Do(req) + client, err := g.httpClient() + if err != nil { + return nil, err + } + + resp, err := client.Do(req) if err != nil { return buf, err } if resp.StatusCode != 200 { - return buf, fmt.Errorf("Failed to fetch %s : %s", href, resp.Status) + return buf, errors.Errorf("failed to fetch %s : %s", href, resp.Status) } _, err = io.Copy(buf, resp.Body) @@ -74,35 +78,52 @@ func (g *HttpGetter) get(href string) (*bytes.Buffer, error) { return buf, err } -// newHTTPGetter constructs a valid http/https client as Getter -func newHTTPGetter(URL, CertFile, KeyFile, CAFile string) (Getter, error) { - return NewHTTPGetter(URL, CertFile, KeyFile, CAFile) +// NewHTTPGetter constructs a valid http/https client as a Getter +func NewHTTPGetter(options ...Option) (Getter, error) { + var client HTTPGetter + + for _, opt := range options { + opt(&client.opts) + } + + return &client, nil } -// NewHTTPGetter constructs a valid http/https client as HttpGetter -func NewHTTPGetter(URL, CertFile, KeyFile, CAFile string) (*HttpGetter, error) { - var client HttpGetter - if CertFile != "" && KeyFile != "" { - tlsConf, err := tlsutil.NewClientTLS(CertFile, KeyFile, CAFile) +func (g *HTTPGetter) httpClient() (*http.Client, error) { + transport := &http.Transport{ + DisableCompression: true, + Proxy: http.ProxyFromEnvironment, + } + if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" { + tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile) if err != nil { - return &client, fmt.Errorf("can't create TLS config for client: %s", err.Error()) + return nil, errors.Wrap(err, "can't create TLS config for client") } tlsConf.BuildNameToCertificate() - sni, err := urlutil.ExtractHostname(URL) + sni, err := urlutil.ExtractHostname(g.opts.url) if err != nil { - return &client, err + return nil, err } tlsConf.ServerName = sni - client.client = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConf, - Proxy: http.ProxyFromEnvironment, - }, + transport.TLSClientConfig = tlsConf + } + + if g.opts.insecureSkipVerifyTLS { + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } else { + transport.TLSClientConfig.InsecureSkipVerify = true } - } else { - client.client = http.DefaultClient } - return &client, nil + + client := &http.Client{ + Transport: transport, + Timeout: g.opts.timeout, + } + + return client, nil } diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index fe3fde22a94..ad97898cb81 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,33 +16,332 @@ limitations under the License. package getter import ( + "fmt" + "io" "net/http" + "net/http/httptest" + "net/url" + "os" "path/filepath" + "strconv" + "strings" "testing" + "time" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/internal/tlsutil" + "helm.sh/helm/v3/internal/version" + "helm.sh/helm/v3/pkg/cli" ) func TestHTTPGetter(t *testing.T) { - g, err := newHTTPGetter("http://example.com", "", "", "") + g, err := NewHTTPGetter(WithURL("http://example.com")) if err != nil { t.Fatal(err) } - if hg, ok := g.(*HttpGetter); !ok { - t.Fatal("Expected newHTTPGetter to produce an httpGetter") - } else if hg.client != http.DefaultClient { - t.Fatal("Expected newHTTPGetter to return a default HTTP client.") + if _, ok := g.(*HTTPGetter); !ok { + t.Fatal("Expected NewHTTPGetter to produce an *HTTPGetter") } - // Test with SSL: cd := "../../testdata" join := filepath.Join - ca, pub, priv := join(cd, "ca.pem"), join(cd, "crt.pem"), join(cd, "key.pem") - g, err = newHTTPGetter("http://example.com/", pub, priv, ca) + ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem") + insecure := false + timeout := time.Second * 5 + + // Test with options + g, err = NewHTTPGetter( + WithBasicAuth("I", "Am"), + WithUserAgent("Groot"), + WithTLSClientConfig(pub, priv, ca), + WithInsecureSkipVerifyTLS(insecure), + WithTimeout(timeout), + ) + if err != nil { + t.Fatal(err) + } + + hg, ok := g.(*HTTPGetter) + if !ok { + t.Fatal("expected NewHTTPGetter to produce an *HTTPGetter") + } + + if hg.opts.username != "I" { + t.Errorf("Expected NewHTTPGetter to contain %q as the username, got %q", "I", hg.opts.username) + } + + if hg.opts.password != "Am" { + t.Errorf("Expected NewHTTPGetter to contain %q as the password, got %q", "Am", hg.opts.password) + } + + if hg.opts.userAgent != "Groot" { + t.Errorf("Expected NewHTTPGetter to contain %q as the user agent, got %q", "Groot", hg.opts.userAgent) + } + + if hg.opts.certFile != pub { + t.Errorf("Expected NewHTTPGetter to contain %q as the public key file, got %q", pub, hg.opts.certFile) + } + + if hg.opts.keyFile != priv { + t.Errorf("Expected NewHTTPGetter to contain %q as the private key file, got %q", priv, hg.opts.keyFile) + } + + if hg.opts.caFile != ca { + t.Errorf("Expected NewHTTPGetter to contain %q as the CA file, got %q", ca, hg.opts.caFile) + } + + if hg.opts.insecureSkipVerifyTLS != insecure { + t.Errorf("Expected NewHTTPGetter to contain %t as InsecureSkipVerifyTLs flag, got %t", false, hg.opts.insecureSkipVerifyTLS) + } + + if hg.opts.timeout != timeout { + t.Errorf("Expected NewHTTPGetter to contain %s as Timeout flag, got %s", timeout, hg.opts.timeout) + } + + // Test if setting insecureSkipVerifyTLS is being passed to the ops + insecure = true + + g, err = NewHTTPGetter( + WithInsecureSkipVerifyTLS(insecure), + ) if err != nil { t.Fatal(err) } - if _, ok := g.(*HttpGetter); !ok { - t.Fatal("Expected newHTTPGetter to produce an httpGetter") + hg, ok = g.(*HTTPGetter) + if !ok { + t.Fatal("expected NewHTTPGetter to produce an *HTTPGetter") + } + + if hg.opts.insecureSkipVerifyTLS != insecure { + t.Errorf("Expected NewHTTPGetter to contain %t as InsecureSkipVerifyTLs flag, got %t", insecure, hg.opts.insecureSkipVerifyTLS) + } +} + +func TestDownload(t *testing.T) { + expect := "Call me Ishmael" + expectedUserAgent := "I am Groot" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defaultUserAgent := "Helm/" + strings.TrimPrefix(version.GetVersion(), "v") + if r.UserAgent() != defaultUserAgent { + t.Errorf("Expected '%s', got '%s'", defaultUserAgent, r.UserAgent()) + } + fmt.Fprint(w, expect) + })) + defer srv.Close() + + g, err := All(cli.New()).ByScheme("http") + if err != nil { + t.Fatal(err) + } + got, err := g.Get(srv.URL, WithURL(srv.URL)) + if err != nil { + t.Fatal(err) + } + + if got.String() != expect { + t.Errorf("Expected %q, got %q", expect, got.String()) + } + + // test with http server + basicAuthSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok || username != "username" || password != "password" { + t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) + } + if r.UserAgent() != expectedUserAgent { + t.Errorf("Expected '%s', got '%s'", expectedUserAgent, r.UserAgent()) + } + fmt.Fprint(w, expect) + })) + + defer basicAuthSrv.Close() + + u, _ := url.ParseRequestURI(basicAuthSrv.URL) + httpgetter, err := NewHTTPGetter( + WithURL(u.String()), + WithBasicAuth("username", "password"), + WithUserAgent(expectedUserAgent), + ) + if err != nil { + t.Fatal(err) + } + got, err = httpgetter.Get(u.String()) + if err != nil { + t.Fatal(err) + } + + if got.String() != expect { + t.Errorf("Expected %q, got %q", expect, got.String()) + } +} + +func TestDownloadTLS(t *testing.T) { + cd := "../../testdata" + ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem") + + tlsSrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca) + if err != nil { + t.Fatal(errors.Wrap(err, "can't create TLS config for client")) + } + tlsConf.BuildNameToCertificate() + tlsConf.ServerName = "helm.sh" + tlsSrv.TLS = tlsConf + tlsSrv.StartTLS() + defer tlsSrv.Close() + + u, _ := url.ParseRequestURI(tlsSrv.URL) + g, err := NewHTTPGetter( + WithURL(u.String()), + WithTLSClientConfig(pub, priv, ca), + ) + if err != nil { + t.Fatal(err) + } + + if _, err := g.Get(u.String()); err != nil { + t.Error(err) + } + + // now test with TLS config being passed along in .Get (see #6635) + g, err = NewHTTPGetter() + if err != nil { + t.Fatal(err) + } + + if _, err := g.Get(u.String(), WithURL(u.String()), WithTLSClientConfig(pub, priv, ca)); err != nil { + t.Error(err) + } + + // test with only the CA file (see also #6635) + g, err = NewHTTPGetter() + if err != nil { + t.Fatal(err) + } + + if _, err := g.Get(u.String(), WithURL(u.String()), WithTLSClientConfig("", "", ca)); err != nil { + t.Error(err) + } +} + +func TestDownloadInsecureSkipTLSVerify(t *testing.T) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + defer ts.Close() + + u, _ := url.ParseRequestURI(ts.URL) + + // Ensure the default behavior did not change + g, err := NewHTTPGetter( + WithURL(u.String()), + ) + if err != nil { + t.Error(err) + } + + if _, err := g.Get(u.String()); err == nil { + t.Errorf("Expected Getter to throw an error, got %s", err) + } + + // Test certificate check skip + g, err = NewHTTPGetter( + WithURL(u.String()), + WithInsecureSkipVerifyTLS(true), + ) + if err != nil { + t.Error(err) + } + if _, err = g.Get(u.String()); err != nil { + t.Error(err) + } + +} + +func TestHTTPGetterTarDownload(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + f, _ := os.Open("testdata/empty-0.0.1.tgz") + defer f.Close() + + b := make([]byte, 512) + f.Read(b) + //Get the file size + FileStat, _ := f.Stat() + FileSize := strconv.FormatInt(FileStat.Size(), 10) + + //Simulating improper header values from bitbucket + w.Header().Set("Content-Type", "application/x-tar") + w.Header().Set("Content-Encoding", "gzip") + w.Header().Set("Content-Length", FileSize) + + f.Seek(0, 0) + io.Copy(w, f) + })) + + defer srv.Close() + + g, err := NewHTTPGetter(WithURL(srv.URL)) + if err != nil { + t.Fatal(err) + } + + data, _ := g.Get(srv.URL) + mimeType := http.DetectContentType(data.Bytes()) + + expectedMimeType := "application/x-gzip" + if mimeType != expectedMimeType { + t.Fatalf("Expected response with MIME type %s, but got %s", expectedMimeType, mimeType) + } +} + +func TestHttpClientInsecureSkipVerify(t *testing.T) { + g := HTTPGetter{} + g.opts.url = "https://localhost" + verifyInsecureSkipVerify(t, g, "Blank HTTPGetter", false) + + g = HTTPGetter{} + g.opts.url = "https://localhost" + g.opts.caFile = "testdata/ca.crt" + verifyInsecureSkipVerify(t, g, "HTTPGetter with ca file", false) + + g = HTTPGetter{} + g.opts.url = "https://localhost" + g.opts.insecureSkipVerifyTLS = true + verifyInsecureSkipVerify(t, g, "HTTPGetter with skip cert verification only", true) + + g = HTTPGetter{} + g.opts.url = "https://localhost" + g.opts.certFile = "testdata/client.crt" + g.opts.keyFile = "testdata/client.key" + g.opts.insecureSkipVerifyTLS = true + transport := verifyInsecureSkipVerify(t, g, "HTTPGetter with 2 way ssl", true) + if len(transport.TLSClientConfig.Certificates) <= 0 { + t.Fatal("transport.TLSClientConfig.Certificates is not present") + } + if transport.TLSClientConfig.ServerName == "" { + t.Fatal("TLSClientConfig.ServerName is blank") + } +} + +func verifyInsecureSkipVerify(t *testing.T, g HTTPGetter, caseName string, expectedValue bool) *http.Transport { + returnVal, err := g.httpClient() + + if err != nil { + t.Fatal(err) + } + + if returnVal == nil { + t.Fatalf("Expected non nil value for http client") + } + transport := (returnVal.Transport).(*http.Transport) + gotValue := false + if transport.TLSClientConfig != nil { + gotValue = transport.TLSClientConfig.InsecureSkipVerify + } + if gotValue != expectedValue { + t.Fatalf("Case Name = %s\nInsecureSkipVerify did not come as expected. Expected = %t; Got = %v", + caseName, expectedValue, gotValue) } + return transport } diff --git a/pkg/getter/ocigetter.go b/pkg/getter/ocigetter.go new file mode 100644 index 00000000000..3f85b9862b1 --- /dev/null +++ b/pkg/getter/ocigetter.go @@ -0,0 +1,78 @@ +/* +Copyright The Helm Authors. +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 getter + +import ( + "bytes" + "fmt" + "strings" + + "helm.sh/helm/v3/internal/experimental/registry" +) + +// OCIGetter is the default HTTP(/S) backend handler +type OCIGetter struct { + opts options +} + +//Get performs a Get from repo.Getter and returns the body. +func (g *OCIGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { + for _, opt := range options { + opt(&g.opts) + } + return g.get(href) +} + +func (g *OCIGetter) get(href string) (*bytes.Buffer, error) { + client := g.opts.registryClient + + ref := strings.TrimPrefix(href, "oci://") + if version := g.opts.version; version != "" { + ref = fmt.Sprintf("%s:%s", ref, version) + } + + r, err := registry.ParseReference(ref) + if err != nil { + return nil, err + } + + buf, err := client.PullChart(r) + if err != nil { + return nil, err + } + + return buf, nil +} + +// NewOCIGetter constructs a valid http/https client as a Getter +func NewOCIGetter(ops ...Option) (Getter, error) { + registryClient, err := registry.NewClient() + if err != nil { + return nil, err + } + + client := OCIGetter{ + opts: options{ + registryClient: registryClient, + }, + } + + for _, opt := range ops { + opt(&client.opts) + } + + return &client, nil +} diff --git a/pkg/getter/ocigetter_test.go b/pkg/getter/ocigetter_test.go new file mode 100644 index 00000000000..fc548b7a649 --- /dev/null +++ b/pkg/getter/ocigetter_test.go @@ -0,0 +1,30 @@ +/* +Copyright The Helm Authors. +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 getter + +import ( + "testing" +) + +func TestNewOCIGetter(t *testing.T) { + testfn := func(ops *options) { + if ops.registryClient == nil { + t.Fatalf("the OCIGetter's registryClient should not be null") + } + } + + NewOCIGetter(testfn) +} diff --git a/pkg/getter/plugingetter.go b/pkg/getter/plugingetter.go index c747eef7fdd..0d13ade5707 100644 --- a/pkg/getter/plugingetter.go +++ b/pkg/getter/plugingetter.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -17,19 +17,21 @@ package getter import ( "bytes" - "fmt" "os" "os/exec" "path/filepath" + "strings" - "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/plugin" + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/plugin" ) // collectPlugins scans for getter plugins. -// This will load plugins according to the environment. -func collectPlugins(settings environment.EnvSettings) (Providers, error) { - plugins, err := plugin.FindPlugins(settings.PluginDirs()) +// This will load plugins according to the cli. +func collectPlugins(settings *cli.EnvSettings) (Providers, error) { + plugins, err := plugin.FindPlugins(settings.PluginsDirectory) if err != nil { return nil, err } @@ -38,7 +40,7 @@ func collectPlugins(settings environment.EnvSettings) (Providers, error) { for _, downloader := range plugin.Metadata.Downloaders { result = append(result, Provider{ Schemes: downloader.Protocols, - New: newPluginGetter( + New: NewPluginGetter( downloader.Command, settings, plugin.Metadata.Name, @@ -53,17 +55,21 @@ func collectPlugins(settings environment.EnvSettings) (Providers, error) { // pluginGetter is a generic type to invoke custom downloaders, // implemented in plugins. type pluginGetter struct { - command string - certFile, keyFile, cAFile string - settings environment.EnvSettings - name string - base string + command string + settings *cli.EnvSettings + name string + base string + opts options } // Get runs downloader plugin command -func (p *pluginGetter) Get(href string) (*bytes.Buffer, error) { - argv := []string{p.certFile, p.keyFile, p.cAFile, href} - prog := exec.Command(filepath.Join(p.base, p.command), argv...) +func (p *pluginGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { + for _, opt := range options { + opt(&p.opts) + } + commands := strings.Split(p.command, " ") + argv := append(commands[1:], p.opts.certFile, p.opts.keyFile, p.opts.caFile, href) + prog := exec.Command(filepath.Join(p.base, commands[0]), argv...) plugin.SetupPluginEnv(p.settings, p.name, p.base) prog.Env = os.Environ() buf := bytes.NewBuffer(nil) @@ -72,25 +78,25 @@ func (p *pluginGetter) Get(href string) (*bytes.Buffer, error) { if err := prog.Run(); err != nil { if eerr, ok := err.(*exec.ExitError); ok { os.Stderr.Write(eerr.Stderr) - return nil, fmt.Errorf("plugin %q exited with error", p.command) + return nil, errors.Errorf("plugin %q exited with error", p.command) } return nil, err } return buf, nil } -// newPluginGetter constructs a valid plugin getter -func newPluginGetter(command string, settings environment.EnvSettings, name, base string) Constructor { - return func(URL, CertFile, KeyFile, CAFile string) (Getter, error) { +// NewPluginGetter constructs a valid plugin getter +func NewPluginGetter(command string, settings *cli.EnvSettings, name, base string) Constructor { + return func(options ...Option) (Getter, error) { result := &pluginGetter{ command: command, - certFile: CertFile, - keyFile: KeyFile, - cAFile: CAFile, settings: settings, name: name, base: base, } + for _, opt := range options { + opt(&result.opts) + } return result, nil } } diff --git a/pkg/getter/plugingetter_test.go b/pkg/getter/plugingetter_test.go index f1fe9bf29d3..a18fa302b06 100644 --- a/pkg/getter/plugingetter_test.go +++ b/pkg/getter/plugingetter_test.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,34 +16,17 @@ limitations under the License. package getter import ( - "os" - "path/filepath" + "runtime" "strings" "testing" - "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/helm/helmpath" + "helm.sh/helm/v3/pkg/cli" ) -func hh(debug bool) environment.EnvSettings { - apath, err := filepath.Abs("./testdata") - if err != nil { - panic(err) - } - hp := helmpath.Home(apath) - return environment.EnvSettings{ - Home: hp, - Debug: debug, - } -} - func TestCollectPlugins(t *testing.T) { - // Reset HELM HOME to testdata. - oldhh := os.Getenv("HELM_HOME") - defer os.Setenv("HELM_HOME", oldhh) - os.Setenv("HELM_HOME", "") + env := cli.New() + env.PluginsDirectory = pluginDir - env := hh(false) p, err := collectPlugins(env) if err != nil { t.Fatal(err) @@ -67,13 +50,14 @@ func TestCollectPlugins(t *testing.T) { } func TestPluginGetter(t *testing.T) { - oldhh := os.Getenv("HELM_HOME") - defer os.Setenv("HELM_HOME", oldhh) - os.Setenv("HELM_HOME", "") + if runtime.GOOS == "windows" { + t.Skip("TODO: refactor this test to work on windows") + } - env := hh(false) - pg := newPluginGetter("echo", env, "test", ".") - g, err := pg("test://foo/bar", "", "", "") + env := cli.New() + env.PluginsDirectory = pluginDir + pg := NewPluginGetter("echo", env, "test", ".") + g, err := pg() if err != nil { t.Fatal(err) } @@ -89,3 +73,29 @@ func TestPluginGetter(t *testing.T) { t.Errorf("Expected %q, got %q", expect, got) } } + +func TestPluginSubCommands(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("TODO: refactor this test to work on windows") + } + + env := cli.New() + env.PluginsDirectory = pluginDir + + pg := NewPluginGetter("echo -n", env, "test", ".") + g, err := pg() + if err != nil { + t.Fatal(err) + } + + data, err := g.Get("test://foo/bar") + if err != nil { + t.Fatal(err) + } + + expect := " test://foo/bar" + got := data.String() + if got != expect { + t.Errorf("Expected %q, got %q", expect, got) + } +} diff --git a/pkg/getter/testdata/ca.crt b/pkg/getter/testdata/ca.crt new file mode 100644 index 00000000000..c17820085c2 --- /dev/null +++ b/pkg/getter/testdata/ca.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEJDCCAwygAwIBAgIUcGE5xyj7IH7sZLntsHKxZHCd3awwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCSU4xDzANBgNVBAgMBktlcmFsYTEOMAwGA1UEBwwFS29j +aGkxGDAWBgNVBAoMD2NoYXJ0bXVzZXVtLmNvbTEXMBUGA1UEAwwOY2hhcnRtdXNl +dW1fY2EwIBcNMjAxMjA0MDkxMjU4WhgPMjI5NDA5MTkwOTEyNThaMGExCzAJBgNV +BAYTAklOMQ8wDQYDVQQIDAZLZXJhbGExDjAMBgNVBAcMBUtvY2hpMRgwFgYDVQQK +DA9jaGFydG11c2V1bS5jb20xFzAVBgNVBAMMDmNoYXJ0bXVzZXVtX2NhMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqQJi/BRWzaXlkDP48kUAWgaLtD0Y +72E30WBZDAw3S+BaYulRk1LWK1QM+ALiZQb1a6YgNvuERyywOv45pZaC2xtP6Bju ++59kwBrEtNCTNa2cSqs0hSw6NCDe+K8lpFKlTdh4c5sAkiDkMBr1R6uu7o4HvfO0 +iGMZ9VUdrbf4psZIyPVRdt/sAkAKqbjQfxr6VUmMktrZNND+mwPgrhS2kPL4P+JS +zpxgpkuSUvg5DvJuypmCI0fDr6GwshqXM1ONHE0HT8MEVy1xZj9rVHt7sgQhjBX1 +PsFySZrq1lSz8R864c1l+tCGlk9+1ldQjc9tBzdvCjJB+nYfTTpBUk/VKwIDAQAB +o4HRMIHOMB0GA1UdDgQWBBSv1IMZGHWsZVqJkJoPDzVLMcUivjCBngYDVR0jBIGW +MIGTgBSv1IMZGHWsZVqJkJoPDzVLMcUivqFlpGMwYTELMAkGA1UEBhMCSU4xDzAN +BgNVBAgMBktlcmFsYTEOMAwGA1UEBwwFS29jaGkxGDAWBgNVBAoMD2NoYXJ0bXVz +ZXVtLmNvbTEXMBUGA1UEAwwOY2hhcnRtdXNldW1fY2GCFHBhOcco+yB+7GS57bBy +sWRwnd2sMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAI6Fg9F8cjB9 +2jJn1vZPpynSFs7XPlUBVh0YXBt+o6g7+nKInwFBPzPEQ7ZZotz3GIe4I7wYiQAn +c6TU2nnqK+9TLbJIyv6NOfikLgwrTy+dAW8wrOiu+IIzA8Gdy8z8m3B7v9RUYVhx +zoNoqCEvOIzCZKDH68PZDJrDVSuvPPK33Ywj3zxYeDNXU87BKGER0vjeVG4oTAcQ +hKJURh4IRy/eW9NWiFqvNgst7k5MldOgLIOUBh1faaxlWkjuGpfdr/EBAAr491S5 +IPFU7TopsrgANnxldSzVbcgfo2nt0A976T3xZQHy3xpk1rIt55xVzT0W55NRAc7v ++9NTUOB10so= +-----END CERTIFICATE----- diff --git a/pkg/getter/testdata/client.crt b/pkg/getter/testdata/client.crt new file mode 100644 index 00000000000..f005f401d00 --- /dev/null +++ b/pkg/getter/testdata/client.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDejCCAmKgAwIBAgIUfSn63/ldeo1prOaxXV8I0Id6HTEwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCSU4xDzANBgNVBAgMBktlcmFsYTEOMAwGA1UEBwwFS29j +aGkxGDAWBgNVBAoMD2NoYXJ0bXVzZXVtLmNvbTEXMBUGA1UEAwwOY2hhcnRtdXNl +dW1fY2EwIBcNMjAxMjA0MDkxMzIwWhgPMjI5NDA5MTkwOTEzMjBaMFwxCzAJBgNV +BAYTAklOMQ8wDQYDVQQIDAZLZXJhbGExDjAMBgNVBAcMBUtvY2hpMRgwFgYDVQQK +DA9jaGFydG11c2V1bS5jb20xEjAQBgNVBAMMCTEyNy4wLjAuMTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAKeCbADaK+7yrM9rQszF54334mGoSXbXY6Ca +7FKdkgmKCjeeqZ+lr+i+6WQ+O+Tn0dhlyHier42IqUw5Rzzegvl7QrhiChd8C6sW +pEqDK7Z1U+cv9gIabYd+qWDwFw67xiMNQfdZxwI/AgPzixlfsMw3ZNKM3Q0Vxtdz +EEYdEDDNgZ34Cj+KXCPpYDi2i5hZnha4wzIfbL3+z2o7sPBBLBrrsOtPdVVkxysN +HM4h7wp7w7QyOosndFvcTaX7yRA1ka0BoulCt2wdVc2ZBRPiPMySi893VCQ8zeHP +QMFDL3rGmKVLbP1to2dgf9ZgckMEwE8chm2D8Ls87F9tsK9fVlUCAwEAAaMtMCsw +EwYDVR0lBAwwCgYIKwYBBQUHAwIwFAYDVR0RBA0wC4IJMTI3LjAuMC4xMA0GCSqG +SIb3DQEBCwUAA4IBAQCi7z5U9J5DkM6eYzyyH/8p32Azrunw+ZpwtxbKq3xEkpcX +0XtbyTG2szegKF0eLr9NizgEN8M1nvaMO1zuxFMB6tCWO/MyNWH/0T4xvFnnVzJ4 +OKlGSvyIuMW3wofxCLRG4Cpw750iWpJ0GwjTOu2ep5tbnEMC5Ueg55WqCAE/yDrd +nL1wZSGXy1bj5H6q8EM/4/yrzK80QkfdpbDR0NGkDO2mmAKL8d57NuASWljieyV3 +Ty5C8xXw5jF2JIESvT74by8ufozUOPKmgRqySgEPgAkNm0s5a05KAi5Cpyxgdylm +CEvjni1LYGhJp9wXucF9ehKSdsw4qn9T5ire8YfI +-----END CERTIFICATE----- diff --git a/pkg/getter/testdata/client.key b/pkg/getter/testdata/client.key new file mode 100644 index 00000000000..4f676ba4276 --- /dev/null +++ b/pkg/getter/testdata/client.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAp4JsANor7vKsz2tCzMXnjffiYahJdtdjoJrsUp2SCYoKN56p +n6Wv6L7pZD475OfR2GXIeJ6vjYipTDlHPN6C+XtCuGIKF3wLqxakSoMrtnVT5y/2 +Ahpth36pYPAXDrvGIw1B91nHAj8CA/OLGV+wzDdk0ozdDRXG13MQRh0QMM2BnfgK +P4pcI+lgOLaLmFmeFrjDMh9svf7Pajuw8EEsGuuw6091VWTHKw0cziHvCnvDtDI6 +iyd0W9xNpfvJEDWRrQGi6UK3bB1VzZkFE+I8zJKLz3dUJDzN4c9AwUMvesaYpUts +/W2jZ2B/1mByQwTATxyGbYPwuzzsX22wr19WVQIDAQABAoIBABw7qUSDgUAm+uWC +6KFnAd4115wqJye2qf4Z3pcWI9UjxREW1vQnkvyhoOjabHHqeL4GecGKzYAHdrF4 +Pf+OaXjvQ5GcRKMsrzLJACvm6+k24UtoFAjKt4dM2/OQw/IhyAWEaIfuQ9KnGAne +dKV0MXJaK84pG+DmuLr7k9SddWskElEyxK2j0tvdyI5byRfjf5schac9M4i5ZAYV +pT+PuXZQh8L8GEY2koE+uEMpXGOstD7yUxyV8zHFyBC7FVDkqF4S8IWY+RXQtVd6 +l8B8dRLjKSLBKDB+neStepcwNUyCDYiqyqsKfN7eVHDd0arPm6LeTuAsHKBw2OoN +YdAmUUkCgYEA0vb9mxsMgr0ORTZ14vWghz9K12oKPk9ajYuLTQVn8GQazp0XTIi5 +Mil2I78Qj87ApzGqOyHbkEgpg0C9/mheYLOYNHC4of83kNF+pHbDi1TckwxIaIK0 +rZLb3Az3zZQ2rAWZ2IgSyoeVO9RxYK/RuvPFp+UBeucuXiYoI0YlEXcCgYEAy0Sk +LTiYuLsnk21RIFK01iq4Y+4112K1NGTKu8Wm6wPaPsnLznP6339cEkbiSgbRgERE +jgOfa/CiSw5CVT9dWZuQ3OoQ83pMRb7IB0TobPmhBS/HQZ8Ocbfb6PnxQ3o1Bx7I +QuIpZFxzuTP80p1p2DMDxEl+r/DCvT/wgBKX6ZMCgYAdw1bYMSK8tytyPFK5aGnz +asyGQ6GaVNuzqIJIpYCae6UEjUkiNQ/bsdnHBUey4jpv3CPmH8q4OlYQ/GtRnyvh +fLT2gQirYjRWrBev4EmKOLi9zjfQ9s/CxTtbekDjsgtcjZW85MWx6Rr2y+wK9gMi +2w2BuF9TFZaHFd8Hyvej1QKBgAoFbU6pbqYU3AOhrRE54p54ZrTOhqsCu8pEedY+ +DVeizfyweDLKdwDTx5dDFV7u7R80vmh99zscFvQ6VLzdLd4AFGk/xOwsCFyb5kKt +fAP7Xpvh2iH7FHw4w0e+Is3f1YNvWhIqEj5XbIEh9gHwLsqw4SupL+y+ousvnszB +nemvAoGBAJa7bYG8MMCFJ4OFAmkpgQzHSzq7dzOR6O4GKsQQhiZ/0nRK5l3sLcDO +9viuRfhRepJGbcQ/Hw0AVIRWU01y4mejbuxfUE/FgWBoBBvpbot2zfuJgeFAIvkY +iFsZwuxPQUFobTu2hj6gh0gOKj/LpNXHkZGbZ2zTXmK3GDYlf6bR +-----END RSA PRIVATE KEY----- diff --git a/pkg/getter/testdata/empty-0.0.1.tgz b/pkg/getter/testdata/empty-0.0.1.tgz new file mode 100644 index 00000000000..6c4c3d20597 Binary files /dev/null and b/pkg/getter/testdata/empty-0.0.1.tgz differ diff --git a/pkg/getter/testdata/repository/cache/local-index.yaml b/pkg/getter/testdata/repository/cache/local-index.yaml deleted file mode 120000 index ed068e99e4d..00000000000 --- a/pkg/getter/testdata/repository/cache/local-index.yaml +++ /dev/null @@ -1 +0,0 @@ -repository/local/index.yaml \ No newline at end of file diff --git a/pkg/getter/testdata/repository/cache/stable-index.yaml b/pkg/getter/testdata/repository/cache/stable-index.yaml deleted file mode 100644 index bf187e3df74..00000000000 --- a/pkg/getter/testdata/repository/cache/stable-index.yaml +++ /dev/null @@ -1,7852 +0,0 @@ -apiVersion: v1 -entries: - aws-cluster-autoscaler: - - apiVersion: v1 - created: 2017-04-28T00:18:30.071110236Z - description: Scales worker nodes within autoscaling groups. - digest: 291eaabbf54913e71cda39d42a6e9c4c493a3672a5b0b5183ea1991a76d6c317 - engine: gotpl - icon: https://github.com/kubernetes/kubernetes/blob/master/logo/logo.png - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: aws-cluster-autoscaler - sources: - - https://github.com/kubernetes/contrib/tree/master/cluster-autoscaler/cloudprovider/aws - urls: - - https://kubernetes-charts.storage.googleapis.com/aws-cluster-autoscaler-0.2.1.tgz - version: 0.2.1 - - apiVersion: v1 - created: 2017-03-02T18:48:30.418071154Z - description: Scales worker nodes within autoscaling groups. - digest: 52ee58b51619f641d0f6df4c778bcd068242379a1a040a269f0fc93867b79b13 - engine: gotpl - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: aws-cluster-autoscaler - sources: - - https://github.com/kubernetes/contrib/tree/master/cluster-autoscaler/cloudprovider/aws - urls: - - https://kubernetes-charts.storage.googleapis.com/aws-cluster-autoscaler-0.2.0.tgz - version: 0.2.0 - chaoskube: - - created: 2017-04-28T00:18:30.071358212Z - description: Chaoskube periodically kills random pods in your Kubernetes cluster. - digest: c90ff57a6205c725520dc600b439fc8eda120c3d8d4d4b7c9feee60bf62629cb - engine: gotpl - home: https://github.com/linki/chaoskube - maintainers: - - email: linki+kubernetes.io@posteo.de - name: Martin Linkhorst - name: chaoskube - sources: - - https://github.com/linki/chaoskube - urls: - - https://kubernetes-charts.storage.googleapis.com/chaoskube-0.5.0.tgz - version: 0.5.0 - - created: 2017-03-14T23:48:31.878196764Z - description: Chaoskube periodically kills random pods in your Kubernetes cluster. - digest: cc211be37255f2fdf7cc74022f51473c2c4af98c06b0871ab8c9b8341ee9d349 - engine: gotpl - home: https://github.com/linki/chaoskube - maintainers: - - email: linki+kubernetes.io@posteo.de - name: Martin Linkhorst - name: chaoskube - sources: - - https://github.com/linki/chaoskube - urls: - - https://kubernetes-charts.storage.googleapis.com/chaoskube-0.4.0.tgz - version: 0.4.0 - - created: 2017-02-13T04:18:31.525717582Z - description: A Helm chart for chaoskube - digest: 6e6466b2a7294853fbad6a1dc5526e7fd6eb75bafd035748259511ccf49f9c47 - engine: gotpl - home: https://github.com/linki/chaoskube - maintainers: - - email: linki+helm.sh@posteo.de - name: Martin Linkhorst - name: chaoskube - sources: - - https://github.com/linki/chaoskube - urls: - - https://kubernetes-charts.storage.googleapis.com/chaoskube-0.3.1.tgz - version: 0.3.1 - - created: 2017-02-13T21:18:28.251529085Z - description: Chaoskube periodically kills random pods in your Kubernetes cluster. - digest: 6439a906666fc62e7aeb28186ce2f4a730216941163edd799176cc30616e713a - engine: gotpl - home: https://github.com/linki/chaoskube - maintainers: - - email: linki+kubernetes.io@posteo.de - name: Martin Linkhorst - name: chaoskube - sources: - - https://github.com/linki/chaoskube - urls: - - https://kubernetes-charts.storage.googleapis.com/chaoskube-0.3.1-1.tgz - version: 0.3.1-1 - chronograf: - - created: 2017-04-28T00:18:30.071786276Z - description: Open-source web application written in Go and React.js that provides - the tools to visualize your monitoring data and easily create alerting and automation - rules. - digest: 5f6c0ada37696c624ebdfad1703666b91a84dedea81cbae4335109e7046c9f86 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/chronograf/ - keywords: - - chronograf - - visualizaion - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: chronograf - urls: - - https://kubernetes-charts.storage.googleapis.com/chronograf-0.2.0.tgz - version: 0.2.0 - - created: 2017-03-22T23:03:29.448801697Z - description: Open-source web application written in Go and React.js that provides - the tools to visualize your monitoring data and easily create alerting and automation - rules. - digest: 6bc90a815f7fc513bfa2b4d1a56a03e53444bfd08b742e0d0f1ff9f3b5db52f7 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/chronograf/ - keywords: - - chronograf - - visualizaion - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: chronograf - urls: - - https://kubernetes-charts.storage.googleapis.com/chronograf-0.1.2.tgz - version: 0.1.2 - - created: 2017-02-13T04:18:31.52604543Z - description: Chart for Chronograf - digest: 985fa74feb6ed111220ca7a8c5da529accd42def9d75f56c0c82902631bcf15f - engine: gotpl - home: https://www.influxdata.com/time-series-platform/chronograf/ - keywords: - - chronograf - - visualizaion - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: chronograf - urls: - - https://kubernetes-charts.storage.googleapis.com/chronograf-0.1.1.tgz - version: 0.1.1 - - created: 2017-01-28T00:33:31.060189495Z - description: Chart for Chronograf - digest: ea03695da15a018e84d05e0fd97d581f3fd348d8461aa098cd221b5010176a35 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/chronograf/ - keywords: - - chronograf - - visualizaion - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: chronograf - urls: - - https://kubernetes-charts.storage.googleapis.com/chronograf-0.1.0.tgz - version: 0.1.0 - cockroachdb: - - created: 2017-04-28T00:18:30.07217685Z - description: CockroachDB is a scalable, survivable, strongly-consistent SQL database. - digest: c51bf6c3d07067b80ca8661ff8460b2bb7d25d242f153045668ba95566fc8069 - home: https://www.cockroachlabs.com - icon: https://raw.githubusercontent.com/cockroachdb/cockroach/master/docs/media/cockroach_db.png - maintainers: - - email: alex@cockroachlabs.com - name: Alex Robinson - name: cockroachdb - sources: - - https://github.com/cockroachdb/cockroach - urls: - - https://kubernetes-charts.storage.googleapis.com/cockroachdb-0.2.2.tgz - version: 0.2.2 - - created: 2017-02-13T04:18:31.526409785Z - description: CockroachDB Helm chart for Kubernetes. - digest: 0eec741613e00f7092ec81469f919cd79fec52a22e8685063c0b0d8fd6570c37 - home: https://www.cockroachlabs.com - maintainers: - - email: alex@cockroachlabs.com - name: Alex Robinson - name: cockroachdb - sources: - - https://github.com/cockroachdb/cockroach - urls: - - https://kubernetes-charts.storage.googleapis.com/cockroachdb-0.2.1.tgz - version: 0.2.1 - - created: 2017-01-27T21:48:32.059935168Z - description: CockroachDB Helm chart for Kubernetes. - digest: 7b41add319a997fd3d862d6649b707313f59ac6fa137842db65230342baff619 - home: https://www.cockroachlabs.com - maintainers: - - email: alex@cockroachlabs.com - name: Alex Robinson - name: cockroachdb - sources: - - https://github.com/cockroachdb/cockroach - urls: - - https://kubernetes-charts.storage.googleapis.com/cockroachdb-0.2.0.tgz - version: 0.2.0 - concourse: - - created: 2017-04-28T00:18:30.074578589Z - description: Concourse is a simple and scalable CI system. - digest: b7ea57e289002deba839f52acf6a5919870ab99910f12bcc6edadd4ae5651826 - engine: gotpl - home: https://concourse.ci/ - icon: https://avatars1.githubusercontent.com/u/7809479 - keywords: - - ci - - concourse - - concourse.ci - maintainers: - - email: frodenas@gmail.com - name: Ferran Rodenas - name: concourse - sources: - - https://github.com/concourse/bin - - https://github.com/kubernetes/charts - urls: - - https://kubernetes-charts.storage.googleapis.com/concourse-0.1.3.tgz - version: 0.1.3 - - created: 2017-02-14T19:03:29.77100439Z - description: Concourse is a simple and scalable CI system. - digest: 5f8ed4eb5b0acf8da7b34772714154322405b796553a33fc6ffd779e0a556003 - engine: gotpl - home: https://concourse.ci/ - icon: https://avatars1.githubusercontent.com/u/7809479 - keywords: - - ci - - concourse - - concourse.ci - maintainers: - - email: frodenas@gmail.com - name: Ferran Rodenas - name: concourse - sources: - - https://github.com/concourse/bin - - https://github.com/kubernetes/charts - urls: - - https://kubernetes-charts.storage.googleapis.com/concourse-0.1.2.tgz - version: 0.1.2 - - created: 2017-02-13T21:18:28.254273427Z - description: Concourse is a simple and scalable CI system. - digest: cba53dadd21dbd85b31a1a522a5eaeb49cadfa595ba0762c02dca04905d35f20 - engine: gotpl - home: https://concourse.ci/ - icon: https://avatars1.githubusercontent.com/u/7809479 - keywords: - - ci - - concourse - - concourse.ci - maintainers: - - email: frodenas@gmail.com - name: Ferran Rodenas - name: concourse - sources: - - https://github.com/concourse/bin - - https://github.com/kubernetes/charts - urls: - - https://kubernetes-charts.storage.googleapis.com/concourse-0.1.1.tgz - version: 0.1.1 - - created: 2017-02-10T23:18:26.003782261Z - description: Concourse Helm chart for Kubernetes. - digest: 08e6b4c56357ce15dfd66b0fad8c2df37664877b16b4a0afcbaeb301ddcc7432 - engine: gotpl - home: https://concourse.ci/ - keywords: - - ci - - concourse - - concourse.ci - maintainers: - - email: frodenas@gmail.com - name: Ferran Rodenas - name: concourse - sources: - - https://github.com/concourse/bin - - https://github.com/kubernetes/charts - urls: - - https://kubernetes-charts.storage.googleapis.com/concourse-0.1.0.tgz - version: 0.1.0 - consul: - - created: 2017-04-28T00:18:30.075038045Z - description: Highly available and distributed service discovery and key-value - store designed with support for the modern data center to make distributed systems - and configuration easy. - digest: 7e0093709abc7a2c475e4e8c14e856d9834f88683b4a9e8c6099f4e0ad7e434e - home: https://github.com/hashicorp/consul - icon: https://www.consul.io/assets/images/logo_large-475cebb0.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - name: consul - sources: - - https://github.com/kelseyhightower/consul-on-kubernetes - urls: - - https://kubernetes-charts.storage.googleapis.com/consul-0.2.2.tgz - version: 0.2.2 - - created: 2017-04-26T14:48:28.225526691Z - description: Highly available and distributed service discovery and key-value - store designed with support for the modern data center to make distributed systems - and configuration easy. - digest: 03daa04827bf93627b7087001c1d2424337d268a5875a51d8db4bb4e53bbc7db - home: https://github.com/hashicorp/consul - icon: https://www.consul.io/assets/images/logo_large-475cebb0.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - name: consul - sources: - - https://github.com/kelseyhightower/consul-on-kubernetes - urls: - - https://kubernetes-charts.storage.googleapis.com/consul-0.2.0.tgz - version: 0.2.0 - coredns: - - created: 2017-04-28T00:18:30.075393511Z - description: CoreDNS is a DNS server that chains middleware and provides Kubernetes - DNS Services - digest: adbdc4a8895f7c2e7cca64c2dcf36ddfffeff1115a3ade32011ec82b60be119b - home: https://coredns.io - icon: https://raw.githubusercontent.com/coredns/logo/master/Icon/CoreDNS_Colour_Icon.svg - keywords: - - coredns - - dns - - kubedns - maintainers: - - email: hello@acale.ph - name: Acaleph - - email: shashidhara.huawei@gmail.com - name: Shashidhara TD - name: coredns - sources: - - https://github.com/coredns/coredns - urls: - - https://kubernetes-charts.storage.googleapis.com/coredns-0.1.0.tgz - version: 0.1.0 - datadog: - - created: 2017-04-28T00:18:30.075800677Z - description: DataDog Agent - digest: bc559f013b738704089c4964a268c447b22e82181e9fa9e8219d46d40b388709 - home: https://www.datadoghq.com - icon: https://datadog-live.imgix.net/img/dd_logo_70x75.png - keywords: - - monitoring - - alerting - - metric - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: datadog - sources: - - https://app.datadoghq.com/account/settings#agent/kubernetes - - https://github.com/DataDog/docker-dd-agent - urls: - - https://kubernetes-charts.storage.googleapis.com/datadog-0.3.0.tgz - version: 0.3.0 - - created: 2017-04-06T11:33:26.056402381Z - description: DataDog Agent - digest: b32c28e76004eedf5c160936ccf35adca3a150ae1d0b491df52d017725b5ee90 - home: https://www.datadoghq.com - icon: https://datadog-live.imgix.net/img/dd_logo_70x75.png - keywords: - - monitoring - - alerting - - metric - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: datadog - sources: - - https://app.datadoghq.com/account/settings#agent/kubernetes - - https://github.com/DataDog/docker-dd-agent - urls: - - https://kubernetes-charts.storage.googleapis.com/datadog-0.2.1.tgz - version: 0.2.1 - - created: 2017-02-11T03:18:26.518137684Z - description: DataDog Agent - digest: d534bdcf4644d88ebfa70c58e57aafed41b75da4264042d4975f70d091e2b493 - keywords: - - monitoring - - alerting - - metric - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: datadog - sources: - - https://app.datadoghq.com/account/settings#agent/kubernetes - - https://github.com/DataDog/docker-dd-agent - urls: - - https://kubernetes-charts.storage.googleapis.com/datadog-0.2.0.tgz - version: 0.2.0 - - created: 2017-01-04T00:48:19.731877862Z - description: DataDog Agent - digest: 694c1d036d92c8bb60638f7bd66144a991a807dc879bedacf8a5d32e9d72081a - keywords: - - monitoring - - alerting - - metric - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: datadog - sources: - - https://app.datadoghq.com/account/settings#agent/kubernetes - - https://github.com/DataDog/docker-dd-agent - urls: - - https://kubernetes-charts.storage.googleapis.com/datadog-0.1.0.tgz - version: 0.1.0 - dokuwiki: - - created: 2017-04-28T00:18:30.076184541Z - description: DokuWiki is a standards-compliant, simple to use wiki optimized for - creating documentation. It is targeted at developer teams, workgroups, and small - companies. All data is stored in plain text files, so no database is required. - digest: 5cfff9542341a391abf9029dd9b42e7c44813c520ef0301ce62e9c08586ceca2 - engine: gotpl - home: http://www.dokuwiki.org/ - icon: https://bitnami.com/assets/stacks/dokuwiki/img/dokuwiki-stack-110x117.png - keywords: - - dokuwiki - - wiki - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: dokuwiki - sources: - - https://github.com/bitnami/bitnami-docker-dokuwiki - urls: - - https://kubernetes-charts.storage.googleapis.com/dokuwiki-0.1.4.tgz - version: 0.1.4 - - created: 2017-04-13T05:18:28.897680481Z - description: DokuWiki is a standards-compliant, simple to use wiki optimized for - creating documentation. It is targeted at developer teams, workgroups, and small - companies. All data is stored in plain text files, so no database is required. - digest: 3c46f9d9196bbf975711b2bb7c889fd3df1976cc57c3c120c7374d721da0e240 - engine: gotpl - home: http://www.dokuwiki.org/ - icon: https://bitnami.com/assets/stacks/dokuwiki/img/dokuwiki-stack-110x117.png - keywords: - - dokuwiki - - wiki - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: dokuwiki - sources: - - https://github.com/bitnami/bitnami-docker-dokuwiki - urls: - - https://kubernetes-charts.storage.googleapis.com/dokuwiki-0.1.3.tgz - version: 0.1.3 - - created: 2017-03-02T19:33:28.170205427Z - description: DokuWiki is a standards-compliant, simple to use wiki optimized for - creating documentation. It is targeted at developer teams, workgroups, and small - companies. All data is stored in plain text files, so no database is required. - digest: f533bc20e08179a49cca77b175f897087dc3f2c57e6c89ecbd7264ab5975d66a - engine: gotpl - home: http://www.dokuwiki.org/ - icon: https://bitnami.com/assets/stacks/dokuwiki/img/dokuwiki-stack-110x117.png - keywords: - - dokuwiki - - wiki - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: dokuwiki - sources: - - https://github.com/bitnami/bitnami-docker-dokuwiki - urls: - - https://kubernetes-charts.storage.googleapis.com/dokuwiki-0.1.2.tgz - version: 0.1.2 - - created: 2017-02-01T02:18:29.116555882Z - description: DokuWiki is a standards-compliant, simple to use wiki optimized for - creating documentation. It is targeted at developer teams, workgroups, and small - companies. All data is stored in plain text files, so no database is required. - digest: 34a926398cfafbf426ff468167ef49577252e260ebce5df33380e6e67b79fe59 - engine: gotpl - home: http://www.dokuwiki.org/ - icon: https://bitnami.com/assets/stacks/dokuwiki/img/dokuwiki-stack-110x117.png - keywords: - - dokuwiki - - wiki - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: dokuwiki - sources: - - https://github.com/bitnami/bitnami-docker-dokuwiki - urls: - - https://kubernetes-charts.storage.googleapis.com/dokuwiki-0.1.1.tgz - version: 0.1.1 - - created: 2017-01-28T00:33:31.06436596Z - description: DokuWiki is a standards-compliant, simple to use wiki optimized for - creating documentation. It is targeted at developer teams, workgroups, and small - companies. All data is stored in plain text files, so no database is required. - digest: 6825fbacb709cf05901985561be10ba9473a379488d99b71d1590d33f5a81374 - engine: gotpl - home: http://www.dokuwiki.org/ - icon: https://bitnami.com/assets/stacks/dokuwiki/img/dokuwiki-stack-110x117.png - keywords: - - dokuwiki - - wiki - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: dokuwiki - sources: - - https://github.com/bitnami/bitnami-docker-dokuwiki - urls: - - https://kubernetes-charts.storage.googleapis.com/dokuwiki-0.1.0.tgz - version: 0.1.0 - drupal: - - created: 2017-04-28T00:18:30.076853626Z - description: One of the most versatile open source content management systems. - digest: db95c255b19164c5051eb75a6860f3775a1011399a62b27e474cd9ebee0cb578 - engine: gotpl - home: http://www.drupal.org/ - icon: https://bitnami.com/assets/stacks/drupal/img/drupal-stack-220x234.png - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.5.1.tgz - version: 0.5.1 - - created: 2017-04-24T19:18:29.642780033Z - description: One of the most versatile open source content management systems. - digest: 84c13154a9aeb7215dc0d98e9825207207e69ca870f3d54b273bfc2d34699f61 - engine: gotpl - home: http://www.drupal.org/ - icon: https://bitnami.com/assets/stacks/drupal/img/drupal-stack-220x234.png - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.5.0.tgz - version: 0.5.0 - - created: 2017-04-11T17:18:28.883487907Z - description: One of the most versatile open source content management systems. - digest: 17d0bfdcdf5a1a650941343c76b6b928d76d3332fece127c502e91f9597f419e - engine: gotpl - home: http://www.drupal.org/ - icon: https://bitnami.com/assets/stacks/drupal/img/drupal-stack-220x234.png - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.4.6.tgz - version: 0.4.6 - - created: 2017-03-23T01:48:29.309045867Z - description: One of the most versatile open source content management systems. - digest: 317674c89762e0b54156b734203ee93638dd7a25df35120c5cab45546814d89b - engine: gotpl - home: http://www.drupal.org/ - icon: https://bitnami.com/assets/stacks/drupal/img/drupal-stack-220x234.png - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.4.5.tgz - version: 0.4.5 - - created: 2017-03-16T13:33:30.828606332Z - description: One of the most versatile open source content management systems. - digest: 24c4f187b50c0e961cc2cacf6e6b2ce6d6b225c73637c578e001bebd9b3f5d48 - engine: gotpl - home: http://www.drupal.org/ - icon: https://bitnami.com/assets/stacks/drupal/img/drupal-stack-220x234.png - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.4.4.tgz - version: 0.4.4 - - created: 2017-03-02T19:33:28.170963773Z - description: One of the most versatile open source content management systems. - digest: 7fcea4684a3d520454aeaa10beb9f9b1789c09c097680fc484954084f283feb3 - engine: gotpl - home: http://www.drupal.org/ - icon: https://bitnami.com/assets/stacks/drupal/img/drupal-stack-220x234.png - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.4.3.tgz - version: 0.4.3 - - created: 2017-02-27T17:03:27.765392204Z - description: One of the most versatile open source content management systems. - digest: adb23bc71125b9691b407a47dadf4298de3516805218813b56067967e39db7d8 - engine: gotpl - home: http://www.drupal.org/ - icon: https://bitnami.com/assets/stacks/drupal/img/drupal-stack-220x234.png - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.4.2.tgz - version: 0.4.2 - - created: 2017-02-11T00:18:52.26723498Z - description: One of the most versatile open source content management systems. - digest: 5de529e25767e8a37b8d6f413daa0fe99f5c304e48ddcfa8adb4d8c7a0496aa7 - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.4.1.tgz - version: 0.4.1 - - created: 2017-01-28T00:33:31.065139372Z - description: One of the most versatile open source content management systems. - digest: a35dbf9d470972cc2461de3e0a8fcf2fec8d0adc04f5a0f1e924505f22c714d7 - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.4.0.tgz - version: 0.4.0 - - created: 2017-01-04T00:48:19.73297256Z - description: One of the most versatile open source content management systems. - digest: a62d686d6bd47643dfa489e395dda89286954f785123a43a88db7ef34f3ea48d - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.10.tgz - version: 0.3.10 - - created: 2016-12-15T00:48:24.005322531Z - description: One of the most versatile open source content management systems. - digest: 2c189424bda94eeebb7e6370e96884f09bdfa81498cb93ac4723d24c92a3938e - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.9.tgz - version: 0.3.9 - - created: 2016-12-09T18:48:20.182097412Z - description: One of the most versatile open source content management systems. - digest: 3596f47c5dcaa7a975d1c4cb7bf7ef6790c9ad8dda41a5a329e30c1ea8a40d11 - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.8.tgz - version: 0.3.8 - - created: 2016-11-21T19:48:21.904806991Z - description: One of the most versatile open source content management systems. - digest: 78b2bb3717be63dccb02ea06b711ca7cf7869848b296b880099c6264e86d86d3 - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.7.tgz - version: 0.3.7 - - created: 2016-11-08T15:03:20.739400722Z - description: One of the most versatile open source content management systems. - digest: 5508b29e20a5d609f76319869774f008dcc4bed13bbbc7ed40546bc9af8c7cd7 - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.6.tgz - version: 0.3.6 - - created: 2016-11-03T19:33:29.11780736Z - description: One of the most versatile open source content management systems. - digest: 023a282c93f8411fb81bb4fff7820c1337aad0586ccf7dae55bdbed515ad8b05 - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.5.tgz - version: 0.3.5 - - created: 2016-10-21T19:18:18.619010562Z - description: One of the most versatile open source content management systems. - digest: 9bdaa53f7a9e82c9b32c7ac9b34b84fd142671732a54423a2dcdc893c4162801 - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.4.tgz - version: 0.3.4 - - created: 2016-10-19T00:03:14.027652488Z - description: One of the most versatile open source content management systems. - digest: 25650526abc1036398dbb314d77a0062cbb644b2c5791a258fb863fdaad5093d - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.3.tgz - version: 0.3.3 - - created: 2016-10-19T00:03:14.027073479Z - description: One of the most versatile open source content management systems. - digest: 13d5d32d316c08359221d230004dd2adc0152362e87abcc0d61ea191241fa69f - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.2.tgz - version: 0.3.2 - - created: 2016-10-19T00:03:14.025451665Z - description: One of the most versatile open source content management systems. - digest: b3f09ecd191f8c06275c96d9af4d77a97c94355525864201e9baf151b17bd5a7 - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.1.tgz - version: 0.3.1 - - created: 2016-10-19T00:03:14.024557743Z - description: One of the most versatile open source content management systems. - digest: c56fc55b93b0dead65af7b81bbd54befd5115860698ca475baa5acb178a12e5a - engine: gotpl - home: http://www.drupal.org/ - keywords: - - drupal - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: drupal - sources: - - https://github.com/bitnami/bitnami-docker-drupal - urls: - - https://kubernetes-charts.storage.googleapis.com/drupal-0.3.0.tgz - version: 0.3.0 - etcd-operator: - - apiVersion: v1 - created: 2017-04-28T00:18:30.077181321Z - description: CoreOS etcd-operator Helm chart for Kubernetes - digest: 1eb39b2f0ca26762eb13fc8cb577be741f7bb9d3162ab9c4547bb5176383bc2b - home: https://github.com/coreos/etcd-operator - icon: https://raw.githubusercontent.com/coreos/etcd/master/logos/etcd-horizontal-color.png - maintainers: - - email: chance.zibolski@coreos.com - name: Chance Zibolski - - email: lachlan@deis.com - name: Lachlan Evenson - name: etcd-operator - sources: - - https://github.com/coreos/etcd-operator - urls: - - https://kubernetes-charts.storage.googleapis.com/etcd-operator-0.2.0.tgz - version: 0.2.0 - - apiVersion: v1 - created: 2017-03-08T19:18:29.84391993Z - description: CoreOS etcd-operator Helm chart for Kubernetes - digest: 4a65fe6c61e731b373395e524f160eb4ced32a22cbfb3ff5d406b1c8bc3ae3c7 - home: https://github.com/coreos/etcd-operator - icon: https://raw.githubusercontent.com/coreos/etcd/master/logos/etcd-horizontal-color.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - name: etcd-operator - sources: - - https://github.com/coreos/etcd-operator - urls: - - https://kubernetes-charts.storage.googleapis.com/etcd-operator-0.1.1.tgz - version: 0.1.1 - - apiVersion: v1 - created: 2017-02-11T03:18:26.519796919Z - description: CoreOS etcd-operator Helm chart for Kubernetes - digest: 9bf72369082c4bad154171b3b68ad435331d6d07ae6d00e79e859f2a1599017b - icon: https://github.com/coreos/etcd/blob/master/logos/etcd-horizontal-color.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - name: etcd-operator - sources: - - https://github.com/coreos/etcd-operator - urls: - - https://kubernetes-charts.storage.googleapis.com/etcd-operator-0.1.0.tgz - version: 0.1.0 - factorio: - - created: 2017-04-28T00:18:30.077542134Z - description: Factorio dedicated server. - digest: cdc44bc00d42020a7a4df154cdc5cc7ae148aa8d2a3f97b1e18ac1fc42853dc1 - home: https://www.factorio.com/ - icon: https://us1.factorio.com/assets/img/factorio-logo.png - keywords: - - game - - server - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: factorio - sources: - - https://github.com/games-on-k8s/docker-factorio - urls: - - https://kubernetes-charts.storage.googleapis.com/factorio-0.2.0.tgz - version: 0.2.0 - - created: 2017-03-15T11:48:36.74226142Z - description: Factorio dedicated server. - digest: 5a60defdd8ac6f2276950662ba75f65d20c9e37edc85149012a2c84146eb6cff - home: https://www.factorio.com/ - icon: https://us1.factorio.com/assets/img/factorio-logo.png - keywords: - - game - - server - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: factorio - sources: - - https://github.com/games-on-k8s/docker-factorio - urls: - - https://kubernetes-charts.storage.googleapis.com/factorio-0.1.4.tgz - version: 0.1.4 - - created: 2017-02-13T04:18:31.530714209Z - description: Factorio dedicated server. - digest: 26244a5e1b5f992cdbef73ef9c7ffcaa52af538912fa299210f72275f2c756c9 - keywords: - - game - - server - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: factorio - sources: - - https://github.com/games-on-k8s/docker-factorio - urls: - - https://kubernetes-charts.storage.googleapis.com/factorio-0.1.3.tgz - version: 0.1.3 - - created: 2017-01-28T00:33:31.066072163Z - description: Factorio dedicated server. - digest: d6f4e42b82713c2da69e1ddcfce4a04fad31d4af915629d8e83e54b90a822f9a - keywords: - - game - - server - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: factorio - sources: - - https://github.com/games-on-k8s/docker-factorio - urls: - - https://kubernetes-charts.storage.googleapis.com/factorio-0.1.2.tgz - version: 0.1.2 - - created: 2016-12-02T09:03:20.175832035Z - description: Factorio dedicated server. - digest: 3cc1f83669fd1d97eb96e76ba4821e8664350a4c310d3a14e62be18cc09e59b7 - keywords: - - game - - server - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: factorio - sources: - - https://github.com/games-on-k8s/docker-factorio - urls: - - https://kubernetes-charts.storage.googleapis.com/factorio-0.1.1.tgz - version: 0.1.1 - - created: 2016-11-07T18:33:21.243890443Z - description: Factorio dedicated server. - digest: 8edc1340cd99225a769b5843a677896c0d54a079133f9759211a1839bc7bb3eb - keywords: - - game - - server - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: factorio - sources: - - https://github.com/games-on-k8s/docker-factorio - urls: - - https://kubernetes-charts.storage.googleapis.com/factorio-0.1.0.tgz - version: 0.1.0 - gcloud-endpoints: - - created: 2017-04-28T00:18:30.077956448Z - description: Develop, deploy, protect and monitor your APIs with Google Cloud - Endpoints. - digest: 172b56d0343c560f06e18858038e2096c910b37075a90f4303f77a79342b56fd - engine: gotpl - home: https://cloud.google.com/endpoints/ - keywords: - - google - - endpoints - - nginx - - gcloud - - proxy - - authentication - - monitoring - - api - - swagger - - open api - maintainers: - - email: mtucker@deis.com - name: Matt Tucker - name: gcloud-endpoints - urls: - - https://kubernetes-charts.storage.googleapis.com/gcloud-endpoints-0.1.0.tgz - version: 0.1.0 - gcloud-sqlproxy: - - created: 2017-04-28T00:18:30.078355277Z - description: Google Cloud SQL Proxy - digest: 1115afe0958f1ed01e568ec4c35b48ac0094704165b8808634ea5331c0d6da7d - engine: gotpl - home: https://cloud.google.com/sql/docs/postgres/sql-proxy - keywords: - - google - - cloud - - postgresql - - mysql - - sql - - sqlproxy - maintainers: - - email: rmocius@gmail.com - name: Rimas Mocevicius - name: gcloud-sqlproxy - sources: - - https://github.com/rimusz/charts - urls: - - https://kubernetes-charts.storage.googleapis.com/gcloud-sqlproxy-0.1.0.tgz - version: 0.1.0 - ghost: - - created: 2017-04-28T00:18:30.079159648Z - description: A simple, powerful publishing platform that allows you to share your - stories with the world - digest: 91d195c99e00b2801eafef5c23fcf9ced218bb29c7097f08139e2bdc80e38a0f - engine: gotpl - home: http://www.ghost.org/ - icon: https://bitnami.com/assets/stacks/ghost/img/ghost-stack-220x234.png - keywords: - - ghost - - blog - - http - - web - - application - - nodejs - - javascript - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: ghost - sources: - - https://github.com/bitnami/bitnami-docker-ghost - urls: - - https://kubernetes-charts.storage.googleapis.com/ghost-0.4.7.tgz - version: 0.4.7 - - created: 2017-04-06T10:48:26.261677382Z - description: A simple, powerful publishing platform that allows you to share your - stories with the world - digest: 6342a95aeef40690430c2e80b167fbb116a632746cdca49cfac4cbd535eadb22 - engine: gotpl - home: http://www.ghost.org/ - icon: https://bitnami.com/assets/stacks/ghost/img/ghost-stack-220x234.png - keywords: - - ghost - - blog - - http - - web - - application - - nodejs - - javascript - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: ghost - sources: - - https://github.com/bitnami/bitnami-docker-ghost - urls: - - https://kubernetes-charts.storage.googleapis.com/ghost-0.4.6.tgz - version: 0.4.6 - - created: 2017-03-08T19:03:31.719494672Z - description: A simple, powerful publishing platform that allows you to share your - stories with the world - digest: 8998a9a4e75e777edb6f06c05b45d461daebba09021161af3bef523efd193b15 - engine: gotpl - home: http://www.ghost.org/ - icon: https://bitnami.com/assets/stacks/ghost/img/ghost-stack-220x234.png - keywords: - - ghost - - blog - - http - - web - - application - - nodejs - - javascript - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: ghost - sources: - - https://github.com/bitnami/bitnami-docker-ghost - urls: - - https://kubernetes-charts.storage.googleapis.com/ghost-0.4.5.tgz - version: 0.4.5 - - created: 2017-02-27T17:03:27.767416735Z - description: A simple, powerful publishing platform that allows you to share your - stories with the world - digest: e44c9a53355086ded1832f769dca515b863337ad118ba618ef97f37b3ef84030 - engine: gotpl - home: http://www.ghost.org/ - icon: https://bitnami.com/assets/stacks/ghost/img/ghost-stack-220x234.png - keywords: - - ghost - - blog - - http - - web - - application - - nodejs - - javascript - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: ghost - sources: - - https://github.com/bitnami/bitnami-docker-ghost - urls: - - https://kubernetes-charts.storage.googleapis.com/ghost-0.4.4.tgz - version: 0.4.4 - - created: 2017-02-11T00:18:52.2688126Z - description: A simple, powerful publishing platform that allows you to share your - stories with the world - digest: b0c94a93c88fde68bb4fc78e92691d46cd2eb4d32cbac011e034565fbd35d46b - engine: gotpl - home: http://www.ghost.org/ - keywords: - - ghost - - blog - - http - - web - - application - - nodejs - - javascript - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: ghost - sources: - - https://github.com/bitnami/bitnami-docker-ghost - urls: - - https://kubernetes-charts.storage.googleapis.com/ghost-0.4.3.tgz - version: 0.4.3 - - created: 2017-01-29T22:48:35.944809746Z - description: A simple, powerful publishing platform that allows you to share your - stories with the world - digest: 791ccb42b62d56d50c72b37db3282eb3f2af75d667a25542d76c7991004eb822 - engine: gotpl - home: http://www.ghost.org/ - keywords: - - ghost - - blog - - http - - web - - application - - nodejs - - javascript - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: ghost - sources: - - https://github.com/bitnami/bitnami-docker-ghost - urls: - - https://kubernetes-charts.storage.googleapis.com/ghost-0.4.2.tgz - version: 0.4.2 - - created: 2017-01-21T00:18:31.342008382Z - description: A simple, powerful publishing platform that allows you to share your - stories with the world - digest: 331a2b145bfdb39b626313cda7dc539f32dbda5149893957589c5406317fca53 - engine: gotpl - home: http://www.ghost.org/ - keywords: - - ghost - - blog - - http - - web - - application - - nodejs - - javascript - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: ghost - sources: - - https://github.com/bitnami/bitnami-docker-ghost - urls: - - https://kubernetes-charts.storage.googleapis.com/ghost-0.4.1.tgz - version: 0.4.1 - - created: 2016-12-09T18:48:20.183434341Z - description: A simple, powerful publishing platform that allows you to share your - stories with the world - digest: 804227af037082a0f5c3c579feb9e24eb3682449e78876971c93a109bc716f40 - engine: gotpl - home: http://www.ghost.org/ - keywords: - - ghost - - blog - - http - - web - - application - - nodejs - - javascript - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: ghost - sources: - - https://github.com/bitnami/bitnami-docker-ghost - urls: - - https://kubernetes-charts.storage.googleapis.com/ghost-0.4.0.tgz - version: 0.4.0 - gitlab-ce: - - created: 2017-04-28T00:18:30.080184798Z - description: GitLab Community Edition - digest: df5e36c64bf1b8e2b77609c1cd9c717df47c290777a005ebf0edbe42d1f0ac70 - home: https://about.gitlab.com - icon: https://gitlab.com/uploads/group/avatar/6543/gitlab-logo-square.png - keywords: - - git - - ci - - deploy - - issue tracker - - code review - - wiki - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: gitlab-ce - sources: - - https://hub.docker.com/r/gitlab/gitlab-ce/ - - https://docs.gitlab.com/omnibus/ - urls: - - https://kubernetes-charts.storage.googleapis.com/gitlab-ce-0.1.7.tgz - version: 0.1.7 - - created: 2017-04-11T16:33:29.173232912Z - description: GitLab Community Edition - digest: 80094520d1bee55c7e25ad740bbfe3740814f389e6221b2fa536f77aabba9b37 - home: https://about.gitlab.com - icon: https://gitlab.com/uploads/group/avatar/6543/gitlab-logo-square.png - keywords: - - git - - ci - - deploy - - issue tracker - - code review - - wiki - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: gitlab-ce - sources: - - https://hub.docker.com/r/gitlab/gitlab-ce/ - - https://docs.gitlab.com/omnibus/ - urls: - - https://kubernetes-charts.storage.googleapis.com/gitlab-ce-0.1.6.tgz - version: 0.1.6 - - created: 2017-03-23T20:48:32.107763732Z - description: GitLab Community Edition - digest: 20c0895dd3c5c1edbc0e3be4687f0d0874599cd0c53e49560f9f0a91506957ce - home: https://about.gitlab.com - icon: https://gitlab.com/uploads/group/avatar/6543/gitlab-logo-square.png - keywords: - - git - - ci - - deploy - - issue tracker - - code review - - wiki - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: gitlab-ce - sources: - - https://hub.docker.com/r/gitlab/gitlab-ce/ - - https://docs.gitlab.com/omnibus/ - urls: - - https://kubernetes-charts.storage.googleapis.com/gitlab-ce-0.1.5.tgz - version: 0.1.5 - - created: 2017-02-14T02:48:28.88667289Z - description: GitLab Community Edition - digest: e05d4de559597760d59ca684fab107abcc0968b3260a77b16099d31e0b00cd74 - keywords: - - git - - ci - - deploy - - issue tracker - - code review - - wiki - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: gitlab-ce - sources: - - https://hub.docker.com/r/gitlab/gitlab-ce/ - - https://docs.gitlab.com/omnibus/ - urls: - - https://kubernetes-charts.storage.googleapis.com/gitlab-ce-0.1.4.tgz - version: 0.1.4 - - created: 2017-02-13T20:18:28.097071087Z - description: GitLab Community Edition - digest: a2fef3fd8d3187f8a4242d66435488bce4193ae3f2db8d3ec14da1c4373c2519 - keywords: - - git - - ci - - deploy - - issue tracker - - code review - - wiki - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: gitlab-ce - sources: - - https://hub.docker.com/r/gitlab/gitlab-ce/ - - https://docs.gitlab.com/omnibus/ - urls: - - https://kubernetes-charts.storage.googleapis.com/gitlab-ce-0.1.3.tgz - version: 0.1.3 - - created: 2017-01-26T03:18:35.336711333Z - description: GitLab Community Edition - digest: 3ca821c4e3bec2fe7541b95f94503875c508517e2f1200368268511e254df360 - keywords: - - git - - ci - - deploy - - issue tracker - - code review - - wiki - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: gitlab-ce - sources: - - https://hub.docker.com/r/gitlab/gitlab-ce/ - - https://docs.gitlab.com/omnibus/ - urls: - - https://kubernetes-charts.storage.googleapis.com/gitlab-ce-0.1.2.tgz - version: 0.1.2 - - created: 2017-01-13T20:48:31.520266166Z - description: GitLab Community Edition - digest: d78cfc8cc2e262c49e1aee3af046509b92b022a5cd4b522778e846ddcd808d9b - keywords: - - git - - ci - - deploy - - issue tracker - - code review - - wiki - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: gitlab-ce - sources: - - https://hub.docker.com/r/gitlab/gitlab-ce/ - - https://docs.gitlab.com/omnibus/ - urls: - - https://kubernetes-charts.storage.googleapis.com/gitlab-ce-0.1.1.tgz - version: 0.1.1 - - created: 2016-12-15T21:18:24.667256782Z - description: GitLab Community Edition - digest: 2c84a056e3f6d66a6aed763b2f4a26c1f4275eb3f6ca64798962a070809c6272 - keywords: - - git - - ci - - deploy - - issue tracker - - code review - - wiki - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: gitlab-ce - sources: - - https://hub.docker.com/r/gitlab/gitlab-ce/ - - https://docs.gitlab.com/omnibus/ - urls: - - https://kubernetes-charts.storage.googleapis.com/gitlab-ce-0.1.0.tgz - version: 0.1.0 - gitlab-ee: - - created: 2017-04-28T00:18:30.081242553Z - description: GitLab Enterprise Edition - digest: 3fa791ba74b0e43c05178b6eb16fb21eede5bb046e9870b854db38654768de09 - home: https://about.gitlab.com - icon: https://gitlab.com/uploads/group/avatar/6543/gitlab-logo-square.png - keywords: - - git - - ci - - deploy - - issue tracker - - code review - - wiki - maintainers: - - email: contact@quent.in - name: Quentin Rousseau - name: gitlab-ee - sources: - - https://hub.docker.com/r/gitlab/gitlab-ee/ - - https://docs.gitlab.com/omnibus/ - urls: - - https://kubernetes-charts.storage.googleapis.com/gitlab-ee-0.1.6.tgz - version: 0.1.6 - grafana: - - created: 2017-04-28T00:18:30.082782593Z - description: The leading tool for querying and visualizing time series and metrics. - digest: 721c85c6407ef534dc0d2366af3092f63229d51158abb124496efbe1a224907b - engine: gotpl - home: https://grafana.net - icon: https://raw.githubusercontent.com/grafana/grafana/master/public/img/logo_transparent_400x.png - maintainers: - - email: zanhsieh@gmail.com - name: Ming Hsieh - name: grafana - sources: - - https://github.com/grafana/grafana - urls: - - https://kubernetes-charts.storage.googleapis.com/grafana-0.3.1.tgz - version: 0.3.1 - - created: 2017-04-14T01:18:27.369088748Z - description: The leading tool for querying and visualizing time series and metrics. - digest: 8f1db00e769d13c2435841b9b89b2045653039ca377c4547f46b33ec566f60ef - engine: gotpl - home: https://grafana.net - icon: https://raw.githubusercontent.com/grafana/grafana/master/public/img/logo_transparent_400x.png - maintainers: - - email: zanhsieh@gmail.com - name: Ming Hsieh - name: grafana - sources: - - https://github.com/grafana/grafana - urls: - - https://kubernetes-charts.storage.googleapis.com/grafana-0.3.0.tgz - version: 0.3.0 - - created: 2017-04-05T18:03:30.376700685Z - description: The leading tool for querying and visualizing time series and metrics. - digest: e428e33b249f2261882632cc658c36d50df81460c091fd29404a3b3d16886416 - engine: gotpl - home: https://grafana.net - icon: https://raw.githubusercontent.com/grafana/grafana/master/public/img/logo_transparent_400x.png - maintainers: - - email: zanhsieh@gmail.com - name: Ming Hsieh - name: grafana - sources: - - https://github.com/grafana/grafana - urls: - - https://kubernetes-charts.storage.googleapis.com/grafana-0.2.5.tgz - version: 0.2.5 - - created: 2017-03-16T23:33:31.578792832Z - description: The leading tool for querying and visualizing time series and metrics. - digest: 2fefc51028c411641e5bf85b799fc8e608c7646c7054cfaa2149d4e83610d60a - engine: gotpl - home: https://grafana.net - icon: https://raw.githubusercontent.com/grafana/grafana/master/public/img/logo_transparent_400x.png - maintainers: - - email: zanhsieh@gmail.com - name: Ming Hsieh - name: grafana - sources: - - https://github.com/grafana/grafana - urls: - - https://kubernetes-charts.storage.googleapis.com/grafana-0.2.4.tgz - version: 0.2.4 - - created: 2017-03-14T23:48:31.886602615Z - description: The leading tool for querying and visualizing time series and metrics. - digest: 7fac33dcd25d8ac60071e56968db133ecfa4f796732f92044d1f7714495840fd - engine: gotpl - home: https://grafana.net - icon: https://raw.githubusercontent.com/grafana/grafana/master/public/img/logo_transparent_400x.png - maintainers: - - email: zanhsieh@gmail.com - name: Ming Hsieh - name: grafana - sources: - - https://github.com/grafana/grafana - urls: - - https://kubernetes-charts.storage.googleapis.com/grafana-0.2.3.tgz - version: 0.2.3 - - created: 2017-02-13T22:33:28.777518221Z - description: The leading tool for querying and visualizing time series and metrics. - digest: 037158b80733838ab2ad84928c999997ab76b5383cbeb4cce6c174fe5bc5ae8d - engine: gotpl - home: https://grafana.net - icon: https://raw.githubusercontent.com/grafana/grafana/master/public/img/logo_transparent_400x.png - maintainers: - - email: zanhsieh@gmail.com - name: Ming Hsieh - name: grafana - sources: - - https://github.com/grafana/grafana - urls: - - https://kubernetes-charts.storage.googleapis.com/grafana-0.2.2.tgz - version: 0.2.2 - - created: 2017-02-13T04:18:31.532928133Z - description: A Helm chart for Kubernetes - digest: e49f08bd26843eb0449304c72fd9be3e65de0d8647457f39807f9aa296ba9b6a - engine: gotpl - home: https://grafana.net - maintainers: - - email: zanhsieh@gmail.com - name: Ming Hsieh - name: grafana - sources: - - https://github.com/grafana/grafana - urls: - - https://kubernetes-charts.storage.googleapis.com/grafana-0.2.1.tgz - version: 0.2.1 - - created: 2017-01-29T23:03:26.897254812Z - description: A Helm chart for Kubernetes - digest: 283be1abb811d005b3759f65761e4465e79f2031be8a47b3d87256e88888f047 - engine: gotpl - home: https://grafana.net - maintainers: - - email: zanhsieh@gmail.com - name: Ming Hsieh - name: grafana - sources: - - https://github.com/grafana/grafana - urls: - - https://kubernetes-charts.storage.googleapis.com/grafana-0.2.0.tgz - version: 0.2.0 - influxdb: - - created: 2017-04-28T00:18:30.083302575Z - description: Scalable datastore for metrics, events, and real-time analytics. - digest: c34d6d57a1c4f5cdf1d1eb869231b27d2c080c7d219ab330f43fd9fd795b8d40 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/influxdb/ - keywords: - - influxdb - - database - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: influxdb - sources: - - https://github.com/influxdata/influxdb - urls: - - https://kubernetes-charts.storage.googleapis.com/influxdb-0.4.0.tgz - version: 0.4.0 - - created: 2017-03-23T01:19:01.442621577Z - description: Scalable datastore for metrics, events, and real-time analytics. - digest: 76522d9156e4939669344cc72a9674ce16ccc7049b06c03eaa0822ed4ce59254 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/influxdb/ - keywords: - - influxdb - - database - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: influxdb - sources: - - https://github.com/influxdata/influxdb - urls: - - https://kubernetes-charts.storage.googleapis.com/influxdb-0.3.0.tgz - version: 0.3.0 - - created: 2017-03-17T05:33:29.077307939Z - description: Scalable datastore for metrics, events, and real-time analytics. - digest: 499e87e9a0cfb2452107eaabc5e81bb5382554731b12e20dac791d81d492044e - engine: gotpl - home: https://www.influxdata.com/time-series-platform/influxdb/ - keywords: - - influxdb - - database - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: influxdb - sources: - - https://github.com/influxdata/influxdb - urls: - - https://kubernetes-charts.storage.googleapis.com/influxdb-0.2.1.tgz - version: 0.2.1 - - created: 2017-02-13T17:03:30.109119817Z - description: Scalable datastore for metrics, events, and real-time analytics. - digest: e78ce7b2aac9538e5f6087fc77e5e30ab41a2a54d9bb1381b02830dd3324f0a9 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/influxdb/ - keywords: - - influxdb - - database - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: influxdb - sources: - - https://github.com/influxdata/influxdb - urls: - - https://kubernetes-charts.storage.googleapis.com/influxdb-0.1.2.tgz - version: 0.1.2 - - created: 2017-02-13T04:33:52.294695041Z - description: Chart for InfluxDB - digest: 3341f3cca2d60540b219390688ac600ec605a331975cd402d6489969a0346aec - engine: gotpl - home: https://www.influxdata.com/time-series-platform/influxdb/ - keywords: - - influxdb - - database - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: influxdb - urls: - - https://kubernetes-charts.storage.googleapis.com/influxdb-0.1.1.tgz - version: 0.1.1 - - created: 2017-01-28T00:48:33.328518706Z - description: Chart for InfluxDB - digest: a64fad23b1d02c8f14955f652f60a36ca2bc9f01fb5f4d80cd88bcf9f96a7300 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/influxdb/ - keywords: - - influxdb - - database - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: influxdb - urls: - - https://kubernetes-charts.storage.googleapis.com/influxdb-0.1.0.tgz - version: 0.1.0 - jasperreports: - - created: 2017-04-28T00:18:30.084006378Z - description: The JasperReports server can be used as a stand-alone or embedded - reporting and BI server that offers web-based reporting, analytic tools and - visualization, and a dashboard feature for compiling multiple custom views - digest: 8a649026f55b2fa1e743c93fd331e127e66b49c4d7f20116a2bb06e5937f4828 - engine: gotpl - home: http://community.jaspersoft.com/project/jasperreports-server - icon: https://bitnami.com/assets/stacks/jasperserver/img/jasperserver-stack-110x117.png - keywords: - - business intelligence - - java - - jasper - - reporting - - analytic - - visualization - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: jasperreports - sources: - - https://github.com/bitnami/bitnami-docker-jasperreports - urls: - - https://kubernetes-charts.storage.googleapis.com/jasperreports-0.1.5.tgz - version: 0.1.5 - - created: 2017-04-06T10:18:27.580953879Z - description: The JasperReports server can be used as a stand-alone or embedded - reporting and BI server that offers web-based reporting, analytic tools and - visualization, and a dashboard feature for compiling multiple custom views - digest: d4a62f7ace55256852e5c650a56ccf671633c4f223180d304cfb03b9cd7993aa - engine: gotpl - home: http://community.jaspersoft.com/project/jasperreports-server - icon: https://bitnami.com/assets/stacks/jasperserver/img/jasperserver-stack-110x117.png - keywords: - - business intelligence - - java - - jasper - - reporting - - analytic - - visualization - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: jasperreports - sources: - - https://github.com/bitnami/bitnami-docker-jasperreports - urls: - - https://kubernetes-charts.storage.googleapis.com/jasperreports-0.1.4.tgz - version: 0.1.4 - - created: 2017-03-08T19:03:31.722665089Z - description: The JasperReports server can be used as a stand-alone or embedded - reporting and BI server that offers web-based reporting, analytic tools and - visualization, and a dashboard feature for compiling multiple custom views - digest: 99af0fca7ef1c475b239f2c8fc2dee6b040ea76b3c30bba1431f358df873aa49 - engine: gotpl - home: http://community.jaspersoft.com/project/jasperreports-server - icon: https://bitnami.com/assets/stacks/jasperserver/img/jasperserver-stack-110x117.png - keywords: - - business intelligence - - java - - jasper - - reporting - - analytic - - visualization - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: jasperreports - sources: - - https://github.com/bitnami/bitnami-docker-jasperreports - urls: - - https://kubernetes-charts.storage.googleapis.com/jasperreports-0.1.3.tgz - version: 0.1.3 - - created: 2017-02-13T21:33:27.741967589Z - description: The JasperReports server can be used as a stand-alone or embedded - reporting and BI server that offers web-based reporting, analytic tools and - visualization, and a dashboard feature for compiling multiple custom views - digest: f01e53d1b89c4fb1fcd9702cd5f4e48d77607aed65f249e1f94b8b21f7eef3f4 - engine: gotpl - home: http://community.jaspersoft.com/project/jasperreports-server - icon: https://bitnami.com/assets/stacks/jasperserver/img/jasperserver-stack-110x117.png - keywords: - - business intelligence - - java - - jasper - - reporting - - analytic - - visualization - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: jasperreports - sources: - - https://github.com/bitnami/bitnami-docker-jasperreports - urls: - - https://kubernetes-charts.storage.googleapis.com/jasperreports-0.1.2.tgz - version: 0.1.2 - - created: 2017-01-28T01:33:32.784491819Z - description: The JasperReports server can be used as a stand-alone or embedded - reporting and BI server that offers web-based reporting, analytic tools and - visualization, and a dashboard feature for compiling multiple custom views - digest: 5cc4af8c88691d7030602c97be2ccbc125ef11129b361da0aa236a127c31b965 - engine: gotpl - home: http://community.jaspersoft.com/project/jasperreports-server - icon: https://bitnami.com/assets/stacks/jasperserver/img/jasperserver-stack-110x117.png - keywords: - - business intelligence - - java - - jasper - - reporting - - analytic - - visualization - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: jasperreports - sources: - - https://github.com/bitnami/bitnami-docker-jasperreports - urls: - - https://kubernetes-charts.storage.googleapis.com/jasperreports-0.1.1.tgz - version: 0.1.1 - jenkins: - - created: 2017-04-28T00:18:30.084552323Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: e8bb6edabe1af4db4f67e2939b88fa45416167612349a3a32bcf786c529a18e7 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.6.0.tgz - version: 0.6.0 - - created: 2017-04-24T21:03:29.315054715Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: 0c962935ead654ed9422c75978195b9ec893b1e1909b38cdbc15e3dc24863f7e - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.5.1.tgz - version: 0.5.1 - - created: 2017-04-19T16:48:30.580850385Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: 338a9265e567f75469c4e227f583de14b7ab38da137b1d4fd5eee24ddea05724 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.5.0.tgz - version: 0.5.0 - - created: 2017-04-13T19:03:30.206806842Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: 66cb4ab56bdac82c98eea4ceaf0fbcd9bb7c5c446f21bb396c3ce4246a76f423 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.4.1.tgz - version: 0.4.1 - - created: 2017-04-13T05:33:26.915479665Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: 109c67f85696136b5629126952d5eb4196ca3cf36524976adf7babd3b8631782 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.4.0.tgz - version: 0.4.0 - - created: 2017-04-13T05:18:28.907645759Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: afa4583548e2e89617e21b91ef014285f060ad4a5355741260d72e27bb8eb4d3 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.3.1.tgz - version: 0.3.1 - - created: 2017-03-30T07:48:56.453711055Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: e9c5280a7cc57b16c51e70af9e4bf8b10f6525daf191c22a3a050a1791e5f7c7 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.3.0.tgz - version: 0.3.0 - - created: 2017-03-22T22:18:33.707478335Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: f5cf5bafd797d65bbbb55ff0b31935123d3f7ac283e703ac0b9df73b016edf59 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.2.0.tgz - version: 0.2.0 - - created: 2017-03-15T09:48:31.97803944Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: f8167252e615a10eb155087d6666985e8eb083781faa0485c0413de8c13deeb8 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.15.tgz - version: 0.1.15 - - created: 2017-03-14T13:33:36.105867963Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: 6c5039f7ab62e7f74bb902f5804814f66733c535908b4ffae1cf759efa3c6f24 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.14.tgz - version: 0.1.14 - - created: 2017-02-13T22:18:28.949796594Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: 90aa0fe9bac319690c871327dff6fee85e7de117e961dfa95ba12abedec4e99b - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.13.tgz - version: 0.1.13 - - created: 2017-02-13T21:48:52.587710548Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: 92224a4dcf96772652e91cee6369a08f21f4ba7d1513ee458b5724720f7045ca - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.12.tgz - version: 0.1.12 - - created: 2017-02-13T21:03:28.21318035Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: c75cadf6bc32c90cfc8a61c1f233884004670c8583edefcfcaa43b5573422923 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.10.tgz - version: 0.1.10 - - created: 2017-02-13T17:03:30.11276321Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: 47f32f975f942607a4b4ca05bd804ece5e2381840508d049251ca692e83080c0 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.9.tgz - version: 0.1.9 - - created: 2017-02-13T04:18:31.534794405Z - description: Open source continuous integration server. It supports multiple SCM - tools including CVS, Subversion and Git. It can execute Apache Ant and Apache - Maven-based projects as well as arbitrary scripts. - digest: 0764541a4165016e68bef6de1f405964b58ebb2b43b4d738f4bbbbad794b5df8 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.8.tgz - version: 0.1.8 - - created: 2017-01-27T21:48:32.069740444Z - description: A Jenkins Helm chart for Kubernetes. - digest: c29819a435e9474277846492930e910c9f7c0605717421c4efe411ce476283ab - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.7.tgz - version: 0.1.7 - - created: 2017-01-04T00:48:19.7378014Z - description: A Jenkins Helm chart for Kubernetes. - digest: 58ddd6442c8534de79a8d5eaca48263c744ca9fa6e9af7ceb28d1fda9b88ddb5 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.6.tgz - version: 0.1.6 - - created: 2016-12-19T22:03:51.103057889Z - description: A Jenkins Helm chart for Kubernetes. - digest: 2c8dff77b7ad736ac54e5335f5d52bfacaf7ea2ad97558da507b7f5bb9f4d549 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.5.tgz - version: 0.1.5 - - created: 2016-12-09T18:48:20.183887307Z - description: A Jenkins Helm chart for Kubernetes. - digest: 0b7016624acec8d66f6d919611e8f1c9853a5475c9801c3da6e50b7ce044ed57 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.4.tgz - version: 0.1.4 - - created: 2016-12-05T18:33:22.028569484Z - description: A Jenkins Helm chart for Kubernetes. - digest: 82b53a4c90ac2cf71bb41d870939ce1e980192e1407ba8825051c0ed98c04906 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.1.tgz - version: 0.1.1 - - created: 2016-10-19T00:03:14.028672648Z - description: A Jenkins Helm chart for Kubernetes. - digest: ea97fbfeaf9b9701d71dbc2b6fa50bff25a33f0565233d81170a6ac225d22ab4 - home: https://jenkins.io/ - icon: https://wiki.jenkins-ci.org/download/attachments/2916393/logo.png - maintainers: - - email: lachlan@deis.com - name: Lachlan Evenson - - email: viglesias@google.com - name: Vic Iglesias - name: jenkins - sources: - - https://github.com/jenkinsci/jenkins - - https://github.com/jenkinsci/docker-jnlp-slave - urls: - - https://kubernetes-charts.storage.googleapis.com/jenkins-0.1.0.tgz - version: 0.1.0 - joomla: - - created: 2017-04-28T00:18:30.085219154Z - description: PHP content management system (CMS) for publishing web content - digest: 6f9934487533f325515f4877b3af1306c87d64bf3ece9d4bd875289cd73b340d - engine: gotpl - home: http://www.joomla.org/ - icon: https://bitnami.com/assets/stacks/joomla/img/joomla-stack-220x234.png - keywords: - - joomla - - cms - - blog - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: joomla - sources: - - https://github.com/bitnami/bitnami-docker-joomla - urls: - - https://kubernetes-charts.storage.googleapis.com/joomla-0.4.6.tgz - version: 0.4.6 - - created: 2017-04-06T11:03:25.397962583Z - description: PHP content management system (CMS) for publishing web content - digest: f9dedab2fc2dbf170cf45b2c230baa6d20aad9a6f8ccfcb09c459602fc5213dc - engine: gotpl - home: http://www.joomla.org/ - icon: https://bitnami.com/assets/stacks/joomla/img/joomla-stack-220x234.png - keywords: - - joomla - - cms - - blog - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: joomla - sources: - - https://github.com/bitnami/bitnami-docker-joomla - urls: - - https://kubernetes-charts.storage.googleapis.com/joomla-0.4.5.tgz - version: 0.4.5 - - created: 2017-03-02T19:33:28.178324258Z - description: PHP content management system (CMS) for publishing web content - digest: 1e067e459873ae832d54ff516a3420f7f0e16ecd8f72f4c4f02be22e47702077 - engine: gotpl - home: http://www.joomla.org/ - icon: https://bitnami.com/assets/stacks/joomla/img/joomla-stack-220x234.png - keywords: - - joomla - - cms - - blog - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: joomla - sources: - - https://github.com/bitnami/bitnami-docker-joomla - urls: - - https://kubernetes-charts.storage.googleapis.com/joomla-0.4.4.tgz - version: 0.4.4 - - created: 2017-02-27T16:48:32.159029074Z - description: PHP content management system (CMS) for publishing web content - digest: 9a99b15e83e18955eb364985cd545659f1176ef203ac730876dfe39499edfb18 - engine: gotpl - home: http://www.joomla.org/ - icon: https://bitnami.com/assets/stacks/joomla/img/joomla-stack-220x234.png - keywords: - - joomla - - cms - - blog - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: joomla - sources: - - https://github.com/bitnami/bitnami-docker-joomla - urls: - - https://kubernetes-charts.storage.googleapis.com/joomla-0.4.3.tgz - version: 0.4.3 - - created: 2017-02-11T00:18:52.272598223Z - description: PHP content management system (CMS) for publishing web content - digest: 07c3a16eb674ffc74fe5b2b16191b8bb24c63bdae9bce9710bda1999920c46fc - engine: gotpl - home: http://www.joomla.org/ - keywords: - - joomla - - cms - - blog - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: joomla - sources: - - https://github.com/bitnami/bitnami-docker-joomla - urls: - - https://kubernetes-charts.storage.googleapis.com/joomla-0.4.2.tgz - version: 0.4.2 - - created: 2017-01-21T00:18:31.345952551Z - description: PHP content management system (CMS) for publishing web content - digest: 95fbe272015941544609eee90b3bffd5172bfdec10be13636510caa8478a879e - engine: gotpl - home: http://www.joomla.org/ - keywords: - - joomla - - cms - - blog - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: joomla - sources: - - https://github.com/bitnami/bitnami-docker-joomla - urls: - - https://kubernetes-charts.storage.googleapis.com/joomla-0.4.1.tgz - version: 0.4.1 - - created: 2016-12-09T18:48:20.18539038Z - description: PHP content management system (CMS) for publishing web content - digest: 0a01ea051ec15274932c8d82076c1a9fd62584b0fb916a81372319bef223c20e - engine: gotpl - home: http://www.joomla.org/ - keywords: - - joomla - - cms - - blog - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: joomla - sources: - - https://github.com/bitnami/bitnami-docker-joomla - urls: - - https://kubernetes-charts.storage.googleapis.com/joomla-0.4.0.tgz - version: 0.4.0 - kapacitor: - - created: 2017-04-28T00:18:30.085544137Z - description: InfluxDB's native data processing engine. It can process both stream - and batch data from InfluxDB. - digest: f484ef5acbc3ad68bb765adb1fd6c74920b9265371d3379e43a47ec266dc8449 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/kapacitor/ - keywords: - - kapacitor - - stream - - etl - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: kapacitor - sources: - - https://github.com/influxdata/kapacitor - urls: - - https://kubernetes-charts.storage.googleapis.com/kapacitor-0.2.2.tgz - version: 0.2.2 - - created: 2017-03-22T21:33:33.841225941Z - description: InfluxDB's native data processing engine. It can process both stream - and batch data from InfluxDB. - digest: c592b3bf2dec92c2273057b984b7289c17032dc578ea0f2ec86aeb5e838a7047 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/kapacitor/ - keywords: - - kapacitor - - stream - - etl - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: kapacitor - sources: - - https://github.com/influxdata/kapacitor - urls: - - https://kubernetes-charts.storage.googleapis.com/kapacitor-0.2.1.tgz - version: 0.2.1 - - created: 2017-02-13T21:48:52.58883187Z - description: InfluxDB's native data processing engine. It can process both stream - and batch data from InfluxDB. - digest: 18971c9c5b3805830f72fdfacd6c1dfc173db4797994f61b3666296b67abe70a - engine: gotpl - home: https://www.influxdata.com/time-series-platform/kapacitor/ - keywords: - - kapacitor - - stream - - etl - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: kapacitor - sources: - - https://github.com/influxdata/kapacitor - urls: - - https://kubernetes-charts.storage.googleapis.com/kapacitor-0.1.2.tgz - version: 0.1.2 - - created: 2017-02-13T21:03:28.215149313Z - description: Chart for Chronograf - digest: e0d29608602df25a9352629afd3fd7615e01a23549e1d1eba8101ee1b725e528 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/kapacitor/ - keywords: - - kapacitor - - stream - - etl - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: kapacitor - urls: - - https://kubernetes-charts.storage.googleapis.com/kapacitor-0.1.1.tgz - version: 0.1.1 - - created: 2017-01-28T01:33:32.786133788Z - description: Chart for Chronograf - digest: efde65696634d3bf1dc4c9caae44e68f7ed8c7599aab809c8cdd0fde7e4f9efc - engine: gotpl - home: https://www.influxdata.com/time-series-platform/kapacitor/ - keywords: - - kapacitor - - stream - - etl - - timeseries - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: kapacitor - urls: - - https://kubernetes-charts.storage.googleapis.com/kapacitor-0.1.0.tgz - version: 0.1.0 - kube-lego: - - apiVersion: v1 - created: 2017-04-28T00:18:30.085897046Z - description: Automatically requests certificates from Let's Encrypt - digest: adc8f0fe1c244e5abda2d918afe9153d47648b2acb779adaf796fa5a7266ea6b - engine: gotpl - keywords: - - kube-lego - - letsencrypt - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - - email: mgoodness@gmail.com - name: Michael Goodness - name: kube-lego - sources: - - https://github.com/jetstack/kube-lego/tree/master/examples/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/kube-lego-0.1.8.tgz - version: 0.1.8 - - apiVersion: v1 - created: 2017-03-20T20:03:37.426644363Z - description: Automatically requests certificates from Let's Encrypt - digest: 0110b8c36f6ad016684ef904b5e514751ffa7a89ba7b88cf4a8376c889e03693 - engine: gotpl - keywords: - - kube-lego - - letsencrypt - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - - email: mgoodness@gmail.com - name: Michael Goodness - name: kube-lego - sources: - - https://github.com/jetstack/kube-lego/tree/master/examples/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/kube-lego-0.1.7.tgz - version: 0.1.7 - - apiVersion: v1 - created: 2017-03-02T19:33:28.179029307Z - description: Automatically requests certificates from Let's Encrypt - digest: 275265fda80ccc62376ebd3a200bbea94b105cbf3481efcc726d6a33d73da8d0 - engine: gotpl - keywords: - - kube-lego - - letsencrypt - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - - email: mgoodness@gmail.com - name: Michael Goodness - name: kube-lego - sources: - - https://github.com/jetstack/kube-lego/tree/master/examples/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/kube-lego-0.1.6.tgz - version: 0.1.6 - - apiVersion: v1 - created: 2017-02-27T17:33:27.264070807Z - description: Automatically requests certificates from Let's Encrypt - digest: 62de296cc21c6b286097de9ac389033e18a4cd3cf414cd97b7a02d4c3be14d6d - engine: gotpl - keywords: - - kube-lego - - letsencrypt - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - - email: mgoodness@gmail.com - name: Michael Goodness - name: kube-lego - sources: - - https://github.com/jetstack/kube-lego/tree/master/examples/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/kube-lego-0.1.5.tgz - version: 0.1.5 - - apiVersion: v1 - created: 2017-02-14T15:48:28.578398517Z - description: Automatically requests certificates from Let's Encrypt - digest: 1bc993b68eb51fb00e4eb18a65df9e2182f895bd46134d6ddfdc0dd3b6432a98 - engine: gotpl - keywords: - - kube-lego - - letsencrypt - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - - email: mgoodness@gmail.com - name: Michael Goodness - name: kube-lego - sources: - - https://github.com/jetstack/kube-lego/tree/master/examples/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/kube-lego-0.1.4.tgz - version: 0.1.4 - - apiVersion: v1 - created: 2017-02-11T00:48:25.354730418Z - description: Automatically requests certificates from Let's Encrypt - digest: d4b3694951c2bb42b356217a8f12870bd3806dac4de3390056279998fd634c34 - engine: gotpl - keywords: - - kube-lego - - letsencrypt - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - - email: mgoodness@gmail.com - name: Michael Goodness - name: kube-lego - sources: - - https://github.com/jetstack/kube-lego/tree/master/examples/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/kube-lego-0.1.3.tgz - version: 0.1.3 - kube-ops-view: - - created: 2017-04-28T00:18:30.086143954Z - description: Kubernetes Operational View - read-only system dashboard for multiple - K8s clusters - digest: 9e7b62f2d781aaacee801e91efe4d6b6ef6df9d511c0d6fb2d6e7977c8d0a06e - home: https://github.com/hjacobs/kube-ops-view - icon: https://raw.githubusercontent.com/hjacobs/kube-ops-view/master/kube-ops-view-logo.png - keywords: - - kubernetes - - dashboard - - operations - maintainers: - - email: henning@jacobs1.de - name: Henning Jacobs - name: kube-ops-view - sources: - - https://github.com/hjacobs/kube-ops-view - urls: - - https://kubernetes-charts.storage.googleapis.com/kube-ops-view-0.2.0.tgz - version: 0.2.0 - kube2iam: - - created: 2017-04-28T00:18:30.086423576Z - description: Provide IAM credentials to pods based on annotations. - digest: 2035d8dfae5733fa475914694cd060e28954b1f5c930b6c4800ee165d23abc46 - engine: gotpl - keywords: - - kube2iam - - aws - - iam - - security - maintainers: - - email: jm.carp@gmail.com - name: Josh Carp - - email: michael.haselton@gmail.com - name: Michael Haselton - - email: mgoodness@gmail.com - name: Michael Goodness - name: kube2iam - sources: - - https://github.com/jtblin/kube2iam - urls: - - https://kubernetes-charts.storage.googleapis.com/kube2iam-0.2.1.tgz - version: 0.2.1 - - created: 2017-03-02T18:48:30.431985789Z - description: Provide IAM credentials to pods based on annotations. - digest: 5d2226fb101b800fc5a6edb5566d329880c604d75f4245da55f47027bcf5546e - engine: gotpl - keywords: - - kube2iam - - aws - - iam - - security - maintainers: - - email: jm.carp@gmail.com - name: Josh Carp - - email: michael.haselton@gmail.com - name: Michael Haselton - - email: mgoodness@gmail.com - name: Michael Goodness - name: kube2iam - sources: - - https://github.com/jtblin/kube2iam - urls: - - https://kubernetes-charts.storage.googleapis.com/kube2iam-0.2.0.tgz - version: 0.2.0 - - created: 2017-02-13T17:03:30.115672844Z - description: Provide IAM credentials to containers running inside a kubernetes - cluster based on annotations. - digest: 63d913fb8f31c287b1f1d45d310517c0b22597ecdaef19d7ad2520bff990969a - engine: gotpl - keywords: - - kube2iam - - aws - - iam - - security - maintainers: - - email: jm.carp@gmail.com - name: Josh Carp - - email: michael.haselton@gmail.com - name: Michael Haselton - name: kube2iam - sources: - - https://github.com/jtblin/kube2iam - urls: - - https://kubernetes-charts.storage.googleapis.com/kube2iam-0.1.1.tgz - version: 0.1.1 - - created: 2017-01-19T00:33:27.789172853Z - description: Provide IAM credentials to containers running inside a kubernetes - cluster based on annotations. - digest: b4de5bddfa0af6737ea91d9b3a9bcfc3a0e5a656045e06038505613b214120fc - engine: gotpl - keywords: - - kube2iam - - aws - - iam - - security - maintainers: - - email: jm.carp@gmail.com - name: Josh Carp - - email: michael.haselton@gmail.com - name: Michael Haselton - name: kube2iam - sources: - - https://github.com/jtblin/kube2iam - urls: - - https://kubernetes-charts.storage.googleapis.com/kube2iam-0.1.0.tgz - version: 0.1.0 - linkerd: - - apiVersion: v1 - created: 2017-04-28T00:18:30.086763621Z - description: Service mesh for cloud native apps - digest: 9341ea67647129999beadb49f1a33131a187569581095907f902daffa73dd73b - home: https://linkerd.io/ - icon: https://pbs.twimg.com/profile_images/690258997237014528/KNgQd9GL_400x400.png - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: linkerd - sources: - - https://github.com/BuoyantIO/linkerd - urls: - - https://kubernetes-charts.storage.googleapis.com/linkerd-0.2.0.tgz - version: 0.2.0 - - apiVersion: v1 - created: 2017-03-27T13:18:34.232799345Z - description: Service mesh for cloud native apps - digest: f9f2287d026c6de3a522bdcffdff061f81a8e64274da4acefdc9d2177b603570 - home: https://linkerd.io/ - icon: https://pbs.twimg.com/profile_images/690258997237014528/KNgQd9GL_400x400.png - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: linkerd - sources: - - https://github.com/BuoyantIO/linkerd - urls: - - https://kubernetes-charts.storage.googleapis.com/linkerd-0.1.1.tgz - version: 0.1.1 - - apiVersion: v1 - created: 2017-02-13T04:33:52.299092507Z - description: Service mesh for cloud native apps - digest: 147759e4982f1dffce894e0d4242fb914d21014700a7d9891e8dc352318633fa - home: https://linkerd.io/ - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: linkerd - sources: - - https://github.com/BuoyantIO/linkerd - urls: - - https://kubernetes-charts.storage.googleapis.com/linkerd-0.1.0.tgz - version: 0.1.0 - locust: - - created: 2017-04-28T00:18:30.087097677Z - description: A modern load testing framework - digest: eb91b0e3c0b618cf5ad0f24d2685fe4086bc6f497685e58ad8a64032c4e82b7a - home: http://locust.io - icon: https://pbs.twimg.com/profile_images/1867636195/locust-logo-orignal.png - maintainers: - - email: vincent.drl@gmail.com - name: Vincent De Smet - name: locust - sources: - - https://github.com/honestbee/distributed-load-testing - urls: - - https://kubernetes-charts.storage.googleapis.com/locust-0.1.2.tgz - version: 0.1.2 - - created: 2017-04-20T16:18:33.345004534Z - description: A chart for locust distributed load testing - digest: 40a353b38823eaa4954dcb70ae92858671a16fb8d00ac9677a36912aab6b629a - name: locust - sources: - - https://github.com/honestbee/distributed-load-testing - urls: - - https://kubernetes-charts.storage.googleapis.com/locust-0.1.1.tgz - version: 0.1.1 - - created: 2017-04-11T16:03:29.689097384Z - description: A chart for locust distributed load testing - digest: 2cd175e8c8d66625ca3608046d5764662b53e3760aa260a934d3d2ee35148c49 - name: locust - sources: - - https://github.com/honestbee/distributed-load-testing - urls: - - https://kubernetes-charts.storage.googleapis.com/locust-0.1.0.tgz - version: 0.1.0 - magento: - - created: 2017-04-28T00:18:30.087794886Z - description: A feature-rich flexible e-commerce solution. It includes transaction - options, multi-store functionality, loyalty programs, product categorization - and shopper filtering, promotion rules, and more. - digest: 2d7ebf934ca0e742e248753da6502f05929b51329a42d0d4b3db4e1824381f11 - engine: gotpl - home: https://magento.com/ - icon: https://bitnami.com/assets/stacks/magento/img/magento-stack-110x117.png - keywords: - - magento - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: magento - sources: - - https://github.com/bitnami/bitnami-docker-magento - urls: - - https://kubernetes-charts.storage.googleapis.com/magento-0.4.6.tgz - version: 0.4.6 - - created: 2017-03-31T19:33:30.42890495Z - description: A feature-rich flexible e-commerce solution. It includes transaction - options, multi-store functionality, loyalty programs, product categorization - and shopper filtering, promotion rules, and more. - digest: a289a051d6d2e8cf833398d7d63fcb1fd953d202f0755800eebe568804fce525 - engine: gotpl - home: https://magento.com/ - icon: https://bitnami.com/assets/stacks/magento/img/magento-stack-110x117.png - keywords: - - magento - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: magento - sources: - - https://github.com/bitnami/bitnami-docker-magento - urls: - - https://kubernetes-charts.storage.googleapis.com/magento-0.4.5.tgz - version: 0.4.5 - - created: 2017-03-09T19:33:31.142591331Z - description: A feature-rich flexible e-commerce solution. It includes transaction - options, multi-store functionality, loyalty programs, product categorization - and shopper filtering, promotion rules, and more. - digest: 311a98182281b981f60fbd446b42f6af4b86e0bac9445bf2248b1b47dfce5933 - engine: gotpl - home: https://magento.com/ - icon: https://bitnami.com/assets/stacks/magento/img/magento-stack-110x117.png - keywords: - - magento - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: magento - sources: - - https://github.com/bitnami/bitnami-docker-magento - urls: - - https://kubernetes-charts.storage.googleapis.com/magento-0.4.4.tgz - version: 0.4.4 - - created: 2017-03-02T19:33:28.180428252Z - description: A feature-rich flexible e-commerce solution. It includes transaction - options, multi-store functionality, loyalty programs, product categorization - and shopper filtering, promotion rules, and more. - digest: fccfa89493a977a2f898a5e6d3c6fa4fda15faf0dc0a9e48772744a32c2b1d24 - engine: gotpl - home: https://magento.com/ - icon: https://bitnami.com/assets/stacks/magento/img/magento-stack-110x117.png - keywords: - - magento - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: magento - sources: - - https://github.com/bitnami/bitnami-docker-magento - urls: - - https://kubernetes-charts.storage.googleapis.com/magento-0.4.3.tgz - version: 0.4.3 - - created: 2017-02-27T16:48:32.161186722Z - description: A feature-rich flexible e-commerce solution. It includes transaction - options, multi-store functionality, loyalty programs, product categorization - and shopper filtering, promotion rules, and more. - digest: fb33cafa4874855419ea3178dc0660a856ed6f8b581b984f38e86701d54e54a6 - engine: gotpl - home: https://magento.com/ - icon: https://bitnami.com/assets/stacks/magento/img/magento-stack-110x117.png - keywords: - - magento - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: magento - sources: - - https://github.com/bitnami/bitnami-docker-magento - urls: - - https://kubernetes-charts.storage.googleapis.com/magento-0.4.2.tgz - version: 0.4.2 - - created: 2017-01-21T00:18:31.349108265Z - description: A feature-rich flexible e-commerce solution. It includes transaction - options, multi-store functionality, loyalty programs, product categorization - and shopper filtering, promotion rules, and more. - digest: c597107a5cf512d616e5e4134e562f8fcc6e79ad1f78241e9a40ded77e77ef62 - engine: gotpl - home: https://magento.com/ - icon: https://bitnami.com/assets/stacks/magento/img/magento-stack-110x117.png - keywords: - - magento - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: magento - sources: - - https://github.com/bitnami/bitnami-docker-magento - urls: - - https://kubernetes-charts.storage.googleapis.com/magento-0.4.1.tgz - version: 0.4.1 - - created: 2017-01-04T00:48:19.74048297Z - description: A feature-rich flexible e-commerce solution. It includes transaction - options, multi-store functionality, loyalty programs, product categorization - and shopper filtering, promotion rules, and more. - digest: 526115583b8ae741f6819c2a0d3e719a6622a63a0a2e9918e56d2ebaa6ad498b - engine: gotpl - home: https://magento.com/ - icon: https://bitnami.com/assets/stacks/magento/img/magento-stack-110x117.png - keywords: - - magento - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: magento - sources: - - https://github.com/bitnami/bitnami-docker-magento - urls: - - https://kubernetes-charts.storage.googleapis.com/magento-0.4.0.tgz - version: 0.4.0 - mariadb: - - created: 2017-04-28T00:18:30.088188975Z - description: Fast, reliable, scalable, and easy to use open-source relational - database system. MariaDB Server is intended for mission-critical, heavy-load - production systems as well as for embedding into mass-deployed software. - digest: bc13b8aa3728de5595bb83ea8a5c21f9234e169352739d8e72f04d1ed2558508 - engine: gotpl - home: https://mariadb.org - icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.6.0.tgz - version: 0.6.0 - - created: 2017-04-03T22:33:26.672780802Z - description: Fast, reliable, scalable, and easy to use open-source relational - database system. MariaDB Server is intended for mission-critical, heavy-load - production systems as well as for embedding into mass-deployed software. - digest: 23b81e8bc8cd8577a47a73578b56f68a2a3c15640e0a0fb0ac7f1646845bc0b0 - engine: gotpl - home: https://mariadb.org - icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.14.tgz - version: 0.5.14 - - created: 2017-03-23T19:18:29.910295788Z - description: Fast, reliable, scalable, and easy to use open-source relational - database system. MariaDB Server is intended for mission-critical, heavy-load - production systems as well as for embedding into mass-deployed software. - digest: a4c8e19714e2a3d29eae03c63a63a7343b3e4f62df2956603acca7eb42ab15de - engine: gotpl - home: https://mariadb.org - icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.13.tgz - version: 0.5.13 - - created: 2017-03-17T22:48:29.250381078Z - description: Fast, reliable, scalable, and easy to use open-source relational - database system. MariaDB Server is intended for mission-critical, heavy-load - production systems as well as for embedding into mass-deployed software. - digest: 8d5cd0e79d675baa79ab4f999da8e10037ae4c2158e434b7b60d4ff345f1d976 - engine: gotpl - home: https://mariadb.org - icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.12.tgz - version: 0.5.12 - - created: 2017-03-16T13:33:30.840089178Z - description: Fast, reliable, scalable, and easy to use open-source relational - database system. MariaDB Server is intended for mission-critical, heavy-load - production systems as well as for embedding into mass-deployed software. - digest: 0841b64dbe2fe499bd3554fa9b2074be0ed6535e9f87d3ea2c13bcfc2616717b - engine: gotpl - home: https://mariadb.org - icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.11.tgz - version: 0.5.11 - - created: 2017-03-15T23:03:34.130536152Z - description: Fast, reliable, scalable, and easy to use open-source relational - database system. MariaDB Server is intended for mission-critical, heavy-load - production systems as well as for embedding into mass-deployed software. - digest: 587d7ef318ee1229d14e3b783c3e825f90f274f56b2eb62f1c7126f69fec6fc2 - engine: gotpl - home: https://mariadb.org - icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.10.tgz - version: 0.5.10 - - created: 2017-03-08T19:03:31.728285865Z - description: Fast, reliable, scalable, and easy to use open-source relational - database system. MariaDB Server is intended for mission-critical, heavy-load - production systems as well as for embedding into mass-deployed software. - digest: cfec18e7be68a616a5e4944c21ed0b65dab1701cf7182d0d1bdea83df6a77ab1 - engine: gotpl - home: https://mariadb.org - icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.9.tgz - version: 0.5.9 - - created: 2017-02-11T00:18:52.27620633Z - description: Chart for MariaDB - digest: 33990fb57b2fafb4396498e02a8083f6a476cf0b465a4a52905a67aab73f3f43 - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.8.tgz - version: 0.5.8 - - created: 2017-01-31T01:03:26.972704179Z - description: Chart for MariaDB - digest: 177a882b4248d6ebb2e72e125a3be775085bf082e4eb8f4359fee9e8640ede50 - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.7.tgz - version: 0.5.7 - - created: 2017-01-27T22:33:30.411626628Z - description: Chart for MariaDB - digest: 730d179ab0d5922494e3c21594ce7b4f118f7fe796071dfcfdbbc2714ada7d15 - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.6.tgz - version: 0.5.6 - - created: 2016-12-15T23:18:25.557355931Z - description: Chart for MariaDB - digest: c7811fad8165eb5d57eb55ad4e58d63e949d2f710c0fbbcd28cca11bb6af6de6 - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.5.tgz - version: 0.5.5 - - created: 2016-12-09T18:48:20.185787408Z - description: Chart for MariaDB - digest: c6b73c5146d085a202f838741526fe5fdc2fae55b8f5f17c0c42fd195c65311f - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.4.tgz - version: 0.5.4 - - created: 2016-11-23T00:33:20.016012859Z - description: Chart for MariaDB - digest: 38786f4f6fe4fb7a2dcbc38aa0bfea72e40e4d9766e1679942af60e25b5ba9bd - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.3.tgz - version: 0.5.3 - - created: 2016-11-03T19:33:29.120356141Z - description: Chart for MariaDB - digest: 8eb24c65fbdb75711a8ffd8f8f6ed206951cdc3a24d74a355121bc11c6c02f3b - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.2.tgz - version: 0.5.2 - - created: 2016-10-19T00:03:14.031007257Z - description: Chart for MariaDB - digest: 6544dbf62586f570762a3a469f0086fe595379454fb46a53d001206a0e282268 - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.1.tgz - version: 0.5.1 - - created: 2016-10-19T00:03:14.030598543Z - description: Chart for MariaDB - digest: c9f67f8140b3e7243d479f819d4ec8b2e992ee462b8fa579b920e86066955312 - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.5.0.tgz - version: 0.5.0 - - created: 2016-10-19T00:03:14.03012509Z - description: Chart for MariaDB - digest: fe8adb0730567ad8cd63501ac18b178ec2a9a590d43dd7ad91fd2d5fcf6114be - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.4.0.tgz - version: 0.4.0 - - created: 2016-10-19T00:03:14.029775557Z - description: Chart for MariaDB - digest: 47fe08909187da025e7e86e9534a736df11e2629c5c123178113fe776992e9e7 - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.3.1.tgz - version: 0.3.1 - - created: 2016-10-19T00:03:14.02942396Z - description: Chart for MariaDB - digest: 7354d2672b3983e98fe5c31d96d2c3d9c73edefe642eb5e1981484804315c4bc - engine: gotpl - home: https://mariadb.org - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mariadb - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.3.0.tgz - version: 0.3.0 - mediawiki: - - created: 2017-04-28T00:18:30.088860566Z - description: Extremely powerful, scalable software and a feature-rich wiki implementation - that uses PHP to process and display data stored in a database. - digest: 0e51822c5547895109a5b41ce426c77f62d0434b40f3021afee8471ab976a6f5 - engine: gotpl - home: http://www.mediawiki.org/ - icon: https://bitnami.com/assets/stacks/mediawiki/img/mediawiki-stack-220x234.png - keywords: - - mediawiki - - wiki - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mediawiki - sources: - - https://github.com/bitnami/bitnami-docker-mediawiki - urls: - - https://kubernetes-charts.storage.googleapis.com/mediawiki-0.4.6.tgz - version: 0.4.6 - - created: 2017-04-06T10:18:27.586263816Z - description: Extremely powerful, scalable software and a feature-rich wiki implementation - that uses PHP to process and display data stored in a database. - digest: 0e419c2c5d87997f94a32da6597af3f3b52120dc1ec682dcbb6b238fb4825e06 - engine: gotpl - home: http://www.mediawiki.org/ - icon: https://bitnami.com/assets/stacks/mediawiki/img/mediawiki-stack-220x234.png - keywords: - - mediawiki - - wiki - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mediawiki - sources: - - https://github.com/bitnami/bitnami-docker-mediawiki - urls: - - https://kubernetes-charts.storage.googleapis.com/mediawiki-0.4.5.tgz - version: 0.4.5 - - created: 2017-03-08T19:03:31.729226516Z - description: Extremely powerful, scalable software and a feature-rich wiki implementation - that uses PHP to process and display data stored in a database. - digest: 6f4dde26737f7f1aa63ffda6c259ce388e3a3509225f90f334bfc3f0f7617bc1 - engine: gotpl - home: http://www.mediawiki.org/ - icon: https://bitnami.com/assets/stacks/mediawiki/img/mediawiki-stack-220x234.png - keywords: - - mediawiki - - wiki - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mediawiki - sources: - - https://github.com/bitnami/bitnami-docker-mediawiki - urls: - - https://kubernetes-charts.storage.googleapis.com/mediawiki-0.4.4.tgz - version: 0.4.4 - - created: 2017-02-13T04:18:31.539427547Z - description: Extremely powerful, scalable software and a feature-rich wiki implementation - that uses PHP to process and display data stored in a database. - digest: 0ba52b8c4c9e0bee3eb76fe625d2dc88729a1cdf41ace9d13cd4abc5b477cfb8 - engine: gotpl - home: http://www.mediawiki.org/ - keywords: - - mediawiki - - wiki - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mediawiki - sources: - - https://github.com/bitnami/bitnami-docker-mediawiki - urls: - - https://kubernetes-charts.storage.googleapis.com/mediawiki-0.4.3.tgz - version: 0.4.3 - - created: 2017-01-21T00:18:31.350311075Z - description: Extremely powerful, scalable software and a feature-rich wiki implementation - that uses PHP to process and display data stored in a database. - digest: f49df3e17f97b238743aad0376eb9db7e4a9bca3829a3a65d7bbb349344a73be - engine: gotpl - home: http://www.mediawiki.org/ - keywords: - - mediawiki - - wiki - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mediawiki - sources: - - https://github.com/bitnami/bitnami-docker-mediawiki - urls: - - https://kubernetes-charts.storage.googleapis.com/mediawiki-0.4.2.tgz - version: 0.4.2 - - created: 2016-12-15T21:18:24.670966268Z - description: Extremely powerful, scalable software and a feature-rich wiki implementation - that uses PHP to process and display data stored in a database. - digest: 339a90050d5cf4216140409349a356aa7cd8dc95e2cbdca06e4fdd11e87aa963 - engine: gotpl - home: http://www.mediawiki.org/ - keywords: - - mediawiki - - wiki - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mediawiki - sources: - - https://github.com/bitnami/bitnami-docker-mediawiki - urls: - - https://kubernetes-charts.storage.googleapis.com/mediawiki-0.4.1.tgz - version: 0.4.1 - - created: 2016-12-09T18:48:20.186416136Z - description: Extremely powerful, scalable software and a feature-rich wiki implementation - that uses PHP to process and display data stored in a database. - digest: 9617f13f51f5bb016a072f2a026c627420721a1c5b7cd22f32d6cd0c90f34eda - engine: gotpl - home: http://www.mediawiki.org/ - keywords: - - mediawiki - - wiki - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mediawiki - sources: - - https://github.com/bitnami/bitnami-docker-mediawiki - urls: - - https://kubernetes-charts.storage.googleapis.com/mediawiki-0.4.0.tgz - version: 0.4.0 - memcached: - - created: 2017-04-28T00:18:30.090586195Z - description: Free & open source, high-performance, distributed memory object caching - system. - digest: 36ceb2767094598171b2851ecda54bd43d862b9b81aa4b294f3d8c8d59ddd79c - engine: gotpl - home: http://memcached.org/ - icon: https://upload.wikimedia.org/wikipedia/en/thumb/2/27/Memcached.svg/1024px-Memcached.svg.png - keywords: - - memcached - - cache - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: memcached - sources: - - https://github.com/docker-library/memcached - urls: - - https://kubernetes-charts.storage.googleapis.com/memcached-0.4.1.tgz - version: 0.4.1 - - created: 2017-02-13T04:18:31.539713999Z - description: Chart for Memcached - digest: 2b918dd8129a9d706e58b3de459004e3367c05a162d3e3cdb031cb6818d5f820 - engine: gotpl - home: http://memcached.org/ - keywords: - - memcached - - cache - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: memcached - sources: - - https://github.com/docker-library/memcached - urls: - - https://kubernetes-charts.storage.googleapis.com/memcached-0.4.0.tgz - version: 0.4.0 - minecraft: - - created: 2017-04-28T00:18:30.09103508Z - description: Minecraft server - digest: bc18a8356092c19f39ce94ff34b2160650a7bebca5723fd2e2f4e350c38793c0 - keywords: - - game - - server - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: minecraft - sources: - - https://hub.docker.com/r/itzg/minecraft-server/~/dockerfile/ - - https://github.com/itzg/dockerfiles - urls: - - https://kubernetes-charts.storage.googleapis.com/minecraft-0.1.3.tgz - version: 0.1.3 - - created: 2017-03-16T23:33:31.588714127Z - description: Minecraft server - digest: 5a42451d7c2f69b7b77c40e91ef60ca284798bcab8aa5b97b1f3f2559612d443 - keywords: - - game - - server - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: minecraft - sources: - - https://hub.docker.com/r/itzg/minecraft-server/~/dockerfile/ - - https://github.com/itzg/dockerfiles - urls: - - https://kubernetes-charts.storage.googleapis.com/minecraft-0.1.2.tgz - version: 0.1.2 - - created: 2017-01-30T23:33:28.437640114Z - description: Minecraft server - digest: 87c2ef91c8feaee680bb26ba16362c6b366429e0f54119c40dbf7a5ce3f22553 - keywords: - - game - - server - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: minecraft - sources: - - https://hub.docker.com/r/itzg/minecraft-server/~/dockerfile/ - - https://github.com/itzg/dockerfiles - urls: - - https://kubernetes-charts.storage.googleapis.com/minecraft-0.1.1.tgz - version: 0.1.1 - - created: 2016-12-05T21:03:20.22190695Z - description: Minecraft server - digest: 2cd423a28eb8a0186fba171cc17dcbe6f5a53d1a99e4078f9713afed3b61cce6 - keywords: - - game - - server - maintainers: - - email: gtaylor@gc-taylor.com - name: Greg Taylor - name: minecraft - sources: - - https://hub.docker.com/r/itzg/minecraft-server/~/dockerfile/ - - https://github.com/itzg/dockerfiles - urls: - - https://kubernetes-charts.storage.googleapis.com/minecraft-0.1.0.tgz - version: 0.1.0 - minio: - - apiVersion: v1 - created: 2017-04-28T00:18:30.091526674Z - description: Distributed object storage server built for cloud applications and - devops. - digest: 12eb77d0d25971c86a4df25297eea8d579b4b5dc6599d16f340f553977c53914 - home: https://minio.io - icon: https://www.minio.io/logo/img/logo-dark.svg - keywords: - - storage - - object-storage - - S3 - maintainers: - - email: hello@acale.ph - name: Acaleph - - email: hello@minio.io - name: Minio - name: minio - sources: - - https://github.com/minio/minio - urls: - - https://kubernetes-charts.storage.googleapis.com/minio-0.1.0.tgz - version: 0.1.0 - - apiVersion: v1 - created: 2017-03-16T23:48:28.876000842Z - description: Distributed object storage server built for cloud applications and - devops. - digest: d19b721fcb5f0566cce4a259e296b957ba982e87343947e1cbdbe979c770378d - home: https://minio.io - icon: https://www.minio.io/logo/img/logo-dark.svg - keywords: - - storage - - object-storage - - S3 - maintainers: - - email: hello@acale.ph - name: Acaleph - - email: hello@minio.io - name: Minio - name: minio - sources: - - https://github.com/minio/minio - urls: - - https://kubernetes-charts.storage.googleapis.com/minio-0.0.4.tgz - version: 0.0.4 - - apiVersion: v1 - created: 2017-02-13T04:33:52.302413618Z - description: A Minio Helm chart for Kubernetes - digest: cd58148df0776329fe5f518c542759565cab29dbefbc43f6b0ffdac86c9b31a9 - home: https://minio.io - keywords: - - storage - - object-storage - - S3 - maintainers: - - email: hello@acale.ph - name: Acaleph - - email: hello@minio.io - name: Minio - name: minio - sources: - - https://github.com/minio/minio - urls: - - https://kubernetes-charts.storage.googleapis.com/minio-0.0.3.tgz - version: 0.0.3 - - apiVersion: v1 - created: 2017-02-03T20:18:29.15659791Z - description: A Minio Helm chart for Kubernetes - digest: 699003bf2ef4cbb570580887da980412c1b9d6d173636de5def6053c7ba29a2a - home: https://minio.io - keywords: - - storage - - object-storage - - S3 - maintainers: - - email: hello@acale.ph - name: Acaleph - - email: hello@minio.io - name: Minio - name: minio - sources: - - https://github.com/minio/minio - urls: - - https://kubernetes-charts.storage.googleapis.com/minio-0.0.2.tgz - version: 0.0.2 - - apiVersion: v1 - created: 2017-01-12T02:36:05.500400572Z - description: A Minio Helm chart for Kubernetes - digest: aed17de3622988f8366126e158c740535c8d3bc55a0a85a3dcfabf07ac1395e9 - home: https://minio.io - keywords: - - storage - - object-storage - - S3 - maintainers: - - email: hello@acale.ph - name: Acaleph - name: minio - sources: - - https://github.com/minio/minio - urls: - - https://kubernetes-charts.storage.googleapis.com/minio-0.0.1.tgz - version: 0.0.1 - mongodb: - - created: 2017-04-28T00:18:30.091865909Z - description: NoSQL document-oriented database that stores JSON-like documents - with dynamic schemas, simplifying the integration of data in content-driven - applications. - digest: 979d36b208be9b266c70860d4fe1f9e5130d9d60b3bcbd893132452648dfe27f - engine: gotpl - home: https://mongodb.org - icon: https://bitnami.com/assets/stacks/mongodb/img/mongodb-stack-220x234.png - keywords: - - mongodb - - database - - nosql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mongodb - sources: - - https://github.com/bitnami/bitnami-docker-mongodb - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-0.4.9.tgz - version: 0.4.9 - - created: 2017-04-06T10:33:26.324740815Z - description: NoSQL document-oriented database that stores JSON-like documents - with dynamic schemas, simplifying the integration of data in content-driven - applications. - digest: cdc9fc28ff9139fcc6be015177e481f9d3765d6af284dce1999fc334cd7ef3a4 - engine: gotpl - home: https://mongodb.org - icon: https://bitnami.com/assets/stacks/mongodb/img/mongodb-stack-220x234.png - keywords: - - mongodb - - database - - nosql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mongodb - sources: - - https://github.com/bitnami/bitnami-docker-mongodb - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-0.4.8.tgz - version: 0.4.8 - - created: 2017-03-08T19:03:31.731176002Z - description: NoSQL document-oriented database that stores JSON-like documents - with dynamic schemas, simplifying the integration of data in content-driven - applications. - digest: 7d334e12acf9327f58f9a890e884a61ad760da2b1081d4a79b5680bee055cdbd - engine: gotpl - home: https://mongodb.org - icon: https://bitnami.com/assets/stacks/mongodb/img/mongodb-stack-220x234.png - keywords: - - mongodb - - database - - nosql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mongodb - sources: - - https://github.com/bitnami/bitnami-docker-mongodb - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-0.4.7.tgz - version: 0.4.7 - - created: 2017-02-14T03:48:27.566728756Z - description: NoSQL document-oriented database that stores JSON-like documents - with dynamic schemas, simplifying the integration of data in content-driven - applications. - digest: 27f9071cb81e9d3745776861f453db80b6ab6bf4507809f2e5c59e8a34d44939 - engine: gotpl - home: https://mongodb.org - icon: https://bitnami.com/assets/stacks/mongodb/img/mongodb-stack-220x234.png - keywords: - - mongodb - - database - - nosql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mongodb - sources: - - https://github.com/bitnami/bitnami-docker-mongodb - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-0.4.6.tgz - version: 0.4.6 - - created: 2017-02-10T23:18:26.017406698Z - description: Chart for MongoDB - digest: 27a78b0c6300f4567975af18a3ca145940a716a53de42ed89c75872788d2848b - engine: gotpl - home: https://mongodb.org - keywords: - - mongodb - - database - - nosql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mongodb - sources: - - https://github.com/bitnami/bitnami-docker-mongodb - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-0.4.5.tgz - version: 0.4.5 - - created: 2017-01-30T23:33:28.438574863Z - description: Chart for MongoDB - digest: 9bcc0e2aa1d7d8f8c2ce43ef9284af39e5794214d185577ed1baff07c1a019f4 - engine: gotpl - home: https://mongodb.org - keywords: - - mongodb - - database - - nosql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mongodb - sources: - - https://github.com/bitnami/bitnami-docker-mongodb - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-0.4.4.tgz - version: 0.4.4 - - created: 2017-01-13T20:48:31.52900213Z - description: Chart for MongoDB - digest: be548ec183b8c44f031504864b85b7a0d48d745fa450bf733f89646c9cbbda44 - engine: gotpl - home: https://mongodb.org - keywords: - - mongodb - - database - - nosql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mongodb - sources: - - https://github.com/bitnami/bitnami-docker-mongodb - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-0.4.3.tgz - version: 0.4.3 - - created: 2017-01-03T17:48:20.740456601Z - description: Chart for MongoDB - digest: 027dd50ff545309506daa0636a62d633071379040f8be080a403cb6d67399b0d - engine: gotpl - home: https://mongodb.org - keywords: - - mongodb - - database - - nosql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mongodb - sources: - - https://github.com/bitnami/bitnami-docker-mongodb - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-0.4.2.tgz - version: 0.4.2 - - created: 2016-12-15T00:48:24.012807516Z - description: Chart for MongoDB - digest: 42e8e56c715ea3bd2b8f9c188f5a9aec48a87654fb5215c35728ddf6c33c7437 - engine: gotpl - home: https://mongodb.org - keywords: - - mongodb - - database - - nosql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mongodb - sources: - - https://github.com/bitnami/bitnami-docker-mongodb - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-0.4.1.tgz - version: 0.4.1 - - created: 2016-12-09T18:48:20.187403991Z - description: Chart for MongoDB - digest: d5eabbe99b03b4f7f71c461580564e3d965c2602bfd1be4dd09f1c54c8e7e9db - engine: gotpl - home: https://mongodb.org - keywords: - - mongodb - - database - - nosql - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: mongodb - sources: - - https://github.com/bitnami/bitnami-docker-mongodb - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-0.4.0.tgz - version: 0.4.0 - mongodb-replicaset: - - created: 2017-04-28T00:18:30.092344824Z - description: NoSQL document-oriented database that stores JSON-like documents - with dynamic schemas, simplifying the integration of data in content-driven - applications. - digest: 98592fbb471eb98d3c59deb83ed9bd2a808e8df972b21ff183d04fa4659e9a39 - home: https://github.com/mongodb/mongo - icon: https://webassets.mongodb.com/_com_assets/cms/mongodb-logo-rgb-j6w271g1xn.jpg - maintainers: - - email: ramanathana@google.com - name: Anirudh Ramanathan - name: mongodb-replicaset - sources: - - https://github.com/mongodb/mongo - urls: - - https://kubernetes-charts.storage.googleapis.com/mongodb-replicaset-1.0.0.tgz - version: 1.0.0 - moodle: - - created: 2017-04-28T00:18:30.093014917Z - description: Moodle is a learning platform designed to provide educators, administrators - and learners with a single robust, secure and integrated system to create personalised - learning environments - digest: 386bff8ce61cf61961daf8ed6d68a76cd3a360560a08c1fca80bcbd897f11270 - engine: gotpl - home: http://www.moodle.org/ - icon: https://bitnami.com/assets/stacks/moodle/img/moodle-stack-110x117.png - keywords: - - moodle - - learning - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: moodle - sources: - - https://github.com/bitnami/bitnami-docker-moodle - urls: - - https://kubernetes-charts.storage.googleapis.com/moodle-0.1.4.tgz - version: 0.1.4 - - created: 2017-03-31T19:33:30.433327839Z - description: Moodle is a learning platform designed to provide educators, administrators - and learners with a single robust, secure and integrated system to create personalised - learning environments - digest: bd85420a7cefd82e9d96088591601f832ecc60016d6389dbcde51a2050327a66 - engine: gotpl - home: http://www.moodle.org/ - icon: https://bitnami.com/assets/stacks/moodle/img/moodle-stack-110x117.png - keywords: - - moodle - - learning - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: moodle - sources: - - https://github.com/bitnami/bitnami-docker-moodle - urls: - - https://kubernetes-charts.storage.googleapis.com/moodle-0.1.3.tgz - version: 0.1.3 - - created: 2017-03-26T18:03:33.791615833Z - description: Moodle is a learning platform designed to provide educators, administrators - and learners with a single robust, secure and integrated system to create personalised - learning environments - digest: 8656c544a71fa8cc4ac23380e999e072740ec8e481a22aff86517d8362e70121 - engine: gotpl - home: http://www.moodle.org/ - icon: https://bitnami.com/assets/stacks/moodle/img/moodle-stack-110x117.png - keywords: - - moodle - - learning - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: moodle - sources: - - https://github.com/bitnami/bitnami-docker-moodle - urls: - - https://kubernetes-charts.storage.googleapis.com/moodle-0.1.2.tgz - version: 0.1.2 - mysql: - - created: 2017-04-28T00:18:30.093353908Z - description: Fast, reliable, scalable, and easy to use open-source relational - database system. - digest: 2de7736158326da89faed2e5af952b4c7288f61bd96377ef345a99b4b271a3e7 - engine: gotpl - home: https://www.mysql.com/ - icon: https://www.mysql.com/common/logos/logo-mysql-170x115.png - keywords: - - mysql - - database - - sql - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: mysql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/mysql-0.2.6.tgz - version: 0.2.6 - - created: 2017-04-13T05:18:28.91694371Z - description: Fast, reliable, scalable, and easy to use open-source relational - database system. - digest: 66b9a6787a36c72095c24499a45f2d3bfc700e31420a8926d09c029604ac4c98 - engine: gotpl - home: https://www.mysql.com/ - icon: https://www.mysql.com/common/logos/logo-mysql-170x115.png - keywords: - - mysql - - database - - sql - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: mysql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/mysql-0.2.5.tgz - version: 0.2.5 - - created: 2017-02-13T04:18:31.541320579Z - description: Chart for MySQL - digest: 3653a2d111ca60287ffbf10cbbb3c268dcb8666422bf5518d37adb9b2f131f7c - engine: gotpl - home: https://www.mysql.com/ - keywords: - - mysql - - database - - sql - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: mysql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/mysql-0.2.4.tgz - version: 0.2.4 - - created: 2017-01-27T22:33:30.41494884Z - description: Chart for MySQL - digest: 1244814f3490d23172a923e52339ad8b126f3037483db462591405863884e7ce - engine: gotpl - home: https://www.mysql.com/ - keywords: - - mysql - - database - - sql - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: mysql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/mysql-0.2.3.tgz - version: 0.2.3 - - created: 2016-12-21T19:33:19.335178436Z - description: Chart for MySQL - digest: 5dfdd6301aa5c7424c5ecc649ac3038ea72afd0e25d4c350c79e8ba84fe15faf - engine: gotpl - home: https://www.mysql.com/ - keywords: - - mysql - - database - - sql - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: mysql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/mysql-0.2.2.tgz - version: 0.2.2 - - created: 2016-12-09T18:48:20.187750412Z - description: Chart for MySQL - digest: 3dd20c2ed2faf64b9f940e813b18d7fa1c18c1b86101de453c7c836940e05680 - engine: gotpl - home: https://www.mysql.com/ - keywords: - - mysql - - database - - sql - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: mysql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/mysql-0.2.1.tgz - version: 0.2.1 - - created: 2016-11-04T14:18:20.013918541Z - description: Chart for MySQL - digest: 9723417e4d71713ed87b701eff52a1a74abea64c3cf852833b1e9bb96ff6fd55 - engine: gotpl - home: https://www.mysql.com/ - keywords: - - mysql - - database - - sql - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: mysql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/mysql-0.2.0.tgz - version: 0.2.0 - - created: 2016-11-03T03:18:19.715713217Z - description: Chart for MySQL - digest: fef93423760265f8bb3aedf9a922ed0169e018071ea467f22c17527006ae6a60 - engine: gotpl - home: https://www.mysql.com/ - keywords: - - mysql - - database - - sql - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: mysql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/mysql-0.1.2.tgz - version: 0.1.2 - - created: 2016-10-19T18:33:14.866729383Z - description: Chart for MySQL - digest: 3cb4495336e12d4fea28a1f7e3f02bbb18249582227866088cf6ac89587a2527 - engine: gotpl - home: https://www.mysql.com/ - keywords: - - mysql - - database - - sql - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: mysql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/mysql-0.1.1.tgz - version: 0.1.1 - - created: 2016-10-19T00:03:14.031801762Z - description: Chart for MySQL - digest: cba3eff1710520dbfbbeca2b0f9a754e0ddc172eb83ce51211606387955a6572 - engine: gotpl - home: https://www.mysql.com/ - keywords: - - mysql - - database - - sql - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: mysql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/mysql-0.1.0.tgz - version: 0.1.0 - namerd: - - apiVersion: v1 - created: 2017-04-28T00:18:30.093626863Z - description: Service that manages routing for multiple linkerd instances - digest: 52164352b00a196135a608198d12748d063965cfde016dc8a4fdc9021587bbeb - home: https://linkerd.io/in-depth/namerd/ - icon: https://pbs.twimg.com/profile_images/690258997237014528/KNgQd9GL_400x400.png - maintainers: - - email: knoxville@gmail.com - name: Sean Knox - name: namerd - sources: - - https://github.com/linkerd/linkerd - urls: - - https://kubernetes-charts.storage.googleapis.com/namerd-0.1.0.tgz - version: 0.1.0 - nginx-ingress: - - created: 2017-04-28T00:18:30.094151336Z - description: An nginx Ingress controller that uses ConfigMap to store the nginx - configuration. - digest: 2d6183d368b3993b4fdfd7108531067b928083b3db6dc30eab3f9c3a8212df9b - engine: gotpl - icon: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Nginx_logo.svg/500px-Nginx_logo.svg.png - keywords: - - ingress - - nginx - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - - email: mgoodness@gmail.com - name: Michael Goodness - - email: chance.zibolski@coreos.com - name: Chance Zibolski - name: nginx-ingress - sources: - - https://github.com/kubernetes/contrib/tree/master/ingress/controllers/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-ingress-0.3.2.tgz - version: 0.3.2 - - created: 2017-04-03T22:33:26.678796256Z - description: An nginx Ingress controller that uses ConfigMap to store the nginx - configuration. - digest: 4e4632273eb5db95e525a8899b9f6f1697db241c2ff1ccb7456e0fc916bb75c2 - engine: gotpl - icon: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Nginx_logo.svg/500px-Nginx_logo.svg.png - keywords: - - ingress - - nginx - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - - email: mgoodness@gmail.com - name: Michael Goodness - - email: chance.zibolski@coreos.com - name: Chance Zibolski - name: nginx-ingress - sources: - - https://github.com/kubernetes/contrib/tree/master/ingress/controllers/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-ingress-0.3.1.tgz - version: 0.3.1 - - created: 2017-03-10T17:03:37.208481141Z - description: An nginx Ingress controller that uses ConfigMap to store the nginx - configuration. - digest: 731823c88a849f945f405c192d92daf27afad37af76befb1eb92251240de29d7 - engine: gotpl - icon: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Nginx_logo.svg/500px-Nginx_logo.svg.png - keywords: - - ingress - - nginx - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - - email: mgoodness@gmail.com - name: Michael Goodness - name: nginx-ingress - sources: - - https://github.com/kubernetes/contrib/tree/master/ingress/controllers/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-ingress-0.3.0.tgz - version: 0.3.0 - nginx-lego: - - created: 2017-04-28T00:18:30.094506147Z - description: Chart for nginx-ingress-controller and kube-lego - digest: da173cc1a9313ea0b11f5fb7aa67a20a2ac797b2f129a079c06284e8a9765f21 - engine: gotpl - keywords: - - kube-lego - - nginx-ingress-controller - - nginx - - letsencrypt - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - name: nginx-lego - sources: - - https://github.com/kubernetes/contrib/tree/master/ingress/controllers/nginx - - https://github.com/jetstack/kube-lego/tree/master/examples/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-lego-0.2.1.tgz - version: 0.2.1 - - created: 2017-03-02T18:48:30.43738595Z - description: Chart for nginx-ingress-controller and kube-lego - digest: 9a8ea81371900d2c7931b0143f991ef0fde41038d26a968628008aef14ec08ef - engine: gotpl - keywords: - - kube-lego - - nginx-ingress-controller - - nginx - - letsencrypt - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - name: nginx-lego - sources: - - https://github.com/kubernetes/contrib/tree/master/ingress/controllers/nginx - - https://github.com/jetstack/kube-lego/tree/master/examples/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-lego-0.2.0.tgz - version: 0.2.0 - - created: 2017-01-12T02:52:37.835917781Z - description: Chart for nginx-ingress-controller and kube-lego - digest: 2dee942e546940beb8301a94a1a51e22c7a38b4c5592701528149dfaa83e1bb6 - engine: gotpl - keywords: - - kube-lego - - nginx-ingress-controller - - nginx - - letsencrypt - maintainers: - - email: jack.zampolin@gmail.com - name: Jack Zampolin - name: nginx-lego - sources: - - https://github.com/kubernetes/contrib/tree/master/ingress/controllers/nginx - - https://github.com/jetstack/kube-lego/tree/master/examples/nginx - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-lego-0.1.0.tgz - version: 0.1.0 - odoo: - - created: 2017-04-28T00:18:30.095144247Z - description: A suite of web based open source business apps. - digest: 439b9160c3226a0cf6b848a5ac921307e5100ffa36e03e3697619a6c968eec3b - engine: gotpl - home: https://www.odoo.com/ - icon: https://bitnami.com/assets/stacks/odoo/img/odoo-stack-110x117.png - keywords: - - odoo - - crm - - www - - http - - web - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: odoo - sources: - - https://github.com/bitnami/bitnami-docker-odoo - urls: - - https://kubernetes-charts.storage.googleapis.com/odoo-0.5.0.tgz - version: 0.5.0 - - created: 2017-04-20T11:48:30.777572861Z - description: A suite of web based open source business apps. - digest: 4aeba5cf600c9552d3a0ace00b4880aa27d8b2c2aec1b7dc9ebad04f2152d165 - engine: gotpl - home: https://www.odoo.com/ - icon: https://bitnami.com/assets/stacks/odoo/img/odoo-stack-110x117.png - keywords: - - odoo - - crm - - www - - http - - web - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: odoo - sources: - - https://github.com/bitnami/bitnami-docker-odoo - urls: - - https://kubernetes-charts.storage.googleapis.com/odoo-0.4.5.tgz - version: 0.4.5 - - created: 2017-04-19T19:48:30.479799003Z - description: A suite of web based open source business apps. - digest: 74dff526f84c50445e0cc0fb602679c68941e9b93d9410ce66a6f3713a71f39d - engine: gotpl - home: https://www.odoo.com/ - icon: https://bitnami.com/assets/stacks/odoo/img/odoo-stack-110x117.png - keywords: - - odoo - - crm - - www - - http - - web - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: odoo - sources: - - https://github.com/bitnami/bitnami-docker-odoo - urls: - - https://kubernetes-charts.storage.googleapis.com/odoo-0.4.4.tgz - version: 0.4.4 - - created: 2017-03-23T21:18:31.859026332Z - description: A suite of web based open source business apps. - digest: 6bdd86f83c41f079218105162f1e86cf572d005174c0688a2201c59d20cf1e55 - engine: gotpl - home: https://www.odoo.com/ - icon: https://bitnami.com/assets/stacks/odoo/img/odoo-stack-110x117.png - keywords: - - odoo - - crm - - www - - http - - web - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: odoo - sources: - - https://github.com/bitnami/bitnami-docker-odoo - urls: - - https://kubernetes-charts.storage.googleapis.com/odoo-0.4.3.tgz - version: 0.4.3 - - created: 2017-03-23T00:18:30.533565303Z - description: A suite of web based open source business apps. - digest: 9a7723d94d17ff18347074d5a1d6b42530466a8a4882981178f4856138f44072 - engine: gotpl - home: https://www.odoo.com/ - icon: https://bitnami.com/assets/stacks/odoo/img/odoo-stack-110x117.png - keywords: - - odoo - - crm - - www - - http - - web - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: odoo - sources: - - https://github.com/bitnami/bitnami-docker-odoo - urls: - - https://kubernetes-charts.storage.googleapis.com/odoo-0.4.2.tgz - version: 0.4.2 - - created: 2017-03-17T22:48:29.256461745Z - description: A suite of web based open source business apps. - digest: 1339b6efbc6b4df849c460ddbcade9f81ede3a6a800e9566f54acc1c52dccb4b - engine: gotpl - home: https://www.odoo.com/ - icon: https://bitnami.com/assets/stacks/odoo/img/odoo-stack-110x117.png - keywords: - - odoo - - crm - - www - - http - - web - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: odoo - sources: - - https://github.com/bitnami/bitnami-docker-odoo - urls: - - https://kubernetes-charts.storage.googleapis.com/odoo-0.4.1.tgz - version: 0.4.1 - - created: 2017-03-08T19:03:31.734764598Z - description: A suite of web based open source business apps. - digest: 15b1d5339086427990fb3d0ee8f65e768b398195f2db63b0356e6c79f4d4b981 - engine: gotpl - home: https://www.odoo.com/ - icon: https://bitnami.com/assets/stacks/odoo/img/odoo-stack-110x117.png - keywords: - - odoo - - crm - - www - - http - - web - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: odoo - sources: - - https://github.com/bitnami/bitnami-docker-odoo - urls: - - https://kubernetes-charts.storage.googleapis.com/odoo-0.4.0.tgz - version: 0.4.0 - opencart: - - created: 2017-04-28T00:18:30.095906073Z - description: A free and open source e-commerce platform for online merchants. - It provides a professional and reliable foundation for a successful online store. - digest: a22a7ee46a419b2bc4dd0ba66c3fa294e15f86722944a7bc3cc56cb24f3fa737 - engine: gotpl - home: https://opencart.com/ - icon: https://bitnami.com/assets/stacks/opencart/img/opencart-stack-110x117.png - keywords: - - opencart - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: opencart - sources: - - https://github.com/bitnami/bitnami-docker-opencart - urls: - - https://kubernetes-charts.storage.googleapis.com/opencart-0.4.5.tgz - version: 0.4.5 - - created: 2017-04-06T11:18:27.44256999Z - description: A free and open source e-commerce platform for online merchants. - It provides a professional and reliable foundation for a successful online store. - digest: 0abb5c30e0eef2a06b36b83b0c11f4bcf3df14ea416c5e49cb821c78e6783bce - engine: gotpl - home: https://opencart.com/ - icon: https://bitnami.com/assets/stacks/opencart/img/opencart-stack-110x117.png - keywords: - - opencart - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: opencart - sources: - - https://github.com/bitnami/bitnami-docker-opencart - urls: - - https://kubernetes-charts.storage.googleapis.com/opencart-0.4.4.tgz - version: 0.4.4 - - created: 2017-03-02T19:33:28.187180475Z - description: A free and open source e-commerce platform for online merchants. - It provides a professional and reliable foundation for a successful online store. - digest: 1a80dfcec98c190b8710d7db47c22c3882a05a046b3d767c84094cef983a1556 - engine: gotpl - home: https://opencart.com/ - icon: https://bitnami.com/assets/stacks/opencart/img/opencart-stack-110x117.png - keywords: - - opencart - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: opencart - sources: - - https://github.com/bitnami/bitnami-docker-opencart - urls: - - https://kubernetes-charts.storage.googleapis.com/opencart-0.4.3.tgz - version: 0.4.3 - - created: 2017-02-23T02:33:30.963491381Z - description: A free and open source e-commerce platform for online merchants. - It provides a professional and reliable foundation for a successful online store. - digest: d3092c4ed82db569937e435d3dc6bcddce420540bf340dd54a554a57b62c6aaa - engine: gotpl - home: https://opencart.com/ - icon: https://bitnami.com/assets/stacks/opencart/img/opencart-stack-110x117.png - keywords: - - opencart - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: opencart - sources: - - https://github.com/bitnami/bitnami-docker-opencart - urls: - - https://kubernetes-charts.storage.googleapis.com/opencart-0.4.2.tgz - version: 0.4.2 - - created: 2017-01-21T00:18:31.354317974Z - description: A free and open source e-commerce platform for online merchants. - It provides a professional and reliable foundation for a successful online store. - digest: afbaa2517e811990bc4b31495a4634b6399615493cf344215a5658de2f33575b - engine: gotpl - home: https://opencart.com/ - icon: https://bitnami.com/assets/stacks/opencart/img/opencart-stack-110x117.png - keywords: - - opencart - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: opencart - sources: - - https://github.com/bitnami/bitnami-docker-opencart - urls: - - https://kubernetes-charts.storage.googleapis.com/opencart-0.4.1.tgz - version: 0.4.1 - - created: 2017-01-04T00:48:19.745672724Z - description: A free and open source e-commerce platform for online merchants. - It provides a professional and reliable foundation for a successful online store. - digest: f0e46cf67f8594c6d92f02fad4a23fdf7aa94bdb145bfde39436e17f0a8930f5 - engine: gotpl - home: https://opencart.com/ - icon: https://bitnami.com/assets/stacks/opencart/img/opencart-stack-110x117.png - keywords: - - opencart - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: opencart - sources: - - https://github.com/bitnami/bitnami-docker-opencart - urls: - - https://kubernetes-charts.storage.googleapis.com/opencart-0.4.0.tgz - version: 0.4.0 - openvpn: - - apiVersion: v1 - created: 2017-04-28T00:18:30.096266269Z - description: A Helm chart to install an openvpn server inside a kubernetes cluster. Certificate - generation is also part of the deployment, and this chart will generate client - keys as needed. - digest: 403a80ed3f204442afe4236e275035bf39f9e1f6af8d48153d6698f51efeb16d - home: https://openvpn.net/index.php/open-source.html - icon: https://forums.openvpn.net/styles/openvpn/theme/images/ovpnlogo.png - keywords: - - openvpn - - vpn - - tunnel - - network - - service - - connectivity - - encryption - maintainers: - - email: john.felten@gmail.com - name: John Felten - name: openvpn - urls: - - https://kubernetes-charts.storage.googleapis.com/openvpn-1.0.2.tgz - version: 1.0.2 - - apiVersion: v1 - created: 2017-04-06T10:18:27.592699587Z - description: A Helm chart to install an openvpn server inside a kubernetes cluster. Certificate - generation is also part of the deployment, and this chart will generate client - keys as needed. - digest: c92674b7c6391e412864a3aa73af1a2d89f3b4e768bb459118b8e75b9750fc10 - home: https://openvpn.net/index.php/open-source.html - icon: https://forums.openvpn.net/styles/openvpn/theme/images/ovpnlogo.png - keywords: - - openvpn - - vpn - - tunnel - - network - - service - - connectivity - - encryption - maintainers: - - email: john.felten@gmail.com - name: John Felten - name: openvpn - urls: - - https://kubernetes-charts.storage.googleapis.com/openvpn-1.0.1.tgz - version: 1.0.1 - - apiVersion: v1 - created: 2017-01-27T21:48:32.078827588Z - description: A Helm chart to install an openvpn server inside a kubernetes cluster. Certificate - generation is also part of the deployment, and this chart will generate client - keys as needed. - digest: a12cfddce900c8a4ef6cea0cef5426d5fb23c657cdab433f9307f6d048273f6b - home: https://openvpn.net/index.php/open-source.html - icon: https://forums.openvpn.net/styles/openvpn/theme/images/ovpnlogo.png - keywords: - - openvpn - - vpn - - tunnel - - network - - service - - connectivity - - encryption - maintainers: - - email: john.felten@gmail.com - name: John Felten - name: openvpn - urls: - - https://kubernetes-charts.storage.googleapis.com/openvpn-1.0.0.tgz - version: 1.0.0 - orangehrm: - - created: 2017-04-28T00:18:30.097427186Z - description: OrangeHRM is a free HR management system that offers a wealth of - modules to suit the needs of your business. - digest: d796649de3610a41a9f92b0ee3f5ce327a250d1b86d7a4c76fd46a31144c59e5 - engine: gotpl - home: https://www.orangehrm.com - icon: https://bitnami.com/assets/stacks/orangehrm/img/orangehrm-stack-110x117.png - keywords: - - orangehrm - - http - - https - - web - - application - - php - - human resources - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: orangehrm - sources: - - https://github.com/bitnami/bitnami-docker-orangehrm - urls: - - https://kubernetes-charts.storage.googleapis.com/orangehrm-0.4.5.tgz - version: 0.4.5 - - created: 2017-04-12T21:03:28.668395677Z - description: OrangeHRM is a free HR management system that offers a wealth of - modules to suit the needs of your business. - digest: 9c954806dfd415ee07cbcc6ee5e82eeeebeb765ecfe173ba80c0003b1d0be504 - engine: gotpl - home: https://www.orangehrm.com - icon: https://bitnami.com/assets/stacks/orangehrm/img/orangehrm-stack-110x117.png - keywords: - - orangehrm - - http - - https - - web - - application - - php - - human resources - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: orangehrm - sources: - - https://github.com/bitnami/bitnami-docker-orangehrm - urls: - - https://kubernetes-charts.storage.googleapis.com/orangehrm-0.4.4.tgz - version: 0.4.4 - - created: 2017-03-18T18:33:36.171180792Z - description: OrangeHRM is a free HR management system that offers a wealth of - modules to suit the needs of your business. - digest: 87483fe0c39f63c61549a85103b9423b30ac4bfe2b19a7af083eaf7dd3491ce2 - engine: gotpl - home: https://www.orangehrm.com - icon: https://bitnami.com/assets/stacks/orangehrm/img/orangehrm-stack-110x117.png - keywords: - - orangehrm - - http - - https - - web - - application - - php - - human resources - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: orangehrm - sources: - - https://github.com/bitnami/bitnami-docker-orangehrm - urls: - - https://kubernetes-charts.storage.googleapis.com/orangehrm-0.4.3.tgz - version: 0.4.3 - - created: 2017-03-02T19:33:28.188778055Z - description: OrangeHRM is a free HR management system that offers a wealth of - modules to suit the needs of your business. - digest: d03e9f261dd50212f6c2c0ecc3a84cbabda89493d8bfc28ad95c6c9a6a195af9 - engine: gotpl - home: https://www.orangehrm.com - icon: https://bitnami.com/assets/stacks/orangehrm/img/orangehrm-stack-110x117.png - keywords: - - orangehrm - - http - - https - - web - - application - - php - - human resources - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: orangehrm - sources: - - https://github.com/bitnami/bitnami-docker-orangehrm - urls: - - https://kubernetes-charts.storage.googleapis.com/orangehrm-0.4.2.tgz - version: 0.4.2 - - created: 2017-02-23T02:33:30.964778113Z - description: OrangeHRM is a free HR management system that offers a wealth of - modules to suit the needs of your business. - digest: b5e5ed301b5321a99a7147173ec6fa7cb471879a585b9af0e8ac2741f0409e48 - engine: gotpl - home: https://www.orangehrm.com - icon: https://bitnami.com/assets/stacks/orangehrm/img/orangehrm-stack-110x117.png - keywords: - - orangehrm - - http - - https - - web - - application - - php - - human resources - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: orangehrm - sources: - - https://github.com/bitnami/bitnami-docker-orangehrm - urls: - - https://kubernetes-charts.storage.googleapis.com/orangehrm-0.4.1.tgz - version: 0.4.1 - - created: 2017-02-01T00:48:29.581953712Z - description: OrangeHRM is a free HR management system that offers a wealth of - modules to suit the needs of your business. - digest: 4ca6ccafc5bc408cb9e61139b8914468e0d8eeed7bffb62eea51c6e40697e8dd - engine: gotpl - home: https://www.orangehrm.com - icon: https://bitnami.com/assets/stacks/orangehrm/img/orangehrm-stack-110x117.png - keywords: - - orangehrm - - http - - https - - web - - application - - php - - human resources - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: orangehrm - sources: - - https://github.com/bitnami/bitnami-docker-orangehrm - urls: - - https://kubernetes-charts.storage.googleapis.com/orangehrm-0.4.0.tgz - version: 0.4.0 - osclass: - - created: 2017-04-28T00:18:30.098620601Z - description: Osclass is a php script that allows you to quickly create and manage - your own free classifieds site. - digest: 23472bb37c094dad3122a96f6fd6a3967f1d41547121ecf2622b557f2fc5da8b - engine: gotpl - home: https://osclass.org/ - icon: https://bitnami.com/assets/stacks/osclass/img/osclass-stack-110x117.png - keywords: - - osclass - - classifieds - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: osclass - sources: - - https://github.com/bitnami/bitnami-docker-osclass - urls: - - https://kubernetes-charts.storage.googleapis.com/osclass-0.4.1.tgz - version: 0.4.1 - - created: 2017-02-23T02:33:30.965596248Z - description: Osclass is a php script that allows you to quickly create and manage - your own free classifieds site. - digest: 7c21cef0b281e3da5b3c9b60940c1b010c34fe866c95615b7d0afd10c480178c - engine: gotpl - home: https://osclass.org/ - icon: https://bitnami.com/assets/stacks/osclass/img/osclass-stack-110x117.png - keywords: - - osclass - - classifieds - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: osclass - sources: - - https://github.com/bitnami/bitnami-docker-osclass - urls: - - https://kubernetes-charts.storage.googleapis.com/osclass-0.4.0.tgz - version: 0.4.0 - owncloud: - - created: 2017-04-28T00:18:30.099329236Z - description: A file sharing server that puts the control and security of your - own data back into your hands. - digest: 9beee22ffa4b4eae5e6f8da5734e08a9455a063299b33d4350dea91050ae9a25 - engine: gotpl - home: https://owncloud.org/ - icon: https://bitnami.com/assets/stacks/owncloud/img/owncloud-stack-220x234.png - keywords: - - owncloud - - storage - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: owncloud - sources: - - https://github.com/bitnami/bitnami-docker-owncloud - urls: - - https://kubernetes-charts.storage.googleapis.com/owncloud-0.4.7.tgz - version: 0.4.7 - - created: 2017-04-20T16:18:33.358391215Z - description: A file sharing server that puts the control and security of your - own data back into your hands. - digest: 97b3574369c1d690ce8787588668070451c559ac06b6b69942e9905cb6ec94ad - engine: gotpl - home: https://owncloud.org/ - icon: https://bitnami.com/assets/stacks/owncloud/img/owncloud-stack-220x234.png - keywords: - - owncloud - - storage - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: owncloud - sources: - - https://github.com/bitnami/bitnami-docker-owncloud - urls: - - https://kubernetes-charts.storage.googleapis.com/owncloud-0.4.6.tgz - version: 0.4.6 - - created: 2017-03-23T21:18:31.86258413Z - description: A file sharing server that puts the control and security of your - own data back into your hands. - digest: fb78c2ea47996c6781ddfe2dca6aea87cbb8b895e1fc5fea404aa28735d4a2ec - engine: gotpl - home: https://owncloud.org/ - icon: https://bitnami.com/assets/stacks/owncloud/img/owncloud-stack-220x234.png - keywords: - - owncloud - - storage - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: owncloud - sources: - - https://github.com/bitnami/bitnami-docker-owncloud - urls: - - https://kubernetes-charts.storage.googleapis.com/owncloud-0.4.5.tgz - version: 0.4.5 - - created: 2017-03-08T19:03:31.738784117Z - description: A file sharing server that puts the control and security of your - own data back into your hands. - digest: 07e18bccf9dc0a8fcff42e97c422520530c2c5057c2a9c14767842609bcaa7ac - engine: gotpl - home: https://owncloud.org/ - icon: https://bitnami.com/assets/stacks/owncloud/img/owncloud-stack-220x234.png - keywords: - - owncloud - - storage - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: owncloud - sources: - - https://github.com/bitnami/bitnami-docker-owncloud - urls: - - https://kubernetes-charts.storage.googleapis.com/owncloud-0.4.4.tgz - version: 0.4.4 - - created: 2017-02-11T03:18:26.536480213Z - description: A file sharing server that puts the control and security of your - own data back into your hands. - digest: 4e534bdc4be184c59fbcbcff4641f2bfa341cd7f4ed3668912cd47c1336f5dea - engine: gotpl - home: https://owncloud.org/ - keywords: - - owncloud - - storage - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: owncloud - sources: - - https://github.com/bitnami/bitnami-docker-owncloud - urls: - - https://kubernetes-charts.storage.googleapis.com/owncloud-0.4.3.tgz - version: 0.4.3 - - created: 2017-01-30T23:48:29.172608299Z - description: A file sharing server that puts the control and security of your - own data back into your hands. - digest: b41eb1fc1eea6311f3ad15b459f99b77e96b944b81ea50ebccc941191d496141 - engine: gotpl - home: https://owncloud.org/ - keywords: - - owncloud - - storage - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: owncloud - sources: - - https://github.com/bitnami/bitnami-docker-owncloud - urls: - - https://kubernetes-charts.storage.googleapis.com/owncloud-0.4.2.tgz - version: 0.4.2 - - created: 2017-01-21T00:18:31.35624854Z - description: A file sharing server that puts the control and security of your - own data back into your hands. - digest: 89901fc256d6a4f878d77b51bf23eb77358dedb040052b34a09cbe9ca607487f - engine: gotpl - home: https://owncloud.org/ - keywords: - - owncloud - - storage - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: owncloud - sources: - - https://github.com/bitnami/bitnami-docker-owncloud - urls: - - https://kubernetes-charts.storage.googleapis.com/owncloud-0.4.1.tgz - version: 0.4.1 - - created: 2016-12-21T18:33:20.278724682Z - description: A file sharing server that puts the control and security of your - own data back into your hands. - digest: 7ec2dfe27bf42e8ee2e0672964decc2fbec94ec4fb60af2f3e9e91e13fdae08a - engine: gotpl - home: https://owncloud.org/ - keywords: - - owncloud - - storage - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: owncloud - sources: - - https://github.com/bitnami/bitnami-docker-owncloud - urls: - - https://kubernetes-charts.storage.googleapis.com/owncloud-0.4.0.tgz - version: 0.4.0 - percona: - - created: 2017-04-28T00:18:30.099763589Z - description: free, fully compatible, enhanced, open source drop-in replacement - for MySQL - digest: b10837a3de2492c2826aaf9ab740314ffa3f8c6110568b0a88e142791abb0e96 - engine: gotpl - home: https://www.percona.com/ - icon: https://www.percona.com/sites/all/themes/percona2015/logo.png - keywords: - - mysql - - percona - - database - - sql - maintainers: - - email: patg@patg.net - name: Patrick Galbraith - name: percona - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/percona - urls: - - https://kubernetes-charts.storage.googleapis.com/percona-0.1.0.tgz - version: 0.1.0 - phabricator: - - created: 2017-04-28T00:18:30.100508372Z - description: Collection of open source web applications that help software companies - build better software. - digest: 6c53fef9ac9c246983a26d2e8b9e8f032b87706c09667edb9f9bb62dfa822e88 - engine: gotpl - home: https://www.phacility.com/phabricator/ - icon: https://bitnami.com/assets/stacks/phabricator/img/phabricator-stack-110x117.png - keywords: - - phabricator - - http - - https - - web - - application - - collaboration - - project management - - bug tracking - - code review - - wiki - - git - - mercurial - - subversion - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phabricator - sources: - - https://github.com/bitnami/bitnami-docker-phabricator - urls: - - https://kubernetes-charts.storage.googleapis.com/phabricator-0.4.5.tgz - version: 0.4.5 - - created: 2017-04-06T10:33:26.333625406Z - description: Collection of open source web applications that help software companies - build better software. - digest: ef626563185a4b41ff7da03a9bd5548903061c6140d2d65300e4b1c1d39a2f28 - engine: gotpl - home: https://www.phacility.com/phabricator/ - icon: https://bitnami.com/assets/stacks/phabricator/img/phabricator-stack-110x117.png - keywords: - - phabricator - - http - - https - - web - - application - - collaboration - - project management - - bug tracking - - code review - - wiki - - git - - mercurial - - subversion - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phabricator - sources: - - https://github.com/bitnami/bitnami-docker-phabricator - urls: - - https://kubernetes-charts.storage.googleapis.com/phabricator-0.4.4.tgz - version: 0.4.4 - - created: 2017-03-02T19:33:28.191377992Z - description: Collection of open source web applications that help software companies - build better software. - digest: 544373e984918da349351f05fdb7a8cf0c695726890bcd4cc348e786e1c55d57 - engine: gotpl - home: https://www.phacility.com/phabricator/ - icon: https://bitnami.com/assets/stacks/phabricator/img/phabricator-stack-110x117.png - keywords: - - phabricator - - http - - https - - web - - application - - collaboration - - project management - - bug tracking - - code review - - wiki - - git - - mercurial - - subversion - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phabricator - sources: - - https://github.com/bitnami/bitnami-docker-phabricator - urls: - - https://kubernetes-charts.storage.googleapis.com/phabricator-0.4.3.tgz - version: 0.4.3 - - created: 2017-02-23T02:33:30.967143259Z - description: Collection of open source web applications that help software companies - build better software. - digest: 657b118031c1513b95268915ef4f68f5453f2b21ca5a919e9a370facb439dfe5 - engine: gotpl - home: https://www.phacility.com/phabricator/ - icon: https://bitnami.com/assets/stacks/phabricator/img/phabricator-stack-110x117.png - keywords: - - phabricator - - http - - https - - web - - application - - collaboration - - project management - - bug tracking - - code review - - wiki - - git - - mercurial - - subversion - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phabricator - sources: - - https://github.com/bitnami/bitnami-docker-phabricator - urls: - - https://kubernetes-charts.storage.googleapis.com/phabricator-0.4.2.tgz - version: 0.4.2 - - created: 2017-01-21T00:18:31.358403062Z - description: Collection of open source web applications that help software companies - build better software. - digest: 77603dd383c0674e829fa4aa2ca0d33ce61884df6a3ce68f9e165e7802e7eacc - engine: gotpl - home: https://www.phacility.com/phabricator/ - icon: https://bitnami.com/assets/stacks/phabricator/img/phabricator-stack-110x117.png - keywords: - - phabricator - - http - - https - - web - - application - - collaboration - - project management - - bug tracking - - code review - - wiki - - git - - mercurial - - subversion - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phabricator - sources: - - https://github.com/bitnami/bitnami-docker-phabricator - urls: - - https://kubernetes-charts.storage.googleapis.com/phabricator-0.4.1.tgz - version: 0.4.1 - - created: 2017-01-04T00:48:19.747640048Z - description: Collection of open source web applications that help software companies - build better software. - digest: e4f0f5322bac2a2bed822eb8065eec6e29716310af1623975f1ad0ef2b448c66 - engine: gotpl - home: https://www.phacility.com/phabricator/ - icon: https://bitnami.com/assets/stacks/phabricator/img/phabricator-stack-110x117.png - keywords: - - phabricator - - http - - https - - web - - application - - collaboration - - project management - - bug tracking - - code review - - wiki - - git - - mercurial - - subversion - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phabricator - sources: - - https://github.com/bitnami/bitnami-docker-phabricator - urls: - - https://kubernetes-charts.storage.googleapis.com/phabricator-0.4.0.tgz - version: 0.4.0 - phpbb: - - created: 2017-04-28T00:18:30.10119319Z - description: Community forum that supports the notion of users and groups, file - attachments, full-text search, notifications and more. - digest: d4e97de68d28acd69cb655c7fa44cb8829367ce3a7b20ecbf151f1bb2b10d1cb - engine: gotpl - home: https://www.phpbb.com/ - icon: https://bitnami.com/assets/stacks/phpbb/img/phpbb-stack-220x234.png - keywords: - - phpbb - - forum - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phpbb - sources: - - https://github.com/bitnami/bitnami-docker-phpbb - urls: - - https://kubernetes-charts.storage.googleapis.com/phpbb-0.4.7.tgz - version: 0.4.7 - - created: 2017-04-13T05:18:28.926003844Z - description: Community forum that supports the notion of users and groups, file - attachments, full-text search, notifications and more. - digest: 30732d5c200ef312a50f5a89a222a959c99b0a08af87f0c312c3699bb2c56082 - engine: gotpl - home: https://www.phpbb.com/ - icon: https://bitnami.com/assets/stacks/phpbb/img/phpbb-stack-220x234.png - keywords: - - phpbb - - forum - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phpbb - sources: - - https://github.com/bitnami/bitnami-docker-phpbb - urls: - - https://kubernetes-charts.storage.googleapis.com/phpbb-0.4.6.tgz - version: 0.4.6 - - created: 2017-03-02T19:33:28.19218227Z - description: Community forum that supports the notion of users and groups, file - attachments, full-text search, notifications and more. - digest: ed994a70f454f325cf677bdcc0a09aa66b01c7343a624a3304b88148eafa0dd4 - engine: gotpl - home: https://www.phpbb.com/ - icon: https://bitnami.com/assets/stacks/phpbb/img/phpbb-stack-220x234.png - keywords: - - phpbb - - forum - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phpbb - sources: - - https://github.com/bitnami/bitnami-docker-phpbb - urls: - - https://kubernetes-charts.storage.googleapis.com/phpbb-0.4.5.tgz - version: 0.4.5 - - created: 2017-02-23T02:33:30.967854461Z - description: Community forum that supports the notion of users and groups, file - attachments, full-text search, notifications and more. - digest: f50e4b1eb4bf8b2e6815b664e3cfff4bfd7979b650bf1efa709b79c92a8e5dd0 - engine: gotpl - home: https://www.phpbb.com/ - icon: https://bitnami.com/assets/stacks/phpbb/img/phpbb-stack-220x234.png - keywords: - - phpbb - - forum - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phpbb - sources: - - https://github.com/bitnami/bitnami-docker-phpbb - urls: - - https://kubernetes-charts.storage.googleapis.com/phpbb-0.4.4.tgz - version: 0.4.4 - - created: 2017-02-13T17:03:30.134235119Z - description: Community forum that supports the notion of users and groups, file - attachments, full-text search, notifications and more. - digest: c41705e34490d1941adda128b166ef1a327cceb7a447113d2633652f3c85363a - engine: gotpl - home: https://www.phpbb.com/ - icon: https://bitnami.com/assets/stacks/phpbb/img/phpbb-stack-220x234.png - keywords: - - phpbb - - forum - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phpbb - sources: - - https://github.com/bitnami/bitnami-docker-phpbb - urls: - - https://kubernetes-charts.storage.googleapis.com/phpbb-0.4.3.tgz - version: 0.4.3 - - created: 2017-02-13T04:18:31.547850917Z - description: Community forum that supports the notion of users and groups, file - attachments, full-text search, notifications and more. - digest: 77f6dfa8f277b003ba0b8e2ca6d046f6b6526ee7db05e304cbb9924c9fd7d194 - engine: gotpl - home: https://www.phpbb.com/ - keywords: - - phpbb - - forum - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phpbb - sources: - - https://github.com/bitnami/bitnami-docker-phpbb - urls: - - https://kubernetes-charts.storage.googleapis.com/phpbb-0.4.2.tgz - version: 0.4.2 - - created: 2017-01-31T00:03:26.18262303Z - description: Community forum that supports the notion of users and groups, file - attachments, full-text search, notifications and more. - digest: cd6dde525898d91f5f39541ee205e79ba5b8eb7a5b99585141cc88c3a63f5aed - engine: gotpl - home: https://www.phpbb.com/ - keywords: - - phpbb - - forum - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phpbb - sources: - - https://github.com/bitnami/bitnami-docker-phpbb - urls: - - https://kubernetes-charts.storage.googleapis.com/phpbb-0.4.1.tgz - version: 0.4.1 - - created: 2016-12-09T18:48:20.188802837Z - description: Community forum that supports the notion of users and groups, file - attachments, full-text search, notifications and more. - digest: 7e5e8828fdbba9ebc83c95baac4937e569e881d8b68bc79974a0f5d8a2883e13 - engine: gotpl - home: https://www.phpbb.com/ - keywords: - - phpbb - - forum - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: phpbb - sources: - - https://github.com/bitnami/bitnami-docker-phpbb - urls: - - https://kubernetes-charts.storage.googleapis.com/phpbb-0.4.0.tgz - version: 0.4.0 - postgresql: - - created: 2017-04-28T00:18:30.101575321Z - description: Object-relational database management system (ORDBMS) with an emphasis - on extensibility and on standards-compliance. - digest: 60224116f1e803bbf744cb2b3c2d621cfadef709ef0a8c6ed08cbed0f5415915 - engine: gotpl - home: https://www.postgresql.org/ - icon: https://www.postgresql.org/media/img/about/press/elephant.png - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - - name: databus23 - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.6.0.tgz - version: 0.6.0 - - created: 2017-03-16T23:33:31.598678756Z - description: Object-relational database management system (ORDBMS) with an emphasis - on extensibility and on standards-compliance. - digest: abfc2c516cce613f9042eb5d202b01f6766240f54bb3632df9378be033711b30 - engine: gotpl - home: https://www.postgresql.org/ - icon: https://www.postgresql.org/media/img/about/press/elephant.png - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - - name: databus23 - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.5.1.tgz - version: 0.5.1 - - created: 2017-03-15T11:18:31.676836517Z - description: Object-relational database management system (ORDBMS) with an emphasis - on extensibility and on standards-compliance. - digest: 29a90e1b52577e04a42da3ac7464b1b1cf9572b359d7825a375aad122659db55 - engine: gotpl - home: https://www.postgresql.org/ - icon: https://www.postgresql.org/media/img/about/press/elephant.png - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - - name: databus23 - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.5.0.tgz - version: 0.5.0 - - created: 2017-03-15T09:48:31.994004661Z - description: Object-relational database management system (ORDBMS) with an emphasis - on extensibility and on standards-compliance. - digest: 0ca09fbfd539d5258026fcaf7b640f88295271f88773acfa540dcd55cfaf6036 - engine: gotpl - home: https://www.postgresql.org/ - icon: https://www.postgresql.org/media/img/about/press/elephant.png - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - - name: databus23 - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.4.0.tgz - version: 0.4.0 - - created: 2017-02-13T21:33:27.762374795Z - description: Object-relational database management system (ORDBMS) with an emphasis - on extensibility and on standards-compliance. - digest: b07b7c12f13731ebc3019c11601351863030ad3933f72fb7925f3c0de39e23f4 - engine: gotpl - home: https://www.postgresql.org/ - icon: https://www.postgresql.org/media/img/about/press/elephant.png - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - - name: databus23 - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.3.4.tgz - version: 0.3.4 - - created: 2017-02-13T04:18:31.548242286Z - description: Chart for PostgreSQL - digest: 80685774a539b9efa27ea82337e9dd9fccd436688a84e2609d59a68a336d8f18 - engine: gotpl - home: https://www.postgresql.org/ - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - - name: databus23 - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.3.3.tgz - version: 0.3.3 - - created: 2017-02-06T17:48:28.059659169Z - description: Chart for PostgreSQL - digest: d8306a710181a440672795d0b5850e6851ae28b3ecb4cf5f92126c9f533700d5 - engine: gotpl - home: https://www.postgresql.org/ - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - - name: databus23 - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.3.2.tgz - version: 0.3.2 - - created: 2017-01-28T00:18:32.756495622Z - description: Chart for PostgreSQL - digest: 7b4e2b838ccb2e96c26f0b18d75b2cba224a634925abacaeee1b053a3db40f72 - engine: gotpl - home: https://www.postgresql.org/ - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - - name: databus23 - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.3.1.tgz - version: 0.3.1 - - created: 2017-01-26T17:18:33.808053693Z - description: Chart for PostgreSQL - digest: b8e412ddd2f2648efbaa84f85c924e5b94cba0393fb93ea607fdcab3132b7d2e - engine: gotpl - home: https://www.postgresql.org/ - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - - name: databus23 - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.3.0.tgz - version: 0.3.0 - - created: 2016-12-21T18:33:20.280731983Z - description: Chart for PostgreSQL - digest: a0516b4e5b83d9dd4af936859582878683183c6f72e9dddb73c9ff0fd280b972 - engine: gotpl - home: https://www.postgresql.org/ - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.2.2.tgz - version: 0.2.2 - - created: 2016-12-19T22:48:20.03810957Z - description: Chart for PostgreSQL - digest: 8d79ed44bb8477cdbbf8a3eb5a08eef787a147334b30d35a81f8ee43d07eb9ee - engine: gotpl - home: https://www.postgresql.org/ - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.2.1.tgz - version: 0.2.1 - - created: 2016-12-05T20:48:21.242600076Z - description: Chart for PostgreSQL - digest: 713d7ee250af7f914188d889ba3cb3065b4c36565b6f68ca8f55cd5340bda213 - engine: gotpl - home: https://www.postgresql.org/ - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.2.0.tgz - version: 0.2.0 - - created: 2016-11-08T15:03:20.745475633Z - description: Chart for PostgreSQL - digest: c79411d63ad872d0f6d034de9818cef565dbde3ac5f018ee8349305f286031a8 - engine: gotpl - home: https://www.postgresql.org/ - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.1.1.tgz - version: 0.1.1 - - created: 2016-11-04T14:18:20.014925806Z - description: Chart for PostgreSQL - digest: c984e3efb97da1b841c6a07b1e8fd32638b59de42e5b062690b8c9956be58959 - engine: gotpl - home: https://www.postgresql.org/ - keywords: - - postgresql - - postgres - - database - - sql - maintainers: - - name: swordbeta - name: postgresql - sources: - - https://github.com/kubernetes/charts - - https://github.com/docker-library/postgres - urls: - - https://kubernetes-charts.storage.googleapis.com/postgresql-0.1.0.tgz - version: 0.1.0 - prestashop: - - created: 2017-04-28T00:18:30.102289986Z - description: A popular open source ecommerce solution. Professional tools are - easily accessible to increase online sales including instant guest checkout, - abandoned cart reminders and automated Email marketing. - digest: fd575ef671455aa2c85ed91643419219bad86f8766b5775c1e869837cfbef6e5 - engine: gotpl - home: https://prestashop.com/ - icon: https://bitnami.com/assets/stacks/prestashop/img/prestashop-stack-110x117.png - keywords: - - prestashop - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: prestashop - sources: - - https://github.com/bitnami/bitnami-docker-prestashop - urls: - - https://kubernetes-charts.storage.googleapis.com/prestashop-0.4.6.tgz - version: 0.4.6 - - created: 2017-04-06T11:03:25.414976585Z - description: A popular open source ecommerce solution. Professional tools are - easily accessible to increase online sales including instant guest checkout, - abandoned cart reminders and automated Email marketing. - digest: 17eb689a1b04c31c1c78fa1b411f938eaad74bbd1ae78e4eecd0755770156043 - engine: gotpl - home: https://prestashop.com/ - icon: https://bitnami.com/assets/stacks/prestashop/img/prestashop-stack-110x117.png - keywords: - - prestashop - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: prestashop - sources: - - https://github.com/bitnami/bitnami-docker-prestashop - urls: - - https://kubernetes-charts.storage.googleapis.com/prestashop-0.4.5.tgz - version: 0.4.5 - - created: 2017-03-23T01:19:01.462007867Z - description: A popular open source ecommerce solution. Professional tools are - easily accessible to increase online sales including instant guest checkout, - abandoned cart reminders and automated Email marketing. - digest: ff84426385c8abb40d0f4f7e2cfc7fec70b0ae38ca32de832ccbb2b26f74787b - engine: gotpl - home: https://prestashop.com/ - icon: https://bitnami.com/assets/stacks/prestashop/img/prestashop-stack-110x117.png - keywords: - - prestashop - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: prestashop - sources: - - https://github.com/bitnami/bitnami-docker-prestashop - urls: - - https://kubernetes-charts.storage.googleapis.com/prestashop-0.4.4.tgz - version: 0.4.4 - - created: 2017-03-08T19:03:31.741573757Z - description: A popular open source ecommerce solution. Professional tools are - easily accessible to increase online sales including instant guest checkout, - abandoned cart reminders and automated Email marketing. - digest: 4beeb9d17c713b8065f9167d11672862fe2d1eeb5fded4d4ecd395a379825bbc - engine: gotpl - home: https://prestashop.com/ - icon: https://bitnami.com/assets/stacks/prestashop/img/prestashop-stack-110x117.png - keywords: - - prestashop - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: prestashop - sources: - - https://github.com/bitnami/bitnami-docker-prestashop - urls: - - https://kubernetes-charts.storage.googleapis.com/prestashop-0.4.3.tgz - version: 0.4.3 - - created: 2017-02-13T17:03:30.136404798Z - description: A popular open source ecommerce solution. Professional tools are - easily accessible to increase online sales including instant guest checkout, - abandoned cart reminders and automated Email marketing. - digest: fd8c2be6fd9348d2c5bda428ac61f0745974ebd7a4b58ad5d975d8d21888f1c4 - engine: gotpl - home: https://prestashop.com/ - icon: https://bitnami.com/assets/stacks/prestashop/img/prestashop-stack-110x117.png - keywords: - - prestashop - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: prestashop - sources: - - https://github.com/bitnami/bitnami-docker-prestashop - urls: - - https://kubernetes-charts.storage.googleapis.com/prestashop-0.4.2.tgz - version: 0.4.2 - - created: 2017-01-21T00:18:31.360326923Z - description: A popular open source ecommerce solution. Professional tools are - easily accessible to increase online sales including instant guest checkout, - abandoned cart reminders and automated Email marketing. - digest: eba227b32cb9f503c4fd41609d705019372f7936c69febaf2c4200735910e333 - engine: gotpl - home: https://prestashop.com/ - icon: https://bitnami.com/assets/stacks/prestashop/img/prestashop-stack-110x117.png - keywords: - - prestashop - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: prestashop - sources: - - https://github.com/bitnami/bitnami-docker-prestashop - urls: - - https://kubernetes-charts.storage.googleapis.com/prestashop-0.4.1.tgz - version: 0.4.1 - - created: 2017-01-04T00:48:19.749415831Z - description: A popular open source ecommerce solution. Professional tools are - easily accessible to increase online sales including instant guest checkout, - abandoned cart reminders and automated Email marketing. - digest: dd80da4612c962f5d02e2f52f7e0facf42a661d6d6f5eec36d7a3980e2a685d3 - engine: gotpl - home: https://prestashop.com/ - icon: https://bitnami.com/assets/stacks/prestashop/img/prestashop-stack-110x117.png - keywords: - - prestashop - - e-commerce - - http - - web - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: prestashop - sources: - - https://github.com/bitnami/bitnami-docker-prestashop - urls: - - https://kubernetes-charts.storage.googleapis.com/prestashop-0.4.0.tgz - version: 0.4.0 - prometheus: - - created: 2017-04-28T00:18:30.103009703Z - description: Prometheus is a monitoring system and time series database. - digest: 54cd97a780b0af5cd86d84455a29ec2f30765cfbf5c56f9eedf0c30539e325ec - engine: gotpl - home: https://prometheus.io/ - icon: https://raw.githubusercontent.com/prometheus/prometheus.github.io/master/assets/prometheus_logo-cb55bb5c346.png - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - - https://github.com/kubernetes/kube-state-metrics - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-3.0.2.tgz - version: 3.0.2 - - created: 2017-04-14T01:18:27.389282119Z - description: Prometheus is a monitoring system and time series database. - digest: 8240b1319bb4b1382f69e3ae753fce599185a7f21a399c7ed15108138cb6e793 - engine: gotpl - home: https://prometheus.io/ - icon: https://raw.githubusercontent.com/prometheus/prometheus.github.io/master/assets/prometheus_logo-cb55bb5c346.png - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - - https://github.com/kubernetes/kube-state-metrics - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-3.0.1.tgz - version: 3.0.1 - - created: 2017-03-23T19:03:32.048982259Z - description: Prometheus is a monitoring system and time series database. - digest: a6a49f3aaf7768dd84acfec96f95cab74a4ed1b68f95f23a9248d6926e8a2ba7 - engine: gotpl - home: https://prometheus.io/ - icon: https://raw.githubusercontent.com/prometheus/prometheus.github.io/master/assets/prometheus_logo-cb55bb5c346.png - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - - https://github.com/kubernetes/kube-state-metrics - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-3.0.0.tgz - version: 3.0.0 - - created: 2017-03-20T20:18:33.636286259Z - description: A Prometheus Helm chart for Kubernetes. Prometheus is a monitoring - system and time series database. - digest: 0bbefe8b7732b400320007e4f8898518ffcafd3371114be24ca8ded424fe60b3 - engine: gotpl - home: https://prometheus.io/ - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-2.0.4.tgz - version: 2.0.4 - - created: 2017-02-13T04:18:31.549501389Z - description: A Prometheus Helm chart for Kubernetes. Prometheus is a monitoring - system and time series database. - digest: 0f54774b8b258a8e126f09a66749b15c0691e0a330b65f397d58418b0fa0210c - engine: gotpl - home: https://prometheus.io/ - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-2.0.3.tgz - version: 2.0.3 - - created: 2017-02-03T20:18:29.168136057Z - description: A Prometheus Helm chart for Kubernetes. Prometheus is a monitoring - system and time series database. - digest: f1f457be370a944f3c703ceecc35664aa00f7a243730ca9e110bc18f1ed3ab9a - engine: gotpl - home: https://prometheus.io/ - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-2.0.2.tgz - version: 2.0.2 - - created: 2017-02-01T02:18:29.14318599Z - description: A Prometheus Helm chart for Kubernetes. Prometheus is a monitoring - system and time series database. - digest: eebea40b9e86c6dfb92048b0f63d47b11021ab0df437e2b13bc0fd1fc121e8d6 - engine: gotpl - home: https://prometheus.io/ - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-2.0.1.tgz - version: 2.0.1 - - created: 2017-01-19T19:18:27.372125459Z - description: A Prometheus Helm chart for Kubernetes. Prometheus is a monitoring - system and time series database. - digest: 0ae9e1c2ec6e3a6e2148f01e174bbbdd02a5797b4136e5de784383bca9bff938 - engine: gotpl - home: https://prometheus.io/ - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-2.0.0.tgz - version: 2.0.0 - - created: 2017-01-12T02:52:37.843602967Z - description: A Prometheus Helm chart for Kubernetes. Prometheus is a monitoring - system and time series database. - digest: cd8d763bdfe5d7c3c0e9a38f9741a2ef5de1c7c57a0c43a4407e70e2f6232dc9 - engine: gotpl - home: https://prometheus.io/ - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-1.4.2.tgz - version: 1.4.2 - - created: 2017-01-04T00:48:19.750279814Z - description: A Prometheus Helm chart for Kubernetes. Prometheus is a monitoring - system and time series database. - digest: 7209dafe19488487a8a151129deff24fe174d9734ea2c1629dd52bee183a8ad2 - engine: gotpl - home: https://prometheus.io/ - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-1.4.1.tgz - version: 1.4.1 - - created: 2016-12-09T18:48:20.189907856Z - description: A Prometheus Helm chart for Kubernetes. Prometheus is a monitoring - system and time series database. - digest: f6b4c948e408471b51ff6361e0d0f5afc801ee8141aae5002111ffbc12c68895 - engine: gotpl - home: https://prometheus.io/ - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: prometheus - sources: - - https://github.com/prometheus/alertmanager - - https://github.com/prometheus/prometheus - urls: - - https://kubernetes-charts.storage.googleapis.com/prometheus-1.3.1.tgz - version: 1.3.1 - rabbitmq: - - created: 2017-04-28T00:18:30.103417484Z - description: Open source message broker software that implements the Advanced - Message Queuing Protocol (AMQP) - digest: 1b4339d3f866cdc57072ce60fc3a579a5f4233aa6a4cb81254dcb9ddc0fabc33 - engine: gotpl - home: https://www.rabbitmq.com - icon: https://bitnami.com/assets/stacks/rabbitmq/img/rabbitmq-stack-220x234.png - keywords: - - rabbitmq - - message queue - - AMQP - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: rabbitmq - sources: - - https://github.com/bitnami/bitnami-docker-rabbitmq - urls: - - https://kubernetes-charts.storage.googleapis.com/rabbitmq-0.5.1.tgz - version: 0.5.1 - - created: 2017-04-06T10:33:26.336868116Z - description: Open source message broker software that implements the Advanced - Message Queuing Protocol (AMQP) - digest: 60c103b6cd7c57d5bf25588c0d1fcbbe0790ad50968d1947ce11e4b7ae1b2e6e - engine: gotpl - home: https://www.rabbitmq.com - icon: https://bitnami.com/assets/stacks/rabbitmq/img/rabbitmq-stack-220x234.png - keywords: - - rabbitmq - - message queue - - AMQP - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: rabbitmq - sources: - - https://github.com/bitnami/bitnami-docker-rabbitmq - urls: - - https://kubernetes-charts.storage.googleapis.com/rabbitmq-0.5.0.tgz - version: 0.5.0 - - created: 2017-04-03T22:33:26.690098498Z - description: Open source message broker software that implements the Advanced - Message Queuing Protocol (AMQP) - digest: 2f9b4afaffbe72c99c640de71b33e161e9d11386c287bab9028af19d1548bfa8 - engine: gotpl - home: https://www.rabbitmq.com - icon: https://bitnami.com/assets/stacks/rabbitmq/img/rabbitmq-stack-220x234.png - keywords: - - rabbitmq - - message queue - - AMQP - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: rabbitmq - sources: - - https://github.com/bitnami/bitnami-docker-rabbitmq - urls: - - https://kubernetes-charts.storage.googleapis.com/rabbitmq-0.4.5.tgz - version: 0.4.5 - - created: 2017-03-18T18:33:36.178174406Z - description: Open source message broker software that implements the Advanced - Message Queuing Protocol (AMQP) - digest: 833f7c8b0db5eeee50005fe88db8f1aaa899e5cc7875e18473a77162425756be - engine: gotpl - home: https://www.rabbitmq.com - icon: https://bitnami.com/assets/stacks/rabbitmq/img/rabbitmq-stack-220x234.png - keywords: - - rabbitmq - - message queue - - AMQP - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: rabbitmq - sources: - - https://github.com/bitnami/bitnami-docker-rabbitmq - urls: - - https://kubernetes-charts.storage.googleapis.com/rabbitmq-0.4.4.tgz - version: 0.4.4 - - created: 2017-03-16T13:33:30.85508759Z - description: Open source message broker software that implements the Advanced - Message Queuing Protocol (AMQP) - digest: bcfcbfa446f75dc1ca93f9a7d2ccc36731ef41f1dd5615e251344af0d6a1ba83 - engine: gotpl - home: https://www.rabbitmq.com - icon: https://bitnami.com/assets/stacks/rabbitmq/img/rabbitmq-stack-220x234.png - keywords: - - rabbitmq - - message queue - - AMQP - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: rabbitmq - sources: - - https://github.com/bitnami/bitnami-docker-rabbitmq - urls: - - https://kubernetes-charts.storage.googleapis.com/rabbitmq-0.4.3.tgz - version: 0.4.3 - - created: 2017-03-08T19:03:31.743135112Z - description: Open source message broker software that implements the Advanced - Message Queuing Protocol (AMQP) - digest: 7ad8ba0ff9d1b57778ffe60812be9087ad4fac27e8696fad4c9eba9a2529fdba - engine: gotpl - home: https://www.rabbitmq.com - icon: https://bitnami.com/assets/stacks/rabbitmq/img/rabbitmq-stack-220x234.png - keywords: - - rabbitmq - - message queue - - AMQP - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: rabbitmq - sources: - - https://github.com/bitnami/bitnami-docker-rabbitmq - urls: - - https://kubernetes-charts.storage.googleapis.com/rabbitmq-0.4.2.tgz - version: 0.4.2 - - created: 2017-02-13T04:18:31.549853753Z - description: Open source message broker software that implements the Advanced - Message Queuing Protocol (AMQP) - digest: 323ca950152028ecfa421b78ba0b9282265f39b934b07649b239be4d9f2dc10a - engine: gotpl - home: https://www.rabbitmq.com - keywords: - - rabbitmq - - message queue - - AMQP - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: rabbitmq - sources: - - https://github.com/bitnami/bitnami-docker-rabbitmq - urls: - - https://kubernetes-charts.storage.googleapis.com/rabbitmq-0.4.1.tgz - version: 0.4.1 - - created: 2017-01-31T00:03:26.184733426Z - description: Open source message broker software that implements the Advanced - Message Queuing Protocol (AMQP) - digest: 9bd9655f974dc3b2666c141718b65c7786e91c533ffee1784428a6d48cb458ca - engine: gotpl - home: https://www.rabbitmq.com - keywords: - - rabbitmq - - message queue - - AMQP - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: rabbitmq - sources: - - https://github.com/bitnami/bitnami-docker-rabbitmq - urls: - - https://kubernetes-charts.storage.googleapis.com/rabbitmq-0.4.0.tgz - version: 0.4.0 - redis: - - created: 2017-04-28T00:18:30.103747837Z - description: Open source, advanced key-value store. It is often referred to as - a data structure server since keys can contain strings, hashes, lists, sets - and sorted sets. - digest: 72af23bdc2aee61a6cc75e6b7fe3677d1f08ec98afbf8b6fccb3922c9bde47aa - engine: gotpl - home: http://redis.io/ - icon: https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png - keywords: - - redis - - keyvalue - - database - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redis - sources: - - https://github.com/bitnami/bitnami-docker-redis - urls: - - https://kubernetes-charts.storage.googleapis.com/redis-0.5.1.tgz - version: 0.5.1 - - created: 2017-04-03T22:18:27.809077961Z - description: Open source, advanced key-value store. It is often referred to as - a data structure server since keys can contain strings, hashes, lists, sets - and sorted sets. - digest: 84fdd07b6f56e7771696e7a707456808cb925f2a6311bf85e1bd027e5b2d364d - engine: gotpl - home: http://redis.io/ - icon: https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png - keywords: - - redis - - keyvalue - - database - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redis - sources: - - https://github.com/bitnami/bitnami-docker-redis - urls: - - https://kubernetes-charts.storage.googleapis.com/redis-0.5.0.tgz - version: 0.5.0 - - created: 2017-03-23T15:48:30.415372004Z - description: Open source, advanced key-value store. It is often referred to as - a data structure server since keys can contain strings, hashes, lists, sets - and sorted sets. - digest: 83ace7583e93e763b781d74c940d0966d7317d2b1665eaed35be9ca73dcace5e - engine: gotpl - home: http://redis.io/ - icon: https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png - keywords: - - redis - - keyvalue - - database - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redis - sources: - - https://github.com/bitnami/bitnami-docker-redis - urls: - - https://kubernetes-charts.storage.googleapis.com/redis-0.4.6.tgz - version: 0.4.6 - - created: 2017-03-02T19:33:28.196397881Z - description: Open source, advanced key-value store. It is often referred to as - a data structure server since keys can contain strings, hashes, lists, sets - and sorted sets. - digest: 7891aef2647fd00ca93cd6894720a6307d3fdd275f912eb6a05fcbb6b7009c13 - engine: gotpl - home: http://redis.io/ - icon: https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png - keywords: - - redis - - keyvalue - - database - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redis - sources: - - https://github.com/bitnami/bitnami-docker-redis - urls: - - https://kubernetes-charts.storage.googleapis.com/redis-0.4.5.tgz - version: 0.4.5 - - created: 2017-02-13T20:18:28.115940614Z - description: Open source, advanced key-value store. It is often referred to as - a data structure server since keys can contain strings, hashes, lists, sets - and sorted sets. - digest: e8cf2f96a6931397adf372857a6a0da161e7e9eb0cf91f565399d20b26144be1 - engine: gotpl - home: http://redis.io/ - icon: https://bitnami.com/assets/stacks/redis/img/redis-stack-220x234.png - keywords: - - redis - - keyvalue - - database - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redis - sources: - - https://github.com/bitnami/bitnami-docker-redis - urls: - - https://kubernetes-charts.storage.googleapis.com/redis-0.4.4.tgz - version: 0.4.4 - - created: 2017-02-13T04:18:31.550184203Z - description: Chart for Redis - digest: b4cb9b2e0811a83ce269dc06c25a05fe31deb799018eba418232b2c3f4b18b12 - engine: gotpl - home: http://redis.io/ - keywords: - - redis - - keyvalue - - database - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redis - sources: - - https://github.com/bitnami/bitnami-docker-redis - urls: - - https://kubernetes-charts.storage.googleapis.com/redis-0.4.3.tgz - version: 0.4.3 - - created: 2017-01-28T02:03:32.495597111Z - description: Chart for Redis - digest: 160dab504021716867790c3b1ea5c6e4afcaf865d9b8569707e123bc4d1536dc - engine: gotpl - home: http://redis.io/ - keywords: - - redis - - keyvalue - - database - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redis - sources: - - https://github.com/bitnami/bitnami-docker-redis - urls: - - https://kubernetes-charts.storage.googleapis.com/redis-0.4.2.tgz - version: 0.4.2 - - created: 2016-12-09T18:48:20.191261198Z - description: Chart for Redis - digest: 7132d9ddecaf4a890e5177c401228fa031f52e927393063f8d6c5a0881b281a3 - engine: gotpl - home: http://redis.io/ - keywords: - - redis - - keyvalue - - database - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redis - sources: - - https://github.com/bitnami/bitnami-docker-redis - urls: - - https://kubernetes-charts.storage.googleapis.com/redis-0.4.1.tgz - version: 0.4.1 - - created: 2016-10-31T16:33:19.644247028Z - description: Chart for Redis - digest: cef59b98a3607bf0f40560c724fd36a84e5f29498031a36c0f2f80369c35d9c4 - engine: gotpl - home: http://redis.io/ - keywords: - - redis - - keyvalue - - database - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redis - sources: - - https://github.com/bitnami/bitnami-docker-redis - urls: - - https://kubernetes-charts.storage.googleapis.com/redis-0.4.0.tgz - version: 0.4.0 - redmine: - - created: 2017-04-28T00:18:30.104404263Z - description: A flexible project management web application. - digest: 65419e8af0bff73d1c4da279cbb4d2abdaf99a714dabae01b27f67b917d4efc1 - engine: gotpl - home: http://www.redmine.org/ - icon: https://bitnami.com/assets/stacks/redmine/img/redmine-stack-220x234.png - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.4.2.tgz - version: 0.4.2 - - created: 2017-04-13T05:18:28.930142135Z - description: A flexible project management web application. - digest: 3edad0332b7aa4ff2c33729f0e49e7c58c0ad06108d66774d1f1583b8e4ccddf - engine: gotpl - home: http://www.redmine.org/ - icon: https://bitnami.com/assets/stacks/redmine/img/redmine-stack-220x234.png - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.4.1.tgz - version: 0.4.1 - - created: 2017-03-31T19:33:30.446728632Z - description: A flexible project management web application. - digest: facd552d60b39c5f6d37e8cf88f453bcae418a08eee0c401f15d15872e1e3601 - engine: gotpl - home: http://www.redmine.org/ - icon: https://bitnami.com/assets/stacks/redmine/img/redmine-stack-220x234.png - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.4.0.tgz - version: 0.4.0 - - created: 2017-03-14T23:48:31.907843022Z - description: A flexible project management web application. - digest: a21733ee877ad579f8b5be03d5a35008816d64dd56e0ca6482a7c0686fcdfe09 - engine: gotpl - home: http://www.redmine.org/ - icon: https://bitnami.com/assets/stacks/redmine/img/redmine-stack-220x234.png - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.11.tgz - version: 0.3.11 - - created: 2017-03-08T19:03:31.745197966Z - description: A flexible project management web application. - digest: 30253b618b47801a076c6cdd8a9ff93e1e4401e0189e88576553802b224e2775 - engine: gotpl - home: http://www.redmine.org/ - icon: https://bitnami.com/assets/stacks/redmine/img/redmine-stack-220x234.png - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.10.tgz - version: 0.3.10 - - created: 2017-02-13T21:33:27.767502945Z - description: A flexible project management web application. - digest: aa8a3b1be968e99c7a61ad0b7c1d13934562b9c30eeec0b3a3683063b9d38c7b - engine: gotpl - home: http://www.redmine.org/ - icon: https://bitnami.com/assets/stacks/redmine/img/redmine-stack-220x234.png - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.9.tgz - version: 0.3.9 - - created: 2017-02-10T23:18:26.028438027Z - description: A flexible project management web application. - digest: 51f4e834b5d2eb4ab66468e6996419bb20aa4d96ebe35a3663bc8b2c494694e6 - engine: gotpl - home: http://www.redmine.org/ - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.8.tgz - version: 0.3.8 - - created: 2017-01-31T00:18:28.517014253Z - description: A flexible project management web application. - digest: d9e7c4c47c853413107330d4fc0ad44e9bc3be90057ca722d28042b73f244fe5 - engine: gotpl - home: http://www.redmine.org/ - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.7.tgz - version: 0.3.7 - - created: 2016-12-15T21:18:24.678305914Z - description: A flexible project management web application. - digest: 98d9c8c7f241a9418bed6862f7c82295d5d8158cd1702907ced7150e46530768 - engine: gotpl - home: http://www.redmine.org/ - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.6.tgz - version: 0.3.6 - - created: 2016-12-05T21:03:20.228049572Z - description: A flexible project management web application. - digest: ae1c2ced129d05cdae28e1fe9c2bed53ded35cd77d96fc1b26f810d334c601e3 - engine: gotpl - home: http://www.redmine.org/ - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.5.tgz - version: 0.3.5 - - created: 2016-11-03T19:33:29.122956769Z - description: A flexible project management web application. - digest: c591dea135ef93f4af1a05961333125167ae551cf2b666363fe76b5a7ad9f806 - engine: gotpl - home: http://www.redmine.org/ - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.4.tgz - version: 0.3.4 - - created: 2016-10-21T19:18:18.621573514Z - description: A flexible project management web application. - digest: da6a8cb8c355a93ae11d9312be9eca51966d2288eafe96b6724e6154d000b8c3 - engine: gotpl - home: http://www.redmine.org/ - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.3.tgz - version: 0.3.3 - - created: 2016-10-19T00:03:14.035726608Z - description: A flexible project management web application. - digest: 052a0a97ff279db43f06c5ceeabfc5bd26f2e5f4f7ce7c24fdbcf761f97af84e - engine: gotpl - home: http://www.redmine.org/ - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.2.tgz - version: 0.3.2 - - created: 2016-10-19T00:03:14.034750035Z - description: A flexible project management web application. - digest: 88cf358644be274866ec5e88199c257e18a35fc8bbe97417658b9a0ea1e4a260 - engine: gotpl - home: http://www.redmine.org/ - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.1.tgz - version: 0.3.1 - - created: 2016-10-19T00:03:14.033766322Z - description: A flexible project management web application. - digest: f4815d35cbf9f8bb72c051ee528958b9c6f48b1f3bf8b3fdceaadd90d1b88068 - engine: gotpl - home: http://www.redmine.org/ - keywords: - - redmine - - project management - - www - - http - - web - - application - - ruby - - rails - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: redmine - sources: - - https://github.com/bitnami/bitnami-docker-redmine - urls: - - https://kubernetes-charts.storage.googleapis.com/redmine-0.3.0.tgz - version: 0.3.0 - sapho: - - apiVersion: v1 - created: 2017-04-28T00:18:30.105942339Z - description: A micro application development and integration platform that enables - organizations to create and deliver secure micro applications that tie into - existing business systems and track changes to key business data. - digest: 6c499f9875c07b508d23b081ffd991a5737a0acaf1c75def55dbb2dc07bf40ea - engine: gotpl - home: http://www.sapho.com - icon: https://www.sapho.com/wp-content/uploads/2016/04/sapho-logotype.svg - maintainers: - - email: support@sapho.com - name: Sapho - name: sapho - sources: - - https://bitbucket.org/sapho/ops-docker-tomcat/src - - https://hub.docker.com/r/sapho/ops-docker-tomcat - - https://github.com/kubernetes/charts/tree/master/stable/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/sapho-0.1.5.tgz - version: 0.1.5 - - apiVersion: v1 - created: 2017-02-13T04:33:52.314506112Z - description: A micro application development and integration platform that enables - organizations to create and deliver secure micro applications that tie into - existing business systems and track changes to key business data. - digest: abe8e15b8e51369d6d05033177efb524139d3352794e201003d2e3fce3d0669d - engine: gotpl - maintainers: - - email: support@sapho.com - name: Sapho - name: sapho - sources: - - https://bitbucket.org/sapho/ops-docker-tomcat/src - - https://hub.docker.com/r/sapho/ops-docker-tomcat - - https://github.com/kubernetes/charts/tree/master/stable/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/sapho-0.1.4.tgz - version: 0.1.4 - - apiVersion: v1 - created: 2017-01-31T00:18:28.518904Z - description: A micro application development and integration platform that enables - organizations to create and deliver secure micro applications that tie into - existing business systems and track changes to key business data. - digest: d93ff20d61a35de8ab23d5d118c177184a6b8b0578a39ba7d101f818a8742851 - engine: gotpl - maintainers: - - email: support@sapho.com - name: Sapho - name: sapho - sources: - - https://bitbucket.org/sapho/ops-docker-tomcat/src - - https://hub.docker.com/r/sapho/ops-docker-tomcat - - https://github.com/kubernetes/charts/tree/master/stable/mysql - urls: - - https://kubernetes-charts.storage.googleapis.com/sapho-0.1.3.tgz - version: 0.1.3 - selenium: - - created: 2017-04-28T00:18:30.106455441Z - description: Chart for selenium grid - digest: 0e03cf36738e83b3e6ae7384c0ffdeb4ee4b694f0c0a025eb15106acb189b8d2 - engine: gotpl - home: http://www.seleniumhq.org/ - icon: http://docs.seleniumhq.org/images/big-logo.png - keywords: - - qa - maintainers: - - email: techops@adaptly.com - name: Philip Champon (flah00) - name: selenium - sources: - - https://github.com/SeleniumHQ/docker-selenium - urls: - - https://kubernetes-charts.storage.googleapis.com/selenium-0.1.0.tgz - version: 0.1.0 - sensu: - - apiVersion: v1 - created: 2017-04-28T00:18:30.107064065Z - description: Sensu monitoring framework backed by the Redis transport - digest: 56c74a8de76074cfb021057112cf46d11d8b77f9ef5f6ec5d0877698c9931dfa - engine: gotpl - home: https://sensuapp.org/ - icon: https://raw.githubusercontent.com/sensu/sensu/master/sensu-logo.png - keywords: - - sensu - - monitoring - maintainers: - - email: shane.starcher@gmail.com - name: Shane Starcher - name: sensu - sources: - - https://github.com/kubernetes/charts - - https://github.com/sstarcher/docker-sensu - - https://github.com/sensu/sensu - urls: - - https://kubernetes-charts.storage.googleapis.com/sensu-0.1.2.tgz - version: 0.1.2 - - apiVersion: v1 - created: 2017-03-17T05:18:29.12808256Z - description: Sensu monitoring framework backed by the Redis transport - digest: bb8781a9693f3b6df9389b3098a6298658127df2e86ad8156788602f541f33c3 - engine: gotpl - home: https://sensuapp.org/ - icon: https://raw.githubusercontent.com/sensu/sensu/master/sensu-logo.png - keywords: - - sensu - - monitoring - maintainers: - - email: shane.starcher@gmail.com - name: Shane Starcher - name: sensu - sources: - - https://github.com/kubernetes/charts - - https://github.com/sstarcher/docker-sensu - - https://github.com/sensu/sensu - urls: - - https://kubernetes-charts.storage.googleapis.com/sensu-0.1.1.tgz - version: 0.1.1 - - apiVersion: v1 - created: 2016-12-21T23:33:22.277352049Z - description: Sensu monitoring framework backed by the Redis transport - digest: 4592387df52c4110a3a313820dbea81e8bf0252845e8c08ad7c71bce9a92831c - engine: gotpl - home: https://sensuapp.org/ - icon: https://raw.githubusercontent.com/sensu/sensu/master/sensu-logo.png - keywords: - - sensu - - monitoring - maintainers: - - email: shane.starcher@gmail.com - name: Shane Starcher - name: sensu - sources: - - https://github.com/kubernetes/charts - - https://github.com/sstarcher/docker-sensu - - https://github.com/sensu/sensu - urls: - - https://kubernetes-charts.storage.googleapis.com/sensu-0.1.0.tgz - version: 0.1.0 - spark: - - created: 2017-04-28T00:18:30.107369986Z - description: Fast and general-purpose cluster computing system. - digest: d37ec7d7530a5836eeeb5ff54110d594efe188ce8175a7c2e3b50e5d9f5af9bc - home: http://spark.apache.org - icon: http://spark.apache.org/images/spark-logo-trademark.png - maintainers: - - email: lachlan.evenson@gmail.com - name: Lachlan Evenson - name: spark - sources: - - https://github.com/kubernetes/kubernetes/tree/master/examples/spark - - https://github.com/apache/spark - urls: - - https://kubernetes-charts.storage.googleapis.com/spark-0.1.4.tgz - version: 0.1.4 - - created: 2017-03-09T19:03:32.57258203Z - description: Fast and general-purpose cluster computing system. - digest: 1cea71eb812c7ea6d566ad34247ad8d1c7b2a460b908748372618a94f035d974 - home: http://spark.apache.org - icon: http://spark.apache.org/images/spark-logo-trademark.png - maintainers: - - email: lachlan.evenson@gmail.com - name: Lachlan Evenson - name: spark - sources: - - https://github.com/kubernetes/kubernetes/tree/master/examples/spark - - https://github.com/apache/spark - urls: - - https://kubernetes-charts.storage.googleapis.com/spark-0.1.3.tgz - version: 0.1.3 - - created: 2017-02-13T04:33:52.317122021Z - description: A Apache Spark Helm chart for Kubernetes. Apache Spark is a fast - and general-purpose cluster computing system - digest: fd5559299116691e56c85f60be46e3b1d1a647973f4dfd6c0d87d0b0274a349b - home: http://spark.apache.org/ - maintainers: - - email: lachlan.evenson@gmail.com - name: Lachlan Evenson - name: spark - sources: - - https://github.com/kubernetes/kubernetes/tree/master/examples/spark - - https://github.com/apache/spark - urls: - - https://kubernetes-charts.storage.googleapis.com/spark-0.1.2.tgz - version: 0.1.2 - - created: 2017-01-27T21:48:32.088621169Z - description: A Apache Spark Helm chart for Kubernetes. Apache Spark is a fast - and general-purpose cluster computing system - digest: 884cc07e4710011476db63017b48504cc00b00faf461cdfe83aac40f0fd33e49 - home: http://spark.apache.org/ - maintainers: - - email: lachlan.evenson@gmail.com - name: Lachlan Evenson - name: spark - sources: - - https://github.com/kubernetes/kubernetes/tree/master/examples/spark - - https://github.com/apache/spark - urls: - - https://kubernetes-charts.storage.googleapis.com/spark-0.1.1.tgz - version: 0.1.1 - spartakus: - - created: 2017-04-28T00:18:30.107681212Z - description: Collect information about Kubernetes clusters to help improve the - project. - digest: 7db8a6ac7280c8d112b533b2653cfa8ed43d8517a4cf31d28e24d5761d8c6b80 - engine: gotpl - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: spartakus - sources: - - https://github.com/kubernetes-incubator/spartakus - urls: - - https://kubernetes-charts.storage.googleapis.com/spartakus-1.1.1.tgz - version: 1.1.1 - - created: 2017-03-02T18:48:30.451198217Z - description: Collect information about Kubernetes clusters to help improve the - project. - digest: 84720960919addcce5b608717eca0218b7f6cd9edbf77a52ddc0747e51037936 - engine: gotpl - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: spartakus - sources: - - https://github.com/kubernetes-incubator/spartakus - urls: - - https://kubernetes-charts.storage.googleapis.com/spartakus-1.1.0.tgz - version: 1.1.0 - - created: 2017-02-13T17:03:30.144830851Z - description: A Spartakus Helm chart for Kubernetes. Spartakus aims to collect - information about Kubernetes clusters. - digest: 1c202628cd57e01cb324ee6e9457b52d1e1a5fd665f99d4bb25bd17c92c438e9 - engine: gotpl - maintainers: - - email: mgoodness@gmail.com - name: Michael Goodness - name: spartakus - sources: - - https://github.com/kubernetes-incubator/spartakus - urls: - - https://kubernetes-charts.storage.googleapis.com/spartakus-1.0.0.tgz - version: 1.0.0 - spinnaker: - - apiVersion: v1 - created: 2017-04-28T00:18:30.109150773Z - description: Open source, multi-cloud continuous delivery platform for releasing - software changes with high velocity and confidence. - digest: a06ae1d7452e19824110cbb3270c5b7bfc4acf10af23e072e442b81fe26b1dc5 - home: http://spinnaker.io/ - icon: https://pbs.twimg.com/profile_images/669205226994319362/O7OjwPrh_400x400.png - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: spinnaker - sources: - - https://github.com/spinnaker - - https://github.com/viglesiasce/images - urls: - - https://kubernetes-charts.storage.googleapis.com/spinnaker-0.1.1.tgz - version: 0.1.1 - - apiVersion: v1 - created: 2017-02-13T20:48:27.29021219Z - description: A Helm chart for Kubernetes - digest: cc44efeace9d645b2ea824b017986d86b6b3a50fcd94e86199e0e6849eb02731 - home: http://spinnaker.io/ - maintainers: - - email: viglesias@google.com - name: Vic Iglesias - name: spinnaker - sources: - - https://github.com/spinnaker - - https://github.com/viglesiasce/images - urls: - - https://kubernetes-charts.storage.googleapis.com/spinnaker-0.1.0.tgz - version: 0.1.0 - sumokube: - - created: 2017-04-28T00:18:30.109952763Z - description: Sumologic Log Collector - digest: 2f4f5cfc4c1d40cd24085497041fd701f72d4f15cb55241bfb998da82b05c7b9 - keywords: - - monitoring - - logging - maintainers: - - email: jdumars+github@gmail.com - name: Jason DuMars - - email: knoxville+github@gmail.com - name: Sean Knox - name: sumokube - sources: - - https://github.com/SumoLogic/sumologic-collector-docker - urls: - - https://kubernetes-charts.storage.googleapis.com/sumokube-0.1.1.tgz - version: 0.1.1 - - created: 2017-01-27T21:48:32.092039665Z - description: Sumologic Log Collector - digest: 5b173be9b7dc0e1d48a7cd11015b9c405666a40420a290c5fb54e4f8718b4fc0 - keywords: - - monitoring - - logging - maintainers: - - email: jdumars+github@gmail.com - name: Jason DuMars - - email: knoxville+github@gmail.com - name: Sean Knox - name: sumokube - sources: - - https://github.com/SumoLogic/sumologic-collector-docker - urls: - - https://kubernetes-charts.storage.googleapis.com/sumokube-0.1.0.tgz - version: 0.1.0 - telegraf: - - created: 2017-04-28T00:18:30.110460492Z - description: Telegraf is an agent written in Go for collecting, processing, aggregating, - and writing metrics. - digest: 850b4b7543a3dd7f5d33ba65d9098fe4f361981f49452a40ce9774850b4285e3 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/telegraf/ - keywords: - - telegraf - - collector - - timeseries - - influxdata - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: telegraf - urls: - - https://kubernetes-charts.storage.googleapis.com/telegraf-0.2.0.tgz - version: 0.2.0 - - created: 2017-02-13T21:48:52.617397285Z - description: Telegraf is an agent written in Go for collecting, processing, aggregating, - and writing metrics. - digest: 1f74106455808d45d16742f6d7d02164eb328a40dd9699dfa4511b33efaf14e9 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/telegraf/ - keywords: - - telegraf - - collector - - timeseries - - influxdata - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: telegraf - urls: - - https://kubernetes-charts.storage.googleapis.com/telegraf-0.1.1.tgz - version: 0.1.1 - - created: 2017-02-11T03:18:26.54678474Z - description: Chart for Telegraf Kubernetes deployments - digest: 52fa68fd948ee675a5d1a5ffff22d98e293ee37569a8fa56a4022f51e9507184 - engine: gotpl - home: https://www.influxdata.com/time-series-platform/telegraf/ - keywords: - - telegraf - - collector - - timeseries - - influxdata - maintainers: - - email: jack@influxdb.com - name: Jack Zampolin - name: telegraf - urls: - - https://kubernetes-charts.storage.googleapis.com/telegraf-0.1.0.tgz - version: 0.1.0 - testlink: - - created: 2017-04-28T00:18:30.111130012Z - description: Web-based test management system that facilitates software quality - assurance. - digest: 8cffc761a9e6618bc015cec3721964192e909dfaae92a9bb79c4471424c74128 - engine: gotpl - home: http://www.testlink.org/ - icon: https://bitnami.com/assets/stacks/testlink/img/testlink-stack-220x234.png - keywords: - - testlink - - testing - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: testlink - sources: - - https://github.com/bitnami/bitnami-docker-testlink - urls: - - https://kubernetes-charts.storage.googleapis.com/testlink-0.4.6.tgz - version: 0.4.6 - - created: 2017-03-23T18:18:30.028234531Z - description: Web-based test management system that facilitates software quality - assurance. - digest: 08f7104671364ff6bd43270659733ea97a4adc06181f8a5c3027ac3d0078e51c - engine: gotpl - home: http://www.testlink.org/ - icon: https://bitnami.com/assets/stacks/testlink/img/testlink-stack-220x234.png - keywords: - - testlink - - testing - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: testlink - sources: - - https://github.com/bitnami/bitnami-docker-testlink - urls: - - https://kubernetes-charts.storage.googleapis.com/testlink-0.4.5.tgz - version: 0.4.5 - - created: 2017-03-08T19:03:31.751542723Z - description: Web-based test management system that facilitates software quality - assurance. - digest: 7861921ff159f1be6834acfc3e5c139382a8c6461b20a45c4b1561985827c865 - engine: gotpl - home: http://www.testlink.org/ - icon: https://bitnami.com/assets/stacks/testlink/img/testlink-stack-220x234.png - keywords: - - testlink - - testing - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: testlink - sources: - - https://github.com/bitnami/bitnami-docker-testlink - urls: - - https://kubernetes-charts.storage.googleapis.com/testlink-0.4.4.tgz - version: 0.4.4 - - created: 2017-02-11T03:18:26.547570032Z - description: Web-based test management system that facilitates software quality - assurance. - digest: 2c7188d5f1a9fb03c71b2e2d693dfbef9a739ae8889d9eb38854900cf066077b - engine: gotpl - home: http://www.testlink.org/ - keywords: - - testlink - - testing - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: testlink - sources: - - https://github.com/bitnami/bitnami-docker-testlink - urls: - - https://kubernetes-charts.storage.googleapis.com/testlink-0.4.3.tgz - version: 0.4.3 - - created: 2017-01-21T00:18:31.369288453Z - description: Web-based test management system that facilitates software quality - assurance. - digest: 78f6a9cfe1843b8ea99489d8b4c801f84271ee25827ad044989ed0df21ac086b - engine: gotpl - home: http://www.testlink.org/ - keywords: - - testlink - - testing - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: testlink - sources: - - https://github.com/bitnami/bitnami-docker-testlink - urls: - - https://kubernetes-charts.storage.googleapis.com/testlink-0.4.2.tgz - version: 0.4.2 - - created: 2016-12-15T21:18:24.679744308Z - description: Web-based test management system that facilitates software quality - assurance. - digest: 9edb2777c6db4794885a2c7531a28436774edc248aad3a26007bca4076058143 - engine: gotpl - home: http://www.testlink.org/ - keywords: - - testlink - - testing - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: testlink - sources: - - https://github.com/bitnami/bitnami-docker-testlink - urls: - - https://kubernetes-charts.storage.googleapis.com/testlink-0.4.1.tgz - version: 0.4.1 - - created: 2016-12-09T18:48:20.193151472Z - description: Web-based test management system that facilitates software quality - assurance. - digest: df216a31082cdf15867ee9a17b107e4006e9e0a20b79425889b695c4c46fb0c1 - engine: gotpl - home: http://www.testlink.org/ - keywords: - - testlink - - testing - - http - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: testlink - sources: - - https://github.com/bitnami/bitnami-docker-testlink - urls: - - https://kubernetes-charts.storage.googleapis.com/testlink-0.4.0.tgz - version: 0.4.0 - traefik: - - apiVersion: v1 - created: 2017-04-28T00:18:30.111646123Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: 4019610a5fb1defcc5bc90532cb19c986999114f7de4aef3f0272e6c7ed1b914 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.2.1-a.tgz - version: 1.2.1-a - - apiVersion: v1 - created: 2017-03-31T19:33:30.456523182Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: ba0ade25b34f419ad0790b220fb7277a046d48bc76e1c726f66ba535b51d4f63 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.2-h.tgz - version: 1.1.2-h - - apiVersion: v1 - created: 2017-03-16T23:33:31.610346236Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: 9aa401aee6da3b4afc5cc3f8be7ff9f74bf424743ca72a7a7b91a7105d9781b6 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.2-g.tgz - version: 1.1.2-g - - apiVersion: v1 - created: 2017-02-27T17:18:28.185706737Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: 5bb7b98b962098808e3b73f604592bc4c6e6245e0074fa0c99308fc04bf766b8 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.2-f.tgz - version: 1.1.2-f - - apiVersion: v1 - created: 2017-02-13T22:18:28.973464794Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: ae467c4bee7364d17de2583d33031d0eeb2ef55e7962a7db0245d692e65479e1 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.2-e.tgz - version: 1.1.2-e - - apiVersion: v1 - created: 2017-02-13T21:33:27.776086791Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: 399d74bcd8ab26f2de10894d83b59d413752797789b9fe9568e17f7b564f5f75 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.2-d.tgz - version: 1.1.2-d - - apiVersion: v1 - created: 2017-02-03T19:33:30.806247527Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: e225511060509d9cf3e38eaafd93af9ee994f8ed99c40a25500f4a1d06851841 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.2-c.tgz - version: 1.1.2-c - - apiVersion: v1 - created: 2017-02-01T02:18:29.153394653Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: 9cc02b2e43c901c92aa560b4f85e325f04635d052035418f3b27b06bdd571ae9 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.2-b.tgz - version: 1.1.2-b - - apiVersion: v1 - created: 2017-01-28T00:18:32.767314879Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: 9bae960964d5062dd4c412ad7daf6f6f9e8dd070264aa3f44c831c817fc26b7d - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.2-a.tgz - version: 1.1.2-a - - apiVersion: v1 - created: 2017-01-03T17:48:20.753425335Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: e8ab4576505091785b27084e4f4e4f02f1ee3f1744d9842ec086457baabe8b85 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.1-a.tgz - version: 1.1.1-a - - apiVersion: v1 - created: 2016-11-23T00:33:20.024479934Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: 6664534aab03a22531602a415ca14a72e932b08fe1feab8866cc55ba18b77dc8 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.0-rc3-a.tgz - version: 1.1.0-rc3-a - - apiVersion: v1 - created: 2016-11-30T22:03:20.721274307Z - description: A Traefik based Kubernetes ingress controller with Let's Encrypt - support - digest: 2828d7284839baee1fb36f823ce4e2574a4b675b7f4f74e921a4685f4cee28c2 - engine: gotpl - home: http://traefik.io/ - icon: http://traefik.io/traefik.logo.png - keywords: - - traefik - - ingress - - acme - - letsencrypt - maintainers: - - email: engineering@deis.com - name: Deis - name: traefik - sources: - - https://github.com/containous/traefik - - https://github.com/krancour/charts/tree/master/traefik - urls: - - https://kubernetes-charts.storage.googleapis.com/traefik-1.1.0-a.tgz - version: 1.1.0-a - uchiwa: - - apiVersion: v1 - created: 2017-04-28T00:18:30.112528335Z - description: Dashboard for the Sensu monitoring framework - digest: b9b7186c2e53d4049c4b0ef9ba9c89ded7de36bf920653b63f6ea725253354d6 - engine: gotpl - home: https://uchiwa.io/ - icon: https://uchiwa.io/img/favicon.png - keywords: - - uchiwa - - sensu - - monitoring - maintainers: - - email: shane.starcher@gmail.com - name: Shane Starcher - name: uchiwa - sources: - - https://github.com/kubernetes/charts - - https://github.com/sstarcher/docker-uchiwa - - https://github.com/sensu/uchiwa - urls: - - https://kubernetes-charts.storage.googleapis.com/uchiwa-0.2.1.tgz - version: 0.2.1 - - apiVersion: v1 - created: 2017-03-17T06:03:29.101091523Z - description: Dashboard for the Sensu monitoring framework - digest: 9bee21cd61e56e08f58c1ba130e0a4af1a1d62a8d7921f9408509bd501494403 - engine: gotpl - home: https://uchiwa.io/ - icon: https://uchiwa.io/img/favicon.png - keywords: - - uchiwa - - sensu - - monitoring - maintainers: - - email: shane.starcher@gmail.com - name: Shane Starcher - name: uchiwa - sources: - - https://github.com/kubernetes/charts - - https://github.com/sstarcher/docker-uchiwa - - https://github.com/sensu/uchiwa - urls: - - https://kubernetes-charts.storage.googleapis.com/uchiwa-0.2.0.tgz - version: 0.2.0 - - apiVersion: v1 - created: 2017-01-18T23:03:27.817024829Z - description: Dashboard for the Sensu monitoring framework - digest: 868d7e58adb2fead4ed9e4be17e2017c2d1c55d265b2a579625e787e6f15f4d5 - engine: gotpl - home: https://uchiwa.io/ - icon: https://uchiwa.io/img/favicon.png - keywords: - - uchiwa - - sensu - - monitoring - maintainers: - - email: shane.starcher@gmail.com - name: Shane Starcher - name: uchiwa - sources: - - https://github.com/kubernetes/charts - - https://github.com/sstarcher/docker-uchiwa - - https://github.com/sensu/uchiwa - urls: - - https://kubernetes-charts.storage.googleapis.com/uchiwa-0.1.0.tgz - version: 0.1.0 - wordpress: - - created: 2017-04-28T00:18:30.114169329Z - description: Web publishing platform for building blogs and websites. - digest: 8df4b37c471d43b5b3955ecadcc0da1dad31ba28a93ae0b74be5fc94debf2876 - engine: gotpl - home: http://www.wordpress.com/ - icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.6.0.tgz - version: 0.6.0 - - created: 2017-04-03T22:33:26.700088102Z - description: Web publishing platform for building blogs and websites. - digest: 4413a17258eaca753252174a219ba9081283a406375d8ae49e5c1f3313c6619a - engine: gotpl - home: http://www.wordpress.com/ - icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.5.2.tgz - version: 0.5.2 - - created: 2017-03-23T21:18:31.877594706Z - description: Web publishing platform for building blogs and websites. - digest: 3e408baaa5110edfd730603bd5d49d7a8c222f49c7e9de1bd168b564463d57d9 - engine: gotpl - home: http://www.wordpress.com/ - icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.5.1.tgz - version: 0.5.1 - - created: 2017-03-16T13:33:30.866725941Z - description: Web publishing platform for building blogs and websites. - digest: 40c767b4b2b7d494ea6da7a20a9fe58e76896a0bdad7c6c569f9d8cdab71f2e3 - engine: gotpl - home: http://www.wordpress.com/ - icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.5.0.tgz - version: 0.5.0 - - created: 2017-03-14T23:48:31.917245657Z - description: Web publishing platform for building blogs and websites. - digest: 306220e3c19f1360644eade517a2a8ca422e8f9ec6ea9c65181ce8fc9797772f - engine: gotpl - home: http://www.wordpress.com/ - icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.4.3.tgz - version: 0.4.3 - - created: 2017-03-08T19:03:31.755452536Z - description: Web publishing platform for building blogs and websites. - digest: 0689b452d3c9a9bee6e5c84b48172c68de6eedc253223b96ab6500ad88a5de40 - engine: gotpl - home: http://www.wordpress.com/ - icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.4.2.tgz - version: 0.4.2 - - created: 2017-02-13T04:33:52.323397093Z - description: Web publishing platform for building blogs and websites. - digest: 0fc412dea55069b368183afefb74342001a91a7f3a0e9126a921581d7740d61c - engine: gotpl - home: http://www.wordpress.com/ - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.4.1.tgz - version: 0.4.1 - - created: 2017-01-28T00:18:32.769124587Z - description: Web publishing platform for building blogs and websites. - digest: 2f4a5d65350b36a6481c4c3d619f713835f091821d3f56c38c718061628ff712 - engine: gotpl - home: http://www.wordpress.com/ - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.4.0.tgz - version: 0.4.0 - - created: 2017-01-04T00:48:19.757447587Z - description: Web publishing platform for building blogs and websites. - digest: f62b6f1728a33c5d59dd24dc6fb984f13d2dffac2bc6eec01724501e66ffc6a0 - engine: gotpl - home: http://www.wordpress.com/ - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.3.4.tgz - version: 0.3.4 - - created: 2016-12-15T00:48:24.021239603Z - description: Web publishing platform for building blogs and websites. - digest: 0c86b7cec5877a3c3c55d919b2f02ae52340c953afd9dc541ae0280bc23fe9aa - engine: gotpl - home: http://www.wordpress.com/ - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.3.3.tgz - version: 0.3.3 - - created: 2016-12-09T18:48:20.19465733Z - description: Web publishing platform for building blogs and websites. - digest: 589e49370cb09f6d9ddb3ceba3b21f52697570cd4b40aff891a660c5daaa9bec - engine: gotpl - home: http://www.wordpress.com/ - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.3.2.tgz - version: 0.3.2 - - created: 2016-10-21T19:18:18.622178432Z - description: Web publishing platform for building blogs and websites. - digest: e70a072dcbb7252becc8899f54de8cb5977ceaea47197919c3990a6896adc350 - engine: gotpl - home: http://www.wordpress.com/ - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.3.1.tgz - version: 0.3.1 - - created: 2016-10-19T00:03:14.037631856Z - description: Web publishing platform for building blogs and websites. - digest: 1c44515f02fb34b722dce1d8cf5fed0dfbbd2f8c03d63b335211b7bcb12b6dea - engine: gotpl - home: http://www.wordpress.com/ - keywords: - - wordpress - - cms - - blog - - http - - web - - application - - php - maintainers: - - email: containers@bitnami.com - name: Bitnami - name: wordpress - sources: - - https://github.com/bitnami/bitnami-docker-wordpress - urls: - - https://kubernetes-charts.storage.googleapis.com/wordpress-0.3.0.tgz - version: 0.3.0 -generated: 2017-04-28T00:18:30.070608132Z diff --git a/pkg/getter/testdata/repository/repositories.yaml b/pkg/getter/testdata/repository/repositories.yaml index 1d884a0c75c..14ae6a8eb0b 100644 --- a/pkg/getter/testdata/repository/repositories.yaml +++ b/pkg/getter/testdata/repository/repositories.yaml @@ -6,7 +6,7 @@ repositories: certFile: "" keyFile: "" name: stable - url: https://kubernetes-charts.storage.googleapis.com + url: https://charts.helm.sh/stable - caFile: "" cache: repository/cache/local-index.yaml certFile: "" diff --git a/pkg/helm/client.go b/pkg/helm/client.go deleted file mode 100644 index 43e9f4dafa2..00000000000 --- a/pkg/helm/client.go +++ /dev/null @@ -1,522 +0,0 @@ -/* -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 "k8s.io/helm/pkg/helm" - -import ( - "fmt" - "io" - "time" - - "golang.org/x/net/context" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/keepalive" - - healthpb "google.golang.org/grpc/health/grpc_health_v1" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/proto/hapi/chart" - rls "k8s.io/helm/pkg/proto/hapi/services" -) - -// maxMsgSize use 20MB as the default message size limit. -// grpc library default is 4MB -const maxMsgSize = 1024 * 1024 * 20 - -// Client manages client side of the Helm-Tiller protocol. -type Client struct { - opts options -} - -// NewClient creates a new client. -func NewClient(opts ...Option) *Client { - var c Client - // set some sane defaults - c.Option(ConnectTimeout(5)) - return c.Option(opts...) -} - -// Option configures the Helm client with the provided options. -func (h *Client) Option(opts ...Option) *Client { - for _, opt := range opts { - opt(&h.opts) - } - return h -} - -// ListReleases lists the current releases. -func (h *Client) ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) { - reqOpts := h.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &reqOpts.listReq - ctx := NewContext() - - if reqOpts.before != nil { - if err := reqOpts.before(ctx, req); err != nil { - return nil, err - } - } - return h.list(ctx, req) -} - -// InstallRelease loads a chart from chstr, installs it, and returns the release response. -func (h *Client) InstallRelease(chstr, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { - // load the chart to install - chart, err := chartutil.Load(chstr) - if err != nil { - return nil, err - } - - return h.InstallReleaseFromChart(chart, ns, opts...) -} - -// InstallReleaseFromChart installs a new chart and returns the release response. -func (h *Client) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { - // apply the install options - reqOpts := h.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &reqOpts.instReq - req.Chart = chart - req.Namespace = ns - req.DryRun = reqOpts.dryRun - req.DisableHooks = reqOpts.disableHooks - req.ReuseName = reqOpts.reuseName - ctx := NewContext() - - if reqOpts.before != nil { - if err := reqOpts.before(ctx, req); err != nil { - return nil, err - } - } - err := chartutil.ProcessRequirementsEnabled(req.Chart, req.Values) - if err != nil { - return nil, err - } - err = chartutil.ProcessRequirementsImportValues(req.Chart) - if err != nil { - return nil, err - } - - return h.install(ctx, req) -} - -// DeleteRelease uninstalls a named release and returns the response. -func (h *Client) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) { - // apply the uninstall options - reqOpts := h.opts - for _, opt := range opts { - opt(&reqOpts) - } - - if reqOpts.dryRun { - // In the dry run case, just see if the release exists - r, err := h.ReleaseContent(rlsName) - if err != nil { - return &rls.UninstallReleaseResponse{}, err - } - return &rls.UninstallReleaseResponse{Release: r.Release}, nil - } - - req := &reqOpts.uninstallReq - req.Name = rlsName - req.DisableHooks = reqOpts.disableHooks - ctx := NewContext() - - if reqOpts.before != nil { - if err := reqOpts.before(ctx, req); err != nil { - return nil, err - } - } - return h.delete(ctx, req) -} - -// UpdateRelease loads a chart from chstr and updates a release to a new/different chart. -func (h *Client) UpdateRelease(rlsName string, chstr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { - // load the chart to update - chart, err := chartutil.Load(chstr) - if err != nil { - return nil, err - } - - return h.UpdateReleaseFromChart(rlsName, chart, opts...) -} - -// UpdateReleaseFromChart updates a release to a new/different chart. -func (h *Client) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { - // apply the update options - reqOpts := h.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &reqOpts.updateReq - req.Chart = chart - req.DryRun = reqOpts.dryRun - req.Name = rlsName - req.DisableHooks = reqOpts.disableHooks - req.Recreate = reqOpts.recreate - req.Force = reqOpts.force - req.ResetValues = reqOpts.resetValues - req.ReuseValues = reqOpts.reuseValues - ctx := NewContext() - - if reqOpts.before != nil { - if err := reqOpts.before(ctx, req); err != nil { - return nil, err - } - } - err := chartutil.ProcessRequirementsEnabled(req.Chart, req.Values) - if err != nil { - return nil, err - } - err = chartutil.ProcessRequirementsImportValues(req.Chart) - if err != nil { - return nil, err - } - - return h.update(ctx, req) -} - -// GetVersion returns the server version. -func (h *Client) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) { - reqOpts := h.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &rls.GetVersionRequest{} - ctx := NewContext() - - if reqOpts.before != nil { - if err := reqOpts.before(ctx, req); err != nil { - return nil, err - } - } - return h.version(ctx, req) -} - -// RollbackRelease rolls back a release to the previous version. -func (h *Client) RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) { - reqOpts := h.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &reqOpts.rollbackReq - req.Recreate = reqOpts.recreate - req.Force = reqOpts.force - req.DisableHooks = reqOpts.disableHooks - req.DryRun = reqOpts.dryRun - req.Name = rlsName - ctx := NewContext() - - if reqOpts.before != nil { - if err := reqOpts.before(ctx, req); err != nil { - return nil, err - } - } - return h.rollback(ctx, req) -} - -// ReleaseStatus returns the given release's status. -func (h *Client) ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) { - reqOpts := h.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &reqOpts.statusReq - req.Name = rlsName - ctx := NewContext() - - if reqOpts.before != nil { - if err := reqOpts.before(ctx, req); err != nil { - return nil, err - } - } - return h.status(ctx, req) -} - -// ReleaseContent returns the configuration for a given release. -func (h *Client) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) { - reqOpts := h.opts - for _, opt := range opts { - opt(&reqOpts) - } - req := &reqOpts.contentReq - req.Name = rlsName - ctx := NewContext() - - if reqOpts.before != nil { - if err := reqOpts.before(ctx, req); err != nil { - return nil, err - } - } - return h.content(ctx, req) -} - -// ReleaseHistory returns a release's revision history. -func (h *Client) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error) { - reqOpts := h.opts - for _, opt := range opts { - opt(&reqOpts) - } - - req := &reqOpts.histReq - req.Name = rlsName - ctx := NewContext() - - if reqOpts.before != nil { - if err := reqOpts.before(ctx, req); err != nil { - return nil, err - } - } - return h.history(ctx, req) -} - -// RunReleaseTest executes a pre-defined test on a release. -func (h *Client) RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error) { - reqOpts := h.opts - for _, opt := range opts { - opt(&reqOpts) - } - - req := &reqOpts.testReq - req.Name = rlsName - ctx := NewContext() - - return h.test(ctx, req) -} - -// PingTiller pings the Tiller pod and ensure's that it is up and running -func (h *Client) PingTiller() error { - ctx := NewContext() - return h.ping(ctx) -} - -// connect returns a gRPC connection to Tiller or error. The gRPC dial options -// are constructed here. -func (h *Client) connect(ctx context.Context) (conn *grpc.ClientConn, err error) { - opts := []grpc.DialOption{ - grpc.WithBlock(), - grpc.WithKeepaliveParams(keepalive.ClientParameters{ - // Send keepalive every 30 seconds to prevent the connection from - // getting closed by upstreams - Time: time.Duration(30) * time.Second, - }), - grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize)), - } - switch { - case h.opts.useTLS: - opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(h.opts.tlsConfig))) - default: - opts = append(opts, grpc.WithInsecure()) - } - ctx, cancel := context.WithTimeout(ctx, h.opts.connectTimeout) - defer cancel() - if conn, err = grpc.DialContext(ctx, h.opts.host, opts...); err != nil { - return nil, err - } - return conn, nil -} - -// Executes tiller.ListReleases RPC. -func (h *Client) list(ctx context.Context, req *rls.ListReleasesRequest) (*rls.ListReleasesResponse, error) { - c, err := h.connect(ctx) - if err != nil { - return nil, err - } - defer c.Close() - - rlc := rls.NewReleaseServiceClient(c) - s, err := rlc.ListReleases(ctx, req) - if err != nil { - return nil, err - } - var resp *rls.ListReleasesResponse - for { - r, err := s.Recv() - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - if resp == nil { - resp = r - continue - } - resp.Releases = append(resp.Releases, r.GetReleases()[0]) - } - return resp, nil -} - -// Executes tiller.InstallRelease RPC. -func (h *Client) install(ctx context.Context, req *rls.InstallReleaseRequest) (*rls.InstallReleaseResponse, error) { - c, err := h.connect(ctx) - if err != nil { - return nil, err - } - defer c.Close() - - rlc := rls.NewReleaseServiceClient(c) - return rlc.InstallRelease(ctx, req) -} - -// Executes tiller.UninstallRelease RPC. -func (h *Client) delete(ctx context.Context, req *rls.UninstallReleaseRequest) (*rls.UninstallReleaseResponse, error) { - c, err := h.connect(ctx) - if err != nil { - return nil, err - } - defer c.Close() - - rlc := rls.NewReleaseServiceClient(c) - return rlc.UninstallRelease(ctx, req) -} - -// Executes tiller.UpdateRelease RPC. -func (h *Client) update(ctx context.Context, req *rls.UpdateReleaseRequest) (*rls.UpdateReleaseResponse, error) { - c, err := h.connect(ctx) - if err != nil { - return nil, err - } - defer c.Close() - - rlc := rls.NewReleaseServiceClient(c) - return rlc.UpdateRelease(ctx, req) -} - -// Executes tiller.RollbackRelease RPC. -func (h *Client) rollback(ctx context.Context, req *rls.RollbackReleaseRequest) (*rls.RollbackReleaseResponse, error) { - c, err := h.connect(ctx) - if err != nil { - return nil, err - } - defer c.Close() - - rlc := rls.NewReleaseServiceClient(c) - return rlc.RollbackRelease(ctx, req) -} - -// Executes tiller.GetReleaseStatus RPC. -func (h *Client) status(ctx context.Context, req *rls.GetReleaseStatusRequest) (*rls.GetReleaseStatusResponse, error) { - c, err := h.connect(ctx) - if err != nil { - return nil, err - } - defer c.Close() - - rlc := rls.NewReleaseServiceClient(c) - return rlc.GetReleaseStatus(ctx, req) -} - -// Executes tiller.GetReleaseContent RPC. -func (h *Client) content(ctx context.Context, req *rls.GetReleaseContentRequest) (*rls.GetReleaseContentResponse, error) { - c, err := h.connect(ctx) - if err != nil { - return nil, err - } - defer c.Close() - - rlc := rls.NewReleaseServiceClient(c) - return rlc.GetReleaseContent(ctx, req) -} - -// Executes tiller.GetVersion RPC. -func (h *Client) version(ctx context.Context, req *rls.GetVersionRequest) (*rls.GetVersionResponse, error) { - c, err := h.connect(ctx) - if err != nil { - return nil, err - } - defer c.Close() - - rlc := rls.NewReleaseServiceClient(c) - return rlc.GetVersion(ctx, req) -} - -// Executes tiller.GetHistory RPC. -func (h *Client) history(ctx context.Context, req *rls.GetHistoryRequest) (*rls.GetHistoryResponse, error) { - c, err := h.connect(ctx) - if err != nil { - return nil, err - } - defer c.Close() - - rlc := rls.NewReleaseServiceClient(c) - return rlc.GetHistory(ctx, req) -} - -// Executes tiller.TestRelease RPC. -func (h *Client) test(ctx context.Context, req *rls.TestReleaseRequest) (<-chan *rls.TestReleaseResponse, <-chan error) { - errc := make(chan error, 1) - c, err := h.connect(ctx) - if err != nil { - errc <- err - return nil, errc - } - - ch := make(chan *rls.TestReleaseResponse, 1) - go func() { - defer close(errc) - defer close(ch) - defer c.Close() - - rlc := rls.NewReleaseServiceClient(c) - s, err := rlc.RunReleaseTest(ctx, req) - if err != nil { - errc <- err - return - } - - for { - msg, err := s.Recv() - if err == io.EOF { - return - } - if err != nil { - errc <- err - return - } - ch <- msg - } - }() - - return ch, errc -} - -// Executes tiller.Ping RPC. -func (h *Client) ping(ctx context.Context) error { - c, err := h.connect(ctx) - if err != nil { - return err - } - defer c.Close() - - healthClient := healthpb.NewHealthClient(c) - resp, err := healthClient.Check(ctx, &healthpb.HealthCheckRequest{Service: "Tiller"}) - if err != nil { - return err - } - switch resp.GetStatus() { - case healthpb.HealthCheckResponse_SERVING: - return nil - case healthpb.HealthCheckResponse_NOT_SERVING: - return fmt.Errorf("tiller is not serving requests at this time, Please try again later") - default: - return fmt.Errorf("tiller healthcheck returned an unknown status") - } -} diff --git a/pkg/helm/client_test.go b/pkg/helm/client_test.go deleted file mode 100644 index 95e04449983..00000000000 --- a/pkg/helm/client_test.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -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 ( - "testing" - "time" -) - -func TestNewClient(t *testing.T) { - helmClient := NewClient() - if helmClient.opts.connectTimeout != 5*time.Second { - t.Errorf("expected default timeout duration to be 5 seconds, got %v", helmClient.opts.connectTimeout) - } - - helmClient = NewClient(ConnectTimeout(60)) - if helmClient.opts.connectTimeout != time.Minute { - t.Errorf("expected timeout duration to be 1 minute, got %v", helmClient.opts.connectTimeout) - } -} diff --git a/pkg/helm/environment/environment.go b/pkg/helm/environment/environment.go deleted file mode 100644 index 2980e6dc9c9..00000000000 --- a/pkg/helm/environment/environment.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -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 environment describes the operating environment for Tiller. - -Tiller's environment encapsulates all of the service dependencies Tiller has. -These dependencies are expressed as interfaces so that alternate implementations -(mocks, etc.) can be easily generated. -*/ -package environment - -import ( - "os" - "path/filepath" - - "github.com/spf13/pflag" - - "k8s.io/client-go/util/homedir" - "k8s.io/helm/pkg/helm/helmpath" -) - -// DefaultHelmHome is the default HELM_HOME. -var DefaultHelmHome = filepath.Join(homedir.HomeDir(), ".helm") - -// EnvSettings describes all of the environment settings. -type EnvSettings struct { - // TillerHost is the host and port of Tiller. - TillerHost string - // TillerConnectionTimeout is the duration (in seconds) helm will wait to establish a connection to tiller. - TillerConnectionTimeout int64 - // TillerNamespace is the namespace in which Tiller runs. - TillerNamespace string - // Home is the local path to the Helm home directory. - Home helmpath.Home - // Debug indicates whether or not Helm is running in Debug mode. - Debug bool - // KubeContext is the name of the kubeconfig context. - KubeContext string -} - -// AddFlags binds flags to the given flagset. -func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { - fs.StringVar((*string)(&s.Home), "home", DefaultHelmHome, "location of your Helm config. Overrides $HELM_HOME") - fs.StringVar(&s.TillerHost, "host", "", "address of Tiller. Overrides $HELM_HOST") - fs.StringVar(&s.KubeContext, "kube-context", "", "name of the kubeconfig context to use") - fs.BoolVar(&s.Debug, "debug", false, "enable verbose output") - fs.StringVar(&s.TillerNamespace, "tiller-namespace", "kube-system", "namespace of Tiller") - fs.Int64Var(&s.TillerConnectionTimeout, "tiller-connection-timeout", int64(300), "the duration (in seconds) Helm will wait to establish a connection to tiller") -} - -// Init sets values from the environment. -func (s *EnvSettings) Init(fs *pflag.FlagSet) { - for name, envar := range envMap { - setFlagFromEnv(name, envar, fs) - } -} - -// PluginDirs is the path to the plugin directories. -func (s EnvSettings) PluginDirs() string { - if d, ok := os.LookupEnv("HELM_PLUGIN"); ok { - return d - } - return s.Home.Plugins() -} - -// envMap maps flag names to envvars -var envMap = map[string]string{ - "debug": "HELM_DEBUG", - "home": "HELM_HOME", - "host": "HELM_HOST", - "tiller-namespace": "TILLER_NAMESPACE", -} - -func setFlagFromEnv(name, envar string, fs *pflag.FlagSet) { - if fs.Changed(name) { - return - } - if v, ok := os.LookupEnv(envar); ok { - fs.Set(name, v) - } -} - -// Deprecated -const ( - HomeEnvVar = "HELM_HOME" - PluginEnvVar = "HELM_PLUGIN" - PluginDisableEnvVar = "HELM_NO_PLUGINS" - HostEnvVar = "HELM_HOST" - DebugEnvVar = "HELM_DEBUG" -) diff --git a/pkg/helm/environment/environment_test.go b/pkg/helm/environment/environment_test.go deleted file mode 100644 index 8f0caa388a5..00000000000 --- a/pkg/helm/environment/environment_test.go +++ /dev/null @@ -1,134 +0,0 @@ -/* -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 environment - -import ( - "os" - "strings" - "testing" - - "k8s.io/helm/pkg/helm/helmpath" - - "github.com/spf13/pflag" -) - -func TestEnvSettings(t *testing.T) { - tests := []struct { - name string - - // input - args []string - envars map[string]string - - // expected values - home, host, ns, kcontext, plugins string - debug bool - }{ - { - name: "defaults", - args: []string{}, - home: DefaultHelmHome, - plugins: helmpath.Home(DefaultHelmHome).Plugins(), - ns: "kube-system", - }, - { - name: "with flags set", - args: []string{"--home", "/foo", "--host=here", "--debug", "--tiller-namespace=myns"}, - home: "/foo", - plugins: helmpath.Home("/foo").Plugins(), - host: "here", - ns: "myns", - debug: true, - }, - { - name: "with envvars set", - args: []string{}, - envars: map[string]string{"HELM_HOME": "/bar", "HELM_HOST": "there", "HELM_DEBUG": "1", "TILLER_NAMESPACE": "yourns"}, - home: "/bar", - plugins: helmpath.Home("/bar").Plugins(), - host: "there", - ns: "yourns", - debug: true, - }, - { - name: "with flags and envvars set", - args: []string{"--home", "/foo", "--host=here", "--debug", "--tiller-namespace=myns"}, - envars: map[string]string{"HELM_HOME": "/bar", "HELM_HOST": "there", "HELM_DEBUG": "1", "TILLER_NAMESPACE": "yourns", "HELM_PLUGIN": "glade"}, - home: "/foo", - plugins: "glade", - host: "here", - ns: "myns", - debug: true, - }, - } - - cleanup := resetEnv() - defer cleanup() - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for k, v := range tt.envars { - os.Setenv(k, v) - } - - flags := pflag.NewFlagSet("testing", pflag.ContinueOnError) - - settings := &EnvSettings{} - settings.AddFlags(flags) - flags.Parse(tt.args) - - settings.Init(flags) - - if settings.Home != helmpath.Home(tt.home) { - t.Errorf("expected home %q, got %q", tt.home, settings.Home) - } - if settings.PluginDirs() != tt.plugins { - t.Errorf("expected plugins %q, got %q", tt.plugins, settings.PluginDirs()) - } - if settings.TillerHost != tt.host { - t.Errorf("expected host %q, got %q", tt.host, settings.TillerHost) - } - if settings.Debug != tt.debug { - t.Errorf("expected debug %t, got %t", tt.debug, settings.Debug) - } - if settings.TillerNamespace != tt.ns { - t.Errorf("expected tiller-namespace %q, got %q", tt.ns, settings.TillerNamespace) - } - if settings.KubeContext != tt.kcontext { - t.Errorf("expected kube-context %q, got %q", tt.kcontext, settings.KubeContext) - } - - cleanup() - }) - } -} - -func resetEnv() func() { - origEnv := os.Environ() - - // ensure any local envvars do not hose us - for _, e := range envMap { - os.Unsetenv(e) - } - - return func() { - for _, pair := range origEnv { - kv := strings.SplitN(pair, "=", 2) - os.Setenv(kv[0], kv[1]) - } - } -} diff --git a/pkg/helm/fake.go b/pkg/helm/fake.go deleted file mode 100644 index 0a9e77c4406..00000000000 --- a/pkg/helm/fake.go +++ /dev/null @@ -1,277 +0,0 @@ -/* -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 "k8s.io/helm/pkg/helm" - -import ( - "errors" - "fmt" - "math/rand" - "sync" - - "github.com/golang/protobuf/ptypes/timestamp" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/proto/hapi/release" - rls "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/proto/hapi/version" -) - -// FakeClient implements Interface -type FakeClient struct { - Rels []*release.Release - Responses map[string]release.TestRun_Status - Opts options -} - -// Option returns the fake release client -func (c *FakeClient) Option(opts ...Option) Interface { - for _, opt := range opts { - opt(&c.Opts) - } - return c -} - -var _ Interface = &FakeClient{} -var _ Interface = (*FakeClient)(nil) - -// ListReleases lists the current releases -func (c *FakeClient) ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) { - resp := &rls.ListReleasesResponse{ - Count: int64(len(c.Rels)), - Releases: c.Rels, - } - return resp, nil -} - -// InstallRelease creates a new release and returns a InstallReleaseResponse containing that release -func (c *FakeClient) InstallRelease(chStr, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { - chart := &chart.Chart{} - return c.InstallReleaseFromChart(chart, ns, opts...) -} - -// InstallReleaseFromChart adds a new MockRelease to the fake client and returns a InstallReleaseResponse containing that release -func (c *FakeClient) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { - for _, opt := range opts { - opt(&c.Opts) - } - - releaseName := c.Opts.instReq.Name - - // Check to see if the release already exists. - rel, err := c.ReleaseStatus(releaseName, nil) - if err == nil && rel != nil { - return nil, errors.New("cannot re-use a name that is still in use") - } - - release := ReleaseMock(&MockReleaseOptions{Name: releaseName, Namespace: ns}) - c.Rels = append(c.Rels, release) - - return &rls.InstallReleaseResponse{ - Release: release, - }, nil -} - -// DeleteRelease deletes a release from the FakeClient -func (c *FakeClient) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) { - for i, rel := range c.Rels { - if rel.Name == rlsName { - c.Rels = append(c.Rels[:i], c.Rels[i+1:]...) - return &rls.UninstallReleaseResponse{ - Release: rel, - }, nil - } - } - - return nil, fmt.Errorf("No such release: %s", rlsName) -} - -// GetVersion returns a fake version -func (c *FakeClient) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) { - return &rls.GetVersionResponse{ - Version: &version.Version{ - SemVer: "1.2.3-fakeclient+testonly", - }, - }, nil -} - -// UpdateRelease returns an UpdateReleaseResponse containing the updated release, if it exists -func (c *FakeClient) UpdateRelease(rlsName string, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { - return c.UpdateReleaseFromChart(rlsName, &chart.Chart{}, opts...) -} - -// UpdateReleaseFromChart returns an UpdateReleaseResponse containing the updated release, if it exists -func (c *FakeClient) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { - // Check to see if the release already exists. - rel, err := c.ReleaseContent(rlsName, nil) - if err != nil { - return nil, err - } - - return &rls.UpdateReleaseResponse{Release: rel.Release}, nil -} - -// RollbackRelease returns nil, nil -func (c *FakeClient) RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) { - return nil, nil -} - -// ReleaseStatus returns a release status response with info from the matching release name. -func (c *FakeClient) ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) { - for _, rel := range c.Rels { - if rel.Name == rlsName { - return &rls.GetReleaseStatusResponse{ - Name: rel.Name, - Info: rel.Info, - Namespace: rel.Namespace, - }, nil - } - } - return nil, fmt.Errorf("No such release: %s", rlsName) -} - -// ReleaseContent returns the configuration for the matching release name in the fake release client. -func (c *FakeClient) ReleaseContent(rlsName string, opts ...ContentOption) (resp *rls.GetReleaseContentResponse, err error) { - for _, rel := range c.Rels { - if rel.Name == rlsName { - return &rls.GetReleaseContentResponse{ - Release: rel, - }, nil - } - } - return resp, fmt.Errorf("No such release: %s", rlsName) -} - -// ReleaseHistory returns a release's revision history. -func (c *FakeClient) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error) { - return &rls.GetHistoryResponse{Releases: c.Rels}, nil -} - -// RunReleaseTest executes a pre-defined tests on a release -func (c *FakeClient) RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error) { - - results := make(chan *rls.TestReleaseResponse) - errc := make(chan error, 1) - - go func() { - var wg sync.WaitGroup - for m, s := range c.Responses { - wg.Add(1) - - go func(msg string, status release.TestRun_Status) { - defer wg.Done() - results <- &rls.TestReleaseResponse{Msg: msg, Status: status} - }(m, s) - } - - wg.Wait() - close(results) - close(errc) - }() - - return results, errc -} - -// PingTiller pings the Tiller pod and ensure's that it is up and running -func (c *FakeClient) PingTiller() error { - return nil -} - -// MockHookTemplate is the hook template used for all mock release objects. -var MockHookTemplate = `apiVersion: v1 -kind: Job -metadata: - annotations: - "helm.sh/hook": pre-install -` - -// MockManifest is the manifest used for all mock release objects. -var MockManifest = `apiVersion: v1 -kind: Secret -metadata: - name: fixture -` - -// MockReleaseOptions allows for user-configurable options on mock release objects. -type MockReleaseOptions struct { - Name string - Version int32 - Chart *chart.Chart - StatusCode release.Status_Code - Namespace string -} - -// ReleaseMock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing. -func ReleaseMock(opts *MockReleaseOptions) *release.Release { - date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0} - - name := opts.Name - if name == "" { - name = "testrelease-" + string(rand.Intn(100)) - } - - var version int32 = 1 - if opts.Version != 0 { - version = opts.Version - } - - namespace := opts.Namespace - if namespace == "" { - namespace = "default" - } - - ch := opts.Chart - if opts.Chart == nil { - ch = &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "foo", - Version: "0.1.0-beta.1", - }, - Templates: []*chart.Template{ - {Name: "templates/foo.tpl", Data: []byte(MockManifest)}, - }, - } - } - - scode := release.Status_DEPLOYED - if opts.StatusCode > 0 { - scode = opts.StatusCode - } - - return &release.Release{ - Name: name, - Info: &release.Info{ - FirstDeployed: &date, - LastDeployed: &date, - Status: &release.Status{Code: scode}, - Description: "Release mock", - }, - Chart: ch, - Config: &chart.Config{Raw: `name: "value"`}, - Version: version, - Namespace: namespace, - 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, - } -} diff --git a/pkg/helm/fake_test.go b/pkg/helm/fake_test.go deleted file mode 100644 index 9c0a537594e..00000000000 --- a/pkg/helm/fake_test.go +++ /dev/null @@ -1,283 +0,0 @@ -/* -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 ( - "reflect" - "testing" - - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/proto/hapi/release" - rls "k8s.io/helm/pkg/proto/hapi/services" -) - -func TestFakeClient_ReleaseStatus(t *testing.T) { - releasePresent := ReleaseMock(&MockReleaseOptions{Name: "release-present"}) - releaseNotPresent := ReleaseMock(&MockReleaseOptions{Name: "release-not-present"}) - - type fields struct { - Rels []*release.Release - } - type args struct { - rlsName string - opts []StatusOption - } - tests := []struct { - name string - fields fields - args args - want *rls.GetReleaseStatusResponse - wantErr bool - }{ - { - name: "Get a single release that exists", - fields: fields{ - Rels: []*release.Release{ - releasePresent, - }, - }, - args: args{ - rlsName: releasePresent.Name, - opts: nil, - }, - want: &rls.GetReleaseStatusResponse{ - Name: releasePresent.Name, - Info: releasePresent.Info, - Namespace: releasePresent.Namespace, - }, - - wantErr: false, - }, - { - name: "Get a release that does not exist", - fields: fields{ - Rels: []*release.Release{ - releasePresent, - }, - }, - args: args{ - rlsName: releaseNotPresent.Name, - opts: nil, - }, - want: nil, - wantErr: true, - }, - { - name: "Get a single release that exists from list", - fields: fields{ - Rels: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "angry-dolphin", Namespace: "default"}), - ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir", Namespace: "default"}), - releasePresent, - }, - }, - args: args{ - rlsName: releasePresent.Name, - opts: nil, - }, - want: &rls.GetReleaseStatusResponse{ - Name: releasePresent.Name, - Info: releasePresent.Info, - Namespace: releasePresent.Namespace, - }, - - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &FakeClient{ - Rels: tt.fields.Rels, - } - got, err := c.ReleaseStatus(tt.args.rlsName, tt.args.opts...) - if (err != nil) != tt.wantErr { - t.Errorf("FakeClient.ReleaseStatus() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("FakeClient.ReleaseStatus() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestFakeClient_InstallReleaseFromChart(t *testing.T) { - installChart := &chart.Chart{} - type fields struct { - Rels []*release.Release - } - type args struct { - ns string - opts []InstallOption - } - tests := []struct { - name string - fields fields - args args - want *rls.InstallReleaseResponse - relsAfter []*release.Release - wantErr bool - }{ - { - name: "Add release to an empty list.", - fields: fields{ - Rels: []*release.Release{}, - }, - args: args{ - ns: "default", - opts: []InstallOption{ReleaseName("new-release")}, - }, - want: &rls.InstallReleaseResponse{ - Release: ReleaseMock(&MockReleaseOptions{Name: "new-release"}), - }, - relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "new-release"}), - }, - wantErr: false, - }, - { - name: "Try to add a release where the name already exists.", - fields: fields{ - Rels: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "new-release"}), - }, - }, - args: args{ - ns: "default", - opts: []InstallOption{ReleaseName("new-release")}, - }, - relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "new-release"}), - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &FakeClient{ - Rels: tt.fields.Rels, - } - got, err := c.InstallReleaseFromChart(installChart, tt.args.ns, tt.args.opts...) - if (err != nil) != tt.wantErr { - t.Errorf("FakeClient.InstallReleaseFromChart() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("FakeClient.InstallReleaseFromChart() = %v, want %v", got, tt.want) - } - if !reflect.DeepEqual(c.Rels, tt.relsAfter) { - t.Errorf("FakeClient.InstallReleaseFromChart() rels = %v, expected %v", got, tt.relsAfter) - } - }) - } -} - -func TestFakeClient_DeleteRelease(t *testing.T) { - type fields struct { - Rels []*release.Release - } - type args struct { - rlsName string - opts []DeleteOption - } - tests := []struct { - name string - fields fields - args args - want *rls.UninstallReleaseResponse - relsAfter []*release.Release - wantErr bool - }{ - { - name: "Delete a release that exists.", - fields: fields{ - Rels: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "angry-dolphin"}), - ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - }, - args: args{ - rlsName: "trepid-tapir", - opts: []DeleteOption{}, - }, - relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "angry-dolphin"}), - }, - want: &rls.UninstallReleaseResponse{ - Release: ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - wantErr: false, - }, - { - name: "Delete a release that does not exist.", - fields: fields{ - Rels: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "angry-dolphin"}), - ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - }, - args: args{ - rlsName: "release-that-does-not-exists", - opts: []DeleteOption{}, - }, - relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "angry-dolphin"}), - ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - want: nil, - wantErr: true, - }, - { - name: "Delete when only 1 item exists.", - fields: fields{ - Rels: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - }, - args: args{ - rlsName: "trepid-tapir", - opts: []DeleteOption{}, - }, - relsAfter: []*release.Release{}, - want: &rls.UninstallReleaseResponse{ - Release: ReleaseMock(&MockReleaseOptions{Name: "trepid-tapir"}), - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := &FakeClient{ - Rels: tt.fields.Rels, - } - got, err := c.DeleteRelease(tt.args.rlsName, tt.args.opts...) - if (err != nil) != tt.wantErr { - t.Errorf("FakeClient.DeleteRelease() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("FakeClient.DeleteRelease() = %v, want %v", got, tt.want) - } - - if !reflect.DeepEqual(c.Rels, tt.relsAfter) { - t.Errorf("FakeClient.InstallReleaseFromChart() rels = %v, expected %v", got, tt.relsAfter) - } - }) - } -} diff --git a/pkg/helm/helm_test.go b/pkg/helm/helm_test.go deleted file mode 100644 index 2b0436581d7..00000000000 --- a/pkg/helm/helm_test.go +++ /dev/null @@ -1,363 +0,0 @@ -/* -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 "k8s.io/helm/pkg/helm" - -import ( - "errors" - "path/filepath" - "reflect" - "testing" - - "github.com/golang/protobuf/proto" - "golang.org/x/net/context" - - "k8s.io/helm/pkg/chartutil" - cpb "k8s.io/helm/pkg/proto/hapi/chart" - rls "k8s.io/helm/pkg/proto/hapi/release" - tpb "k8s.io/helm/pkg/proto/hapi/services" -) - -// Path to example charts relative to pkg/helm. -const chartsDir = "../../docs/examples/" - -// Sentinel error to indicate to the Helm client to not send the request to Tiller. -var errSkip = errors.New("test: skip") - -// Verify each ReleaseListOption is applied to a ListReleasesRequest correctly. -func TestListReleases_VerifyOptions(t *testing.T) { - // Options testdata - var limit = 2 - var offset = "offset" - var filter = "filter" - var sortBy = int32(2) - var sortOrd = int32(1) - var codes = []rls.Status_Code{ - rls.Status_FAILED, - rls.Status_DELETED, - rls.Status_DEPLOYED, - rls.Status_SUPERSEDED, - } - var namespace = "namespace" - - // Expected ListReleasesRequest message - exp := &tpb.ListReleasesRequest{ - Limit: int64(limit), - Offset: offset, - Filter: filter, - SortBy: tpb.ListSort_SortBy(sortBy), - SortOrder: tpb.ListSort_SortOrder(sortOrd), - StatusCodes: codes, - Namespace: namespace, - } - - // Options used in ListReleases - ops := []ReleaseListOption{ - ReleaseListSort(sortBy), - ReleaseListOrder(sortOrd), - ReleaseListLimit(limit), - ReleaseListOffset(offset), - ReleaseListFilter(filter), - ReleaseListStatuses(codes), - ReleaseListNamespace(namespace), - } - - // BeforeCall option to intercept Helm client ListReleasesRequest - b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { - switch act := msg.(type) { - case *tpb.ListReleasesRequest: - t.Logf("ListReleasesRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type ListReleasesRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - - if _, err := client.ListReleases(ops...); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.listReq.Filter) -} - -// Verify each InstallOption is applied to an InstallReleaseRequest correctly. -func TestInstallRelease_VerifyOptions(t *testing.T) { - // Options testdata - var disableHooks = true - var releaseName = "test" - var namespace = "default" - var reuseName = true - var dryRun = true - var chartName = "alpine" - var chartPath = filepath.Join(chartsDir, chartName) - var overrides = []byte("key1=value1,key2=value2") - - // Expected InstallReleaseRequest message - exp := &tpb.InstallReleaseRequest{ - Chart: loadChart(t, chartName), - Values: &cpb.Config{Raw: string(overrides)}, - DryRun: dryRun, - Name: releaseName, - DisableHooks: disableHooks, - Namespace: namespace, - ReuseName: reuseName, - } - - // Options used in InstallRelease - ops := []InstallOption{ - ValueOverrides(overrides), - InstallDryRun(dryRun), - ReleaseName(releaseName), - InstallReuseName(reuseName), - InstallDisableHooks(disableHooks), - } - - // BeforeCall option to intercept Helm client InstallReleaseRequest - b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { - switch act := msg.(type) { - case *tpb.InstallReleaseRequest: - t.Logf("InstallReleaseRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type InstallReleaseRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.InstallRelease(chartPath, namespace, ops...); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.instReq.Name) -} - -// Verify each DeleteOptions is applied to an UninstallReleaseRequest correctly. -func TestDeleteRelease_VerifyOptions(t *testing.T) { - // Options testdata - var releaseName = "test" - var disableHooks = true - var purgeFlag = true - - // Expected DeleteReleaseRequest message - exp := &tpb.UninstallReleaseRequest{ - Name: releaseName, - Purge: purgeFlag, - DisableHooks: disableHooks, - } - - // Options used in DeleteRelease - ops := []DeleteOption{ - DeletePurge(purgeFlag), - DeleteDisableHooks(disableHooks), - } - - // BeforeCall option to intercept Helm client DeleteReleaseRequest - b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { - switch act := msg.(type) { - case *tpb.UninstallReleaseRequest: - t.Logf("UninstallReleaseRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type UninstallReleaseRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.DeleteRelease(releaseName, ops...); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.uninstallReq.Name) -} - -// Verify each UpdateOption is applied to an UpdateReleaseRequest correctly. -func TestUpdateRelease_VerifyOptions(t *testing.T) { - // Options testdata - var chartName = "alpine" - var chartPath = filepath.Join(chartsDir, chartName) - var releaseName = "test" - var disableHooks = true - var overrides = []byte("key1=value1,key2=value2") - var dryRun = false - - // Expected UpdateReleaseRequest message - exp := &tpb.UpdateReleaseRequest{ - Name: releaseName, - Chart: loadChart(t, chartName), - Values: &cpb.Config{Raw: string(overrides)}, - DryRun: dryRun, - DisableHooks: disableHooks, - } - - // Options used in UpdateRelease - ops := []UpdateOption{ - UpgradeDryRun(dryRun), - UpdateValueOverrides(overrides), - UpgradeDisableHooks(disableHooks), - } - - // BeforeCall option to intercept Helm client UpdateReleaseRequest - b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { - switch act := msg.(type) { - case *tpb.UpdateReleaseRequest: - t.Logf("UpdateReleaseRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type UpdateReleaseRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.UpdateRelease(releaseName, chartPath, ops...); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.updateReq.Name) -} - -// Verify each RollbackOption is applied to a RollbackReleaseRequest correctly. -func TestRollbackRelease_VerifyOptions(t *testing.T) { - // Options testdata - var disableHooks = true - var releaseName = "test" - var revision = int32(2) - var dryRun = true - - // Expected RollbackReleaseRequest message - exp := &tpb.RollbackReleaseRequest{ - Name: releaseName, - DryRun: dryRun, - Version: revision, - DisableHooks: disableHooks, - } - - // Options used in RollbackRelease - ops := []RollbackOption{ - RollbackDryRun(dryRun), - RollbackVersion(revision), - RollbackDisableHooks(disableHooks), - } - - // BeforeCall option to intercept Helm client RollbackReleaseRequest - b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { - switch act := msg.(type) { - case *tpb.RollbackReleaseRequest: - t.Logf("RollbackReleaseRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type RollbackReleaseRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.RollbackRelease(releaseName, ops...); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.rollbackReq.Name) -} - -// Verify each StatusOption is applied to a GetReleaseStatusRequest correctly. -func TestReleaseStatus_VerifyOptions(t *testing.T) { - // Options testdata - var releaseName = "test" - var revision = int32(2) - - // Expected GetReleaseStatusRequest message - exp := &tpb.GetReleaseStatusRequest{ - Name: releaseName, - Version: revision, - } - - // BeforeCall option to intercept Helm client GetReleaseStatusRequest - b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { - switch act := msg.(type) { - case *tpb.GetReleaseStatusRequest: - t.Logf("GetReleaseStatusRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type GetReleaseStatusRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.ReleaseStatus(releaseName, StatusReleaseVersion(revision)); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.statusReq.Name) -} - -// Verify each ContentOption is applied to a GetReleaseContentRequest correctly. -func TestReleaseContent_VerifyOptions(t *testing.T) { - // Options testdata - var releaseName = "test" - var revision = int32(2) - - // Expected GetReleaseContentRequest message - exp := &tpb.GetReleaseContentRequest{ - Name: releaseName, - Version: revision, - } - - // BeforeCall option to intercept Helm client GetReleaseContentRequest - b4c := BeforeCall(func(_ context.Context, msg proto.Message) error { - switch act := msg.(type) { - case *tpb.GetReleaseContentRequest: - t.Logf("GetReleaseContentRequest: %#+v\n", act) - assert(t, exp, act) - default: - t.Fatalf("expected message of type GetReleaseContentRequest, got %T\n", act) - } - return errSkip - }) - - client := NewClient(b4c) - if _, err := client.ReleaseContent(releaseName, ContentReleaseVersion(revision)); err != errSkip { - t.Fatalf("did not expect error but got (%v)\n``", err) - } - - // ensure options for call are not saved to client - assert(t, "", client.opts.contentReq.Name) -} - -func assert(t *testing.T, expect, actual interface{}) { - if !reflect.DeepEqual(expect, actual) { - t.Fatalf("expected %#+v, actual %#+v\n", expect, actual) - } -} - -func loadChart(t *testing.T, name string) *cpb.Chart { - c, err := chartutil.Load(filepath.Join(chartsDir, name)) - if err != nil { - t.Fatalf("failed to load test chart (%q): %s\n", name, err) - } - return c -} diff --git a/pkg/helm/helmpath/helmhome.go b/pkg/helm/helmpath/helmhome.go deleted file mode 100644 index b5ec4909eb9..00000000000 --- a/pkg/helm/helmpath/helmhome.go +++ /dev/null @@ -1,103 +0,0 @@ -/* -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 helmpath - -import ( - "fmt" - "os" - "path/filepath" -) - -// Home describes the location of a CLI configuration. -// -// This helper builds paths relative to a Helm Home directory. -type Home string - -// String returns Home as a string. -// -// Implements fmt.Stringer. -func (h Home) String() string { - return os.ExpandEnv(string(h)) -} - -// Path returns Home with elements appended. -func (h Home) Path(elem ...string) string { - p := []string{h.String()} - p = append(p, elem...) - return filepath.Join(p...) -} - -// Repository returns the path to the local repository. -func (h Home) Repository() string { - return h.Path("repository") -} - -// RepositoryFile returns the path to the repositories.yaml file. -func (h Home) RepositoryFile() string { - return h.Path("repository", "repositories.yaml") -} - -// Cache returns the path to the local cache. -func (h Home) Cache() string { - return h.Path("repository", "cache") -} - -// CacheIndex returns the path to an index for the given named repository. -func (h Home) CacheIndex(name string) string { - target := fmt.Sprintf("%s-index.yaml", name) - return h.Path("repository", "cache", target) -} - -// Starters returns the path to the Helm starter packs. -func (h Home) Starters() string { - return h.Path("starters") -} - -// LocalRepository returns the location to the local repo. -// -// The local repo is the one used by 'helm serve' -// -// If additional path elements are passed, they are appended to the returned path. -func (h Home) LocalRepository(elem ...string) string { - p := []string{"repository", "local"} - p = append(p, elem...) - return h.Path(p...) -} - -// Plugins returns the path to the plugins directory. -func (h Home) Plugins() string { - return h.Path("plugins") -} - -// Archive returns the path to download chart archives. -func (h Home) Archive() string { - return h.Path("cache", "archive") -} - -// TLSCaCert returns the path to fetch the CA certificate. -func (h Home) TLSCaCert() string { - return h.Path("ca.pem") -} - -// TLSCert returns the path to fetch the client certificate. -func (h Home) TLSCert() string { - return h.Path("cert.pem") -} - -// TLSKey returns the path to fetch the client public key. -func (h Home) TLSKey() string { - return h.Path("key.pem") -} diff --git a/pkg/helm/helmpath/helmhome_unix_test.go b/pkg/helm/helmpath/helmhome_unix_test.go deleted file mode 100644 index 494d0f6b493..00000000000 --- a/pkg/helm/helmpath/helmhome_unix_test.go +++ /dev/null @@ -1,50 +0,0 @@ -// 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. - -// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris -// +build !windows - -package helmpath - -import ( - "runtime" - "testing" -) - -func TestHelmHome(t *testing.T) { - hh := Home("/r") - isEq := func(t *testing.T, a, b string) { - if a != b { - t.Error(runtime.GOOS) - t.Errorf("Expected %q, got %q", a, b) - } - } - - isEq(t, hh.String(), "/r") - isEq(t, hh.Repository(), "/r/repository") - isEq(t, hh.RepositoryFile(), "/r/repository/repositories.yaml") - isEq(t, hh.LocalRepository(), "/r/repository/local") - isEq(t, hh.Cache(), "/r/repository/cache") - isEq(t, hh.CacheIndex("t"), "/r/repository/cache/t-index.yaml") - isEq(t, hh.Starters(), "/r/starters") - isEq(t, hh.Archive(), "/r/cache/archive") - isEq(t, hh.TLSCaCert(), "/r/ca.pem") - isEq(t, hh.TLSCert(), "/r/cert.pem") - isEq(t, hh.TLSKey(), "/r/key.pem") -} - -func TestHelmHome_expand(t *testing.T) { - if Home("$HOME").String() == "$HOME" { - t.Error("expected variable expansion") - } -} diff --git a/pkg/helm/helmpath/helmhome_windows_test.go b/pkg/helm/helmpath/helmhome_windows_test.go deleted file mode 100644 index e416bfd5880..00000000000 --- a/pkg/helm/helmpath/helmhome_windows_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// 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. - -// +build windows - -package helmpath - -import ( - "testing" -) - -func TestHelmHome(t *testing.T) { - hh := Home("r:\\") - isEq := func(t *testing.T, a, b string) { - if a != b { - t.Errorf("Expected %q, got %q", b, a) - } - } - - isEq(t, hh.String(), "r:\\") - isEq(t, hh.Repository(), "r:\\repository") - isEq(t, hh.RepositoryFile(), "r:\\repository\\repositories.yaml") - isEq(t, hh.LocalRepository(), "r:\\repository\\local") - isEq(t, hh.Cache(), "r:\\repository\\cache") - isEq(t, hh.CacheIndex("t"), "r:\\repository\\cache\\t-index.yaml") - isEq(t, hh.Starters(), "r:\\starters") - isEq(t, hh.Archive(), "r:\\cache\\archive") - isEq(t, hh.TLSCaCert(), "r:\\ca.pem") - isEq(t, hh.TLSCert(), "r:\\cert.pem") - isEq(t, hh.TLSKey(), "r:\\key.pem") -} diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go deleted file mode 100644 index 10c04c71089..00000000000 --- a/pkg/helm/interface.go +++ /dev/null @@ -1,39 +0,0 @@ -/* -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 ( - "k8s.io/helm/pkg/proto/hapi/chart" - 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, namespace string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) - InstallReleaseFromChart(chart *chart.Chart, namespace string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) - DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) - ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) - UpdateRelease(rlsName, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) - UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) - RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) - ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) - ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error) - GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) - RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error) - PingTiller() error -} diff --git a/pkg/helm/option.go b/pkg/helm/option.go deleted file mode 100644 index 3381e3f80fc..00000000000 --- a/pkg/helm/option.go +++ /dev/null @@ -1,444 +0,0 @@ -/* -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 ( - "crypto/tls" - "time" - - "github.com/golang/protobuf/proto" - "golang.org/x/net/context" - "google.golang.org/grpc/metadata" - - cpb "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/proto/hapi/release" - rls "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/version" -) - -// Option allows specifying various settings configurable by -// the helm client user for overriding the defaults used when -// issuing rpc's to the Tiller release server. -type Option func(*options) - -// options specify optional settings used by the helm client. -type options struct { - // value of helm home override - host string - // if set dry-run helm client calls - dryRun bool - // if set enable TLS on helm client calls - useTLS bool - // if set, re-use an existing name - reuseName bool - // if set, performs pod restart during upgrade/rollback - recreate bool - // if set, force resource update through delete/recreate if needed - force bool - // if set, skip running hooks - disableHooks bool - // name of release - releaseName string - // tls.Config to use for rpc if tls enabled - tlsConfig *tls.Config - // release list options are applied directly to the list releases request - listReq rls.ListReleasesRequest - // release install options are applied directly to the install release request - instReq rls.InstallReleaseRequest - // release update options are applied directly to the update release request - updateReq rls.UpdateReleaseRequest - // release uninstall options are applied directly to the uninstall release request - uninstallReq rls.UninstallReleaseRequest - // release get status options are applied directly to the get release status request - statusReq rls.GetReleaseStatusRequest - // release get content options are applied directly to the get release content request - contentReq rls.GetReleaseContentRequest - // release rollback options are applied directly to the rollback release request - rollbackReq rls.RollbackReleaseRequest - // before intercepts client calls before sending - before func(context.Context, proto.Message) error - // release history options are applied directly to the get release history request - histReq rls.GetHistoryRequest - // resetValues instructs Tiller to reset values to their defaults. - resetValues bool - // reuseValues instructs Tiller to reuse the values from the last release. - reuseValues bool - // release test options are applied directly to the test release history request - testReq rls.TestReleaseRequest - // connectTimeout specifies the time duration Helm will wait to establish a connection to tiller - connectTimeout time.Duration -} - -// Host specifies the host address of the Tiller release server, (default = ":44134"). -func Host(host string) Option { - return func(opts *options) { - opts.host = host - } -} - -// WithTLS specifies the tls configuration if the helm client is enabled to use TLS. -func WithTLS(cfg *tls.Config) Option { - return func(opts *options) { - opts.useTLS = true - opts.tlsConfig = cfg - } -} - -// BeforeCall returns an option that allows intercepting a helm client rpc -// before being sent OTA to tiller. The intercepting function should return -// an error to indicate that the call should not proceed or nil otherwise. -func BeforeCall(fn func(context.Context, proto.Message) error) Option { - return func(opts *options) { - opts.before = fn - } -} - -// ReleaseListOption allows specifying various settings -// configurable by the helm client user for overriding -// the defaults used when running the `helm list` command. -type ReleaseListOption func(*options) - -// ReleaseListOffset specifies the offset into a list of releases. -func ReleaseListOffset(offset string) ReleaseListOption { - return func(opts *options) { - opts.listReq.Offset = offset - } -} - -// ReleaseListFilter specifies a filter to apply a list of releases. -func ReleaseListFilter(filter string) ReleaseListOption { - return func(opts *options) { - opts.listReq.Filter = filter - } -} - -// ReleaseListLimit set an upper bound on the number of releases returned. -func ReleaseListLimit(limit int) ReleaseListOption { - return func(opts *options) { - opts.listReq.Limit = int64(limit) - } -} - -// ReleaseListOrder specifies how to order a list of releases. -func ReleaseListOrder(order int32) ReleaseListOption { - return func(opts *options) { - opts.listReq.SortOrder = rls.ListSort_SortOrder(order) - } -} - -// ReleaseListSort specifies how to sort a release list. -func ReleaseListSort(sort int32) ReleaseListOption { - return func(opts *options) { - opts.listReq.SortBy = rls.ListSort_SortBy(sort) - } -} - -// ReleaseListStatuses specifies which status codes should be returned. -func ReleaseListStatuses(statuses []release.Status_Code) ReleaseListOption { - return func(opts *options) { - if len(statuses) == 0 { - statuses = []release.Status_Code{release.Status_DEPLOYED} - } - opts.listReq.StatusCodes = statuses - } -} - -// ReleaseListNamespace specifies the namespace to list releases from -func ReleaseListNamespace(namespace string) ReleaseListOption { - return func(opts *options) { - opts.listReq.Namespace = namespace - } -} - -// InstallOption allows specifying various settings -// configurable by the helm client user for overriding -// the defaults used when running the `helm install` command. -type InstallOption func(*options) - -// ValueOverrides specifies a list of values to include when installing. -func ValueOverrides(raw []byte) InstallOption { - return func(opts *options) { - opts.instReq.Values = &cpb.Config{Raw: string(raw)} - } -} - -// ReleaseName specifies the name of the release when installing. -func ReleaseName(name string) InstallOption { - return func(opts *options) { - opts.instReq.Name = name - } -} - -// ConnectTimeout specifies the duration (in seconds) Helm will wait to establish a connection to tiller -func ConnectTimeout(timeout int64) Option { - return func(opts *options) { - opts.connectTimeout = time.Duration(timeout) * time.Second - } -} - -// InstallTimeout specifies the number of seconds before kubernetes calls timeout -func InstallTimeout(timeout int64) InstallOption { - return func(opts *options) { - opts.instReq.Timeout = timeout - } -} - -// UpgradeTimeout specifies the number of seconds before kubernetes calls timeout -func UpgradeTimeout(timeout int64) UpdateOption { - return func(opts *options) { - opts.updateReq.Timeout = timeout - } -} - -// DeleteTimeout specifies the number of seconds before kubernetes calls timeout -func DeleteTimeout(timeout int64) DeleteOption { - return func(opts *options) { - opts.uninstallReq.Timeout = timeout - } -} - -// ReleaseTestTimeout specifies the number of seconds before kubernetes calls timeout -func ReleaseTestTimeout(timeout int64) ReleaseTestOption { - return func(opts *options) { - opts.testReq.Timeout = timeout - } -} - -// ReleaseTestCleanup is a boolean value representing whether to cleanup test pods -func ReleaseTestCleanup(cleanup bool) ReleaseTestOption { - return func(opts *options) { - opts.testReq.Cleanup = cleanup - } -} - -// RollbackTimeout specifies the number of seconds before kubernetes calls timeout -func RollbackTimeout(timeout int64) RollbackOption { - return func(opts *options) { - opts.rollbackReq.Timeout = timeout - } -} - -// InstallWait specifies whether or not to wait for all resources to be ready -func InstallWait(wait bool) InstallOption { - return func(opts *options) { - opts.instReq.Wait = wait - } -} - -// UpgradeWait specifies whether or not to wait for all resources to be ready -func UpgradeWait(wait bool) UpdateOption { - return func(opts *options) { - opts.updateReq.Wait = wait - } -} - -// RollbackWait specifies whether or not to wait for all resources to be ready -func RollbackWait(wait bool) RollbackOption { - return func(opts *options) { - opts.rollbackReq.Wait = wait - } -} - -// UpdateValueOverrides specifies a list of values to include when upgrading -func UpdateValueOverrides(raw []byte) UpdateOption { - return func(opts *options) { - opts.updateReq.Values = &cpb.Config{Raw: string(raw)} - } -} - -// DeleteDisableHooks will disable hooks for a deletion operation. -func DeleteDisableHooks(disable bool) DeleteOption { - return func(opts *options) { - opts.disableHooks = disable - } -} - -// DeleteDryRun will (if true) execute a deletion as a dry run. -func DeleteDryRun(dry bool) DeleteOption { - return func(opts *options) { - opts.dryRun = dry - } -} - -// DeletePurge removes the release from the store and make its name free for later use. -func DeletePurge(purge bool) DeleteOption { - return func(opts *options) { - opts.uninstallReq.Purge = purge - } -} - -// InstallDryRun will (if true) execute an installation as a dry run. -func InstallDryRun(dry bool) InstallOption { - return func(opts *options) { - opts.dryRun = dry - } -} - -// InstallDisableHooks disables hooks during installation. -func InstallDisableHooks(disable bool) InstallOption { - return func(opts *options) { - opts.disableHooks = disable - } -} - -// InstallReuseName will (if true) instruct Tiller to re-use an existing name. -func InstallReuseName(reuse bool) InstallOption { - return func(opts *options) { - opts.reuseName = reuse - } -} - -// RollbackDisableHooks will disable hooks for a rollback operation -func RollbackDisableHooks(disable bool) RollbackOption { - return func(opts *options) { - opts.disableHooks = disable - } -} - -// RollbackDryRun will (if true) execute a rollback as a dry run. -func RollbackDryRun(dry bool) RollbackOption { - return func(opts *options) { - opts.dryRun = dry - } -} - -// RollbackRecreate will (if true) recreate pods after rollback. -func RollbackRecreate(recreate bool) RollbackOption { - return func(opts *options) { - opts.recreate = recreate - } -} - -// RollbackForce will (if true) force resource update through delete/recreate if needed -func RollbackForce(force bool) RollbackOption { - return func(opts *options) { - opts.force = force - } -} - -// RollbackVersion sets the version of the release to deploy. -func RollbackVersion(ver int32) RollbackOption { - return func(opts *options) { - opts.rollbackReq.Version = ver - } -} - -// UpgradeDisableHooks will disable hooks for an upgrade operation. -func UpgradeDisableHooks(disable bool) UpdateOption { - return func(opts *options) { - opts.disableHooks = disable - } -} - -// UpgradeDryRun will (if true) execute an upgrade as a dry run. -func UpgradeDryRun(dry bool) UpdateOption { - return func(opts *options) { - opts.dryRun = dry - } -} - -// ResetValues will (if true) trigger resetting the values to their original state. -func ResetValues(reset bool) UpdateOption { - return func(opts *options) { - opts.resetValues = reset - } -} - -// ReuseValues will cause Tiller to reuse the values from the last release. -// This is ignored if ResetValues is true. -func ReuseValues(reuse bool) UpdateOption { - return func(opts *options) { - opts.reuseValues = reuse - } -} - -// UpgradeRecreate will (if true) recreate pods after upgrade. -func UpgradeRecreate(recreate bool) UpdateOption { - return func(opts *options) { - opts.recreate = recreate - } -} - -// UpgradeForce will (if true) force resource update through delete/recreate if needed -func UpgradeForce(force bool) UpdateOption { - return func(opts *options) { - opts.force = force - } -} - -// ContentOption allows setting optional attributes when -// performing a GetReleaseContent tiller rpc. -type ContentOption func(*options) - -// ContentReleaseVersion will instruct Tiller to retrieve the content -// of a particular version of a release. -func ContentReleaseVersion(version int32) ContentOption { - return func(opts *options) { - opts.contentReq.Version = version - } -} - -// StatusOption allows setting optional attributes when -// performing a GetReleaseStatus tiller rpc. -type StatusOption func(*options) - -// StatusReleaseVersion will instruct Tiller to retrieve the status -// of a particular version of a release. -func StatusReleaseVersion(version int32) StatusOption { - return func(opts *options) { - opts.statusReq.Version = version - } -} - -// DeleteOption allows setting optional attributes when -// performing a UninstallRelease tiller rpc. -type DeleteOption func(*options) - -// VersionOption -- TODO -type VersionOption func(*options) - -// UpdateOption allows specifying various settings -// configurable by the helm client user for overriding -// the defaults used when running the `helm upgrade` command. -type UpdateOption func(*options) - -// RollbackOption allows specififying various settings configurable -// by the helm client user for overriding the defaults used when -// running the `helm rollback` command. -type RollbackOption func(*options) - -// HistoryOption allows configuring optional request data for -// issuing a GetHistory rpc. -type HistoryOption func(*options) - -// WithMaxHistory sets the max number of releases to return -// in a release history query. -func WithMaxHistory(max int32) HistoryOption { - return func(opts *options) { - opts.histReq.Max = max - } -} - -// NewContext creates a versioned context. -func NewContext() context.Context { - md := metadata.Pairs("x-helm-api-client", version.GetVersion()) - return metadata.NewOutgoingContext(context.TODO(), md) -} - -// ReleaseTestOption allows configuring optional request data for -// issuing a TestRelease rpc. -type ReleaseTestOption func(*options) diff --git a/pkg/helm/portforwarder/pod.go b/pkg/helm/portforwarder/pod.go deleted file mode 100644 index 7c235520464..00000000000 --- a/pkg/helm/portforwarder/pod.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -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 portforwarder - -import ( - "k8s.io/api/core/v1" -) - -// These functions are adapted from the "kubernetes" repository's file -// -// kubernetes/pkg/api/v1/pod/util.go -// -// where they rely upon the API types specific to that repository. Here we recast them to operate -// upon the type from the "client-go" repository instead. - -// isPodReady returns true if a pod is ready; false otherwise. -func isPodReady(pod *v1.Pod) bool { - return isPodReadyConditionTrue(pod.Status) -} - -// isPodReady retruns true if a pod is ready; false otherwise. -func isPodReadyConditionTrue(status v1.PodStatus) bool { - condition := getPodReadyCondition(status) - return condition != nil && condition.Status == v1.ConditionTrue -} - -// getPodReadyCondition extracts the pod ready condition from the given status and returns that. -// Returns nil if the condition is not present. -func getPodReadyCondition(status v1.PodStatus) *v1.PodCondition { - _, condition := getPodCondition(&status, v1.PodReady) - return condition -} - -// getPodCondition extracts the provided condition from the given status and returns that. -// Returns nil and -1 if the condition is not present, and the index of the located condition. -func getPodCondition(status *v1.PodStatus, conditionType v1.PodConditionType) (int, *v1.PodCondition) { - if status == nil { - return -1, nil - } - for i := range status.Conditions { - if status.Conditions[i].Type == conditionType { - return i, &status.Conditions[i] - } - } - return -1, nil -} diff --git a/pkg/helm/portforwarder/portforwarder.go b/pkg/helm/portforwarder/portforwarder.go deleted file mode 100644 index 878610d5f25..00000000000 --- a/pkg/helm/portforwarder/portforwarder.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -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 portforwarder - -import ( - "fmt" - - "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/client-go/kubernetes" - corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - "k8s.io/client-go/rest" - - "k8s.io/helm/pkg/kube" -) - -var ( - tillerPodLabels = labels.Set{"app": "helm", "name": "tiller"} -) - -// New creates a new and initialized tunnel. -func New(namespace string, client kubernetes.Interface, config *rest.Config) (*kube.Tunnel, error) { - podName, err := GetTillerPodName(client.CoreV1(), namespace) - if err != nil { - return nil, err - } - const tillerPort = 44134 - t := kube.NewTunnel(client.CoreV1().RESTClient(), config, namespace, podName, tillerPort) - return t, t.ForwardPort() -} - -// GetTillerPodName fetches the name of tiller pod running in the given namespace. -func GetTillerPodName(client corev1.PodsGetter, namespace string) (string, error) { - selector := tillerPodLabels.AsSelector() - pod, err := getFirstRunningPod(client, namespace, selector) - if err != nil { - return "", err - } - return pod.ObjectMeta.GetName(), nil -} - -func getFirstRunningPod(client corev1.PodsGetter, namespace string, selector labels.Selector) (*v1.Pod, error) { - options := metav1.ListOptions{LabelSelector: selector.String()} - pods, err := client.Pods(namespace).List(options) - if err != nil { - return nil, err - } - if len(pods.Items) < 1 { - return nil, fmt.Errorf("could not find tiller") - } - for _, p := range pods.Items { - if isPodReady(&p) { - return &p, nil - } - } - return nil, fmt.Errorf("could not find a ready tiller pod") -} diff --git a/pkg/helm/portforwarder/portforwarder_test.go b/pkg/helm/portforwarder/portforwarder_test.go deleted file mode 100644 index e4c14899158..00000000000 --- a/pkg/helm/portforwarder/portforwarder_test.go +++ /dev/null @@ -1,87 +0,0 @@ -/* -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 portforwarder - -import ( - "testing" - - "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" -) - -func mockTillerPod() v1.Pod { - return v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "orca", - Namespace: v1.NamespaceDefault, - Labels: tillerPodLabels, - }, - Status: v1.PodStatus{ - Phase: v1.PodRunning, - Conditions: []v1.PodCondition{ - { - Status: v1.ConditionTrue, - Type: v1.PodReady, - }, - }, - }, - } -} - -func mockTillerPodPending() v1.Pod { - p := mockTillerPod() - p.Name = "blue" - p.Status.Conditions[0].Status = v1.ConditionFalse - return p -} - -func TestGetFirstPod(t *testing.T) { - tests := []struct { - name string - pods []v1.Pod - expected string - err bool - }{ - { - name: "with a ready pod", - pods: []v1.Pod{mockTillerPod()}, - expected: "orca", - }, - { - name: "without a ready pod", - pods: []v1.Pod{mockTillerPodPending()}, - err: true, - }, - { - name: "without a pod", - pods: []v1.Pod{}, - err: true, - }, - } - - for _, tt := range tests { - client := fake.NewSimpleClientset(&v1.PodList{Items: tt.pods}) - name, err := GetTillerPodName(client.Core(), v1.NamespaceDefault) - if (err != nil) != tt.err { - t.Errorf("%q. expected error: %v, got %v", tt.name, tt.err, err) - } - if name != tt.expected { - t.Errorf("%q. expected %q, got %q", tt.name, tt.expected, name) - } - } -} diff --git a/pkg/helmpath/home.go b/pkg/helmpath/home.go new file mode 100644 index 00000000000..bd43e8890c8 --- /dev/null +++ b/pkg/helmpath/home.go @@ -0,0 +1,44 @@ +// Copyright The Helm Authors. +// 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 helmpath calculates filesystem paths to Helm's configuration, cache and data. +package helmpath + +// This helper builds paths to Helm's configuration, cache and data paths. +const lp = lazypath("helm") + +// ConfigPath returns the path where Helm stores configuration. +func ConfigPath(elem ...string) string { return lp.configPath(elem...) } + +// CachePath returns the path where Helm stores cached objects. +func CachePath(elem ...string) string { return lp.cachePath(elem...) } + +// DataPath returns the path where Helm stores data. +func DataPath(elem ...string) string { return lp.dataPath(elem...) } + +// CacheIndexFile returns the path to an index for the given named repository. +func CacheIndexFile(name string) string { + if name != "" { + name += "-" + } + return name + "index.yaml" +} + +// CacheChartsFile returns the path to a text file listing all the charts +// within the given named repository. +func CacheChartsFile(name string) string { + if name != "" { + name += "-" + } + return name + "charts.txt" +} diff --git a/pkg/helmpath/home_unix_test.go b/pkg/helmpath/home_unix_test.go new file mode 100644 index 00000000000..6a72152c49e --- /dev/null +++ b/pkg/helmpath/home_unix_test.go @@ -0,0 +1,46 @@ +// Copyright The Helm Authors. +// 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. + +// +build !windows + +package helmpath + +import ( + "os" + "runtime" + "testing" + + "helm.sh/helm/v3/pkg/helmpath/xdg" +) + +func TestHelmHome(t *testing.T) { + os.Setenv(xdg.CacheHomeEnvVar, "/cache") + os.Setenv(xdg.ConfigHomeEnvVar, "/config") + os.Setenv(xdg.DataHomeEnvVar, "/data") + isEq := func(t *testing.T, got, expected string) { + t.Helper() + if expected != got { + t.Error(runtime.GOOS) + t.Errorf("Expected %q, got %q", expected, got) + } + } + + isEq(t, CachePath(), "/cache/helm") + isEq(t, ConfigPath(), "/config/helm") + isEq(t, DataPath(), "/data/helm") + + // test to see if lazy-loading environment variables at runtime works + os.Setenv(xdg.CacheHomeEnvVar, "/cache2") + + isEq(t, CachePath(), "/cache2/helm") +} diff --git a/pkg/helmpath/home_windows_test.go b/pkg/helmpath/home_windows_test.go new file mode 100644 index 00000000000..796ced62c35 --- /dev/null +++ b/pkg/helmpath/home_windows_test.go @@ -0,0 +1,43 @@ +// Copyright The Helm Authors. +// 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. + +// +build windows + +package helmpath + +import ( + "os" + "testing" + + "helm.sh/helm/v3/pkg/helmpath/xdg" +) + +func TestHelmHome(t *testing.T) { + os.Setenv(xdg.CacheHomeEnvVar, "c:\\") + os.Setenv(xdg.ConfigHomeEnvVar, "d:\\") + os.Setenv(xdg.DataHomeEnvVar, "e:\\") + isEq := func(t *testing.T, a, b string) { + if a != b { + t.Errorf("Expected %q, got %q", b, a) + } + } + + isEq(t, CachePath(), "c:\\helm") + isEq(t, ConfigPath(), "d:\\helm") + isEq(t, DataPath(), "e:\\helm") + + // test to see if lazy-loading environment variables at runtime works + os.Setenv(xdg.CacheHomeEnvVar, "f:\\") + + isEq(t, CachePath(), "f:\\helm") +} diff --git a/pkg/helmpath/lazypath.go b/pkg/helmpath/lazypath.go new file mode 100644 index 00000000000..22d7bf0a1b8 --- /dev/null +++ b/pkg/helmpath/lazypath.go @@ -0,0 +1,72 @@ +// Copyright The Helm Authors. +// 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 helmpath + +import ( + "os" + "path/filepath" + + "helm.sh/helm/v3/pkg/helmpath/xdg" +) + +const ( + // CacheHomeEnvVar is the environment variable used by Helm + // for the cache directory. When no value is set a default is used. + CacheHomeEnvVar = "HELM_CACHE_HOME" + + // ConfigHomeEnvVar is the environment variable used by Helm + // for the config directory. When no value is set a default is used. + ConfigHomeEnvVar = "HELM_CONFIG_HOME" + + // DataHomeEnvVar is the environment variable used by Helm + // for the data directory. When no value is set a default is used. + DataHomeEnvVar = "HELM_DATA_HOME" +) + +// lazypath is an lazy-loaded path buffer for the XDG base directory specification. +type lazypath string + +func (l lazypath) path(helmEnvVar, xdgEnvVar string, defaultFn func() string, elem ...string) string { + + // There is an order to checking for a path. + // 1. See if a Helm specific environment variable has been set. + // 2. Check if an XDG environment variable is set + // 3. Fall back to a default + base := os.Getenv(helmEnvVar) + if base != "" { + return filepath.Join(base, filepath.Join(elem...)) + } + base = os.Getenv(xdgEnvVar) + if base == "" { + base = defaultFn() + } + return filepath.Join(base, string(l), filepath.Join(elem...)) +} + +// cachePath defines the base directory relative to which user specific non-essential data files +// should be stored. +func (l lazypath) cachePath(elem ...string) string { + return l.path(CacheHomeEnvVar, xdg.CacheHomeEnvVar, cacheHome, filepath.Join(elem...)) +} + +// configPath defines the base directory relative to which user specific configuration files should +// be stored. +func (l lazypath) configPath(elem ...string) string { + return l.path(ConfigHomeEnvVar, xdg.ConfigHomeEnvVar, configHome, filepath.Join(elem...)) +} + +// dataPath defines the base directory relative to which user specific data files should be stored. +func (l lazypath) dataPath(elem ...string) string { + return l.path(DataHomeEnvVar, xdg.DataHomeEnvVar, dataHome, filepath.Join(elem...)) +} diff --git a/pkg/helmpath/lazypath_darwin.go b/pkg/helmpath/lazypath_darwin.go new file mode 100644 index 00000000000..e112b8337e4 --- /dev/null +++ b/pkg/helmpath/lazypath_darwin.go @@ -0,0 +1,34 @@ +// Copyright The Helm Authors. +// 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. + +// +build darwin + +package helmpath + +import ( + "path/filepath" + + "k8s.io/client-go/util/homedir" +) + +func dataHome() string { + return filepath.Join(homedir.HomeDir(), "Library") +} + +func configHome() string { + return filepath.Join(homedir.HomeDir(), "Library", "Preferences") +} + +func cacheHome() string { + return filepath.Join(homedir.HomeDir(), "Library", "Caches") +} diff --git a/pkg/helmpath/lazypath_darwin_test.go b/pkg/helmpath/lazypath_darwin_test.go new file mode 100644 index 00000000000..9381a44e258 --- /dev/null +++ b/pkg/helmpath/lazypath_darwin_test.go @@ -0,0 +1,86 @@ +// Copyright The Helm Authors. +// 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. + +// +build darwin + +package helmpath + +import ( + "os" + "path/filepath" + "testing" + + "k8s.io/client-go/util/homedir" + + "helm.sh/helm/v3/pkg/helmpath/xdg" +) + +const ( + appName = "helm" + testFile = "test.txt" + lazy = lazypath(appName) +) + +func TestDataPath(t *testing.T) { + os.Unsetenv(xdg.DataHomeEnvVar) + + expected := filepath.Join(homedir.HomeDir(), "Library", appName, testFile) + + if lazy.dataPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) + } + + os.Setenv(xdg.DataHomeEnvVar, "/tmp") + + expected = filepath.Join("/tmp", appName, testFile) + + if lazy.dataPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) + } +} + +func TestConfigPath(t *testing.T) { + os.Unsetenv(xdg.ConfigHomeEnvVar) + + expected := filepath.Join(homedir.HomeDir(), "Library", "Preferences", appName, testFile) + + if lazy.configPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) + } + + os.Setenv(xdg.ConfigHomeEnvVar, "/tmp") + + expected = filepath.Join("/tmp", appName, testFile) + + if lazy.configPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) + } +} + +func TestCachePath(t *testing.T) { + os.Unsetenv(xdg.CacheHomeEnvVar) + + expected := filepath.Join(homedir.HomeDir(), "Library", "Caches", appName, testFile) + + if lazy.cachePath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) + } + + os.Setenv(xdg.CacheHomeEnvVar, "/tmp") + + expected = filepath.Join("/tmp", appName, testFile) + + if lazy.cachePath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) + } +} diff --git a/pkg/helmpath/lazypath_unix.go b/pkg/helmpath/lazypath_unix.go new file mode 100644 index 00000000000..b4eae9f664e --- /dev/null +++ b/pkg/helmpath/lazypath_unix.go @@ -0,0 +1,45 @@ +// Copyright The Helm Authors. +// 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. + +// +build !windows,!darwin + +package helmpath + +import ( + "path/filepath" + + "k8s.io/client-go/util/homedir" +) + +// dataHome defines the base directory relative to which user specific data files should be stored. +// +// If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share is used. +func dataHome() string { + return filepath.Join(homedir.HomeDir(), ".local", "share") +} + +// configHome defines the base directory relative to which user specific configuration files should +// be stored. +// +// If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config is used. +func configHome() string { + return filepath.Join(homedir.HomeDir(), ".config") +} + +// cacheHome defines the base directory relative to which user specific non-essential data files +// should be stored. +// +// If $XDG_CACHE_HOME is either not set or empty, a default equal to $HOME/.cache is used. +func cacheHome() string { + return filepath.Join(homedir.HomeDir(), ".cache") +} diff --git a/pkg/helmpath/lazypath_unix_test.go b/pkg/helmpath/lazypath_unix_test.go new file mode 100644 index 00000000000..96d66e7a5d7 --- /dev/null +++ b/pkg/helmpath/lazypath_unix_test.go @@ -0,0 +1,86 @@ +// Copyright The Helm Authors. +// 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. + +// +build !windows,!darwin + +package helmpath + +import ( + "os" + "path/filepath" + "testing" + + "k8s.io/client-go/util/homedir" + + "helm.sh/helm/v3/pkg/helmpath/xdg" +) + +const ( + appName = "helm" + testFile = "test.txt" + lazy = lazypath(appName) +) + +func TestDataPath(t *testing.T) { + os.Unsetenv(xdg.DataHomeEnvVar) + + expected := filepath.Join(homedir.HomeDir(), ".local", "share", appName, testFile) + + if lazy.dataPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) + } + + os.Setenv(xdg.DataHomeEnvVar, "/tmp") + + expected = filepath.Join("/tmp", appName, testFile) + + if lazy.dataPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) + } +} + +func TestConfigPath(t *testing.T) { + os.Unsetenv(xdg.ConfigHomeEnvVar) + + expected := filepath.Join(homedir.HomeDir(), ".config", appName, testFile) + + if lazy.configPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) + } + + os.Setenv(xdg.ConfigHomeEnvVar, "/tmp") + + expected = filepath.Join("/tmp", appName, testFile) + + if lazy.configPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) + } +} + +func TestCachePath(t *testing.T) { + os.Unsetenv(xdg.CacheHomeEnvVar) + + expected := filepath.Join(homedir.HomeDir(), ".cache", appName, testFile) + + if lazy.cachePath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) + } + + os.Setenv(xdg.CacheHomeEnvVar, "/tmp") + + expected = filepath.Join("/tmp", appName, testFile) + + if lazy.cachePath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) + } +} diff --git a/pkg/helmpath/lazypath_windows.go b/pkg/helmpath/lazypath_windows.go new file mode 100644 index 00000000000..057a3af1408 --- /dev/null +++ b/pkg/helmpath/lazypath_windows.go @@ -0,0 +1,24 @@ +// Copyright The Helm Authors. +// 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. + +// +build windows + +package helmpath + +import "os" + +func dataHome() string { return configHome() } + +func configHome() string { return os.Getenv("APPDATA") } + +func cacheHome() string { return os.Getenv("TEMP") } diff --git a/pkg/helmpath/lazypath_windows_test.go b/pkg/helmpath/lazypath_windows_test.go new file mode 100644 index 00000000000..866e7b9d97f --- /dev/null +++ b/pkg/helmpath/lazypath_windows_test.go @@ -0,0 +1,89 @@ +// Copyright The Helm Authors. +// 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. + +// +build windows + +package helmpath + +import ( + "os" + "path/filepath" + "testing" + + "k8s.io/client-go/util/homedir" + + "helm.sh/helm/v3/pkg/helmpath/xdg" +) + +const ( + appName = "helm" + testFile = "test.txt" + lazy = lazypath(appName) +) + +func TestDataPath(t *testing.T) { + os.Unsetenv(xdg.DataHomeEnvVar) + os.Setenv("APPDATA", filepath.Join(homedir.HomeDir(), "foo")) + + expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile) + + if lazy.dataPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) + } + + os.Setenv(xdg.DataHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg")) + + expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile) + + if lazy.dataPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) + } +} + +func TestConfigPath(t *testing.T) { + os.Unsetenv(xdg.ConfigHomeEnvVar) + os.Setenv("APPDATA", filepath.Join(homedir.HomeDir(), "foo")) + + expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile) + + if lazy.configPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) + } + + os.Setenv(xdg.ConfigHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg")) + + expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile) + + if lazy.configPath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) + } +} + +func TestCachePath(t *testing.T) { + os.Unsetenv(xdg.CacheHomeEnvVar) + os.Setenv("TEMP", filepath.Join(homedir.HomeDir(), "foo")) + + expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile) + + if lazy.cachePath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) + } + + os.Setenv(xdg.CacheHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg")) + + expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile) + + if lazy.cachePath(testFile) != expected { + t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) + } +} diff --git a/pkg/helmpath/xdg/xdg.go b/pkg/helmpath/xdg/xdg.go new file mode 100644 index 00000000000..eaa3e686434 --- /dev/null +++ b/pkg/helmpath/xdg/xdg.go @@ -0,0 +1,34 @@ +/* +Copyright The Helm Authors. +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 xdg holds constants pertaining to XDG Base Directory Specification. +// +// The XDG Base Directory Specification https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +// specifies the environment variables that define user-specific base directories for various categories of files. +package xdg + +const ( + // CacheHomeEnvVar is the environment variable used by the + // XDG base directory specification for the cache directory. + CacheHomeEnvVar = "XDG_CACHE_HOME" + + // ConfigHomeEnvVar is the environment variable used by the + // XDG base directory specification for the config directory. + ConfigHomeEnvVar = "XDG_CONFIG_HOME" + + // DataHomeEnvVar is the environment variable used by the + // XDG base directory specification for the data directory. + DataHomeEnvVar = "XDG_DATA_HOME" +) diff --git a/pkg/hooks/hooks.go b/pkg/hooks/hooks.go deleted file mode 100644 index 80f838368c7..00000000000 --- a/pkg/hooks/hooks.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -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 hooks - -import ( - "k8s.io/helm/pkg/proto/hapi/release" -) - -// HookAnno is the label name for a hook -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" - PostInstall = "post-install" - PreDelete = "pre-delete" - PostDelete = "post-delete" - PreUpgrade = "pre-upgrade" - PostUpgrade = "post-upgrade" - PreRollback = "pre-rollback" - PostRollback = "post-rollback" - ReleaseTestSuccess = "test-success" - ReleaseTestFailure = "test-failure" -) - -// Type of policy for deleting the hook -const ( - HookSucceeded = "hook-succeeded" - HookFailed = "hook-failed" - BeforeHookCreation = "before-hook-creation" -) - -// FilterTestHooks filters the list of hooks are returns only testing hooks. -func FilterTestHooks(hooks []*release.Hook) []*release.Hook { - testHooks := []*release.Hook{} - - for _, h := range hooks { - for _, e := range h.Events { - if e == release.Hook_RELEASE_TEST_SUCCESS || e == release.Hook_RELEASE_TEST_FAILURE { - testHooks = append(testHooks, h) - continue - } - } - } - - return testHooks -} diff --git a/pkg/ignore/doc.go b/pkg/ignore/doc.go deleted file mode 100644 index 7281c33a9f6..00000000000 --- a/pkg/ignore/doc.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -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 ignore provides tools for writing ignore files (a la .gitignore). - -This provides both an ignore parser and a file-aware processor. - -The format of ignore files closely follows, but does not exactly match, the -format for .gitignore files (https://git-scm.com/docs/gitignore). - -The formatting rules are as follows: - - - Parsing is line-by-line - - Empty lines are ignored - - Lines the begin with # (comments) will be ignored - - Leading and trailing spaces are always ignored - - Inline comments are NOT supported ('foo* # Any foo' does not contain a comment) - - There is no support for multi-line patterns - - Shell glob patterns are supported. See Go's "path/filepath".Match - - If a pattern begins with a leading !, the match will be negated. - - If a pattern begins with a leading /, only paths relatively rooted will match. - - If the pattern ends with a trailing /, only directories will match - - If a pattern contains no slashes, file basenames are tested (not paths) - - The pattern sequence "**", while legal in a glob, will cause an error here - (to indicate incompatibility with .gitignore). - -Example: - - # Match any file named foo.txt - foo.txt - - # Match any text file - *.txt - - # Match only directories named mydir - mydir/ - - # Match only text files in the top-level directory - /*.txt - - # Match only the file foo.txt in the top-level directory - /foo.txt - - # Match any file named ab.txt, ac.txt, or ad.txt - a[b-d].txt - -Notable differences from .gitignore: - - The '**' syntax is not supported. - - The globbing library is Go's 'filepath.Match', not fnmatch(3) - - Trailing spaces are always ignored (there is no supported escape sequence) - - The evaluation of escape sequences has not been tested for compatibility - - There is no support for '\!' as a special leading sequence. -*/ -package ignore // import "k8s.io/helm/pkg/ignore" diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 8a740293829..34079e7a021 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,331 +14,313 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "k8s.io/helm/pkg/kube" +package kube // import "helm.sh/helm/v3/pkg/kube" import ( - "bytes" + "context" "encoding/json" - goerrors "errors" "fmt" "io" - "log" "strings" + "sync" "time" jsonpatch "github.com/evanphx/json-patch" - appsv1 "k8s.io/api/apps/v1" - appsv1beta1 "k8s.io/api/apps/v1beta1" - appsv1beta2 "k8s.io/api/apps/v1beta2" + "github.com/pkg/errors" batch "k8s.io/api/batch/v1" - "k8s.io/api/core/v1" - extv1beta1 "k8s.io/api/extensions/v1beta1" - apiequality "k8s.io/apimachinery/pkg/api/equality" - "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/api/core/v1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/tools/clientcmd" - batchinternal "k8s.io/kubernetes/pkg/apis/batch" - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/client/conditions" - "k8s.io/kubernetes/pkg/kubectl" - cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" - "k8s.io/kubernetes/pkg/kubectl/resource" - "k8s.io/kubernetes/pkg/kubectl/validation" - "k8s.io/kubernetes/pkg/printers" -) - -const ( - // MissingGetHeader is added to Get's output when a resource is not found. - MissingGetHeader = "==> MISSING\nKIND\t\tNAME\n" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + cachetools "k8s.io/client-go/tools/cache" + watchtools "k8s.io/client-go/tools/watch" + cmdutil "k8s.io/kubectl/pkg/cmd/util" ) // ErrNoObjectsVisited indicates that during a visit operation, no matching objects were found. -var ErrNoObjectsVisited = goerrors.New("no objects visited") +var ErrNoObjectsVisited = errors.New("no objects visited") + +var metadataAccessor = meta.NewAccessor() // Client represents a client capable of communicating with the Kubernetes API. type Client struct { - cmdutil.Factory - // SchemaCacheDir is the path for loading cached schema. - SchemaCacheDir string + Factory Factory + Log func(string, ...interface{}) + // Namespace allows to bypass the kubeconfig file for the choice of the namespace + Namespace string - Log func(string, ...interface{}) + kubeClient *kubernetes.Clientset } +var addToScheme sync.Once + // New creates a new Client. -func New(config clientcmd.ClientConfig) *Client { +func New(getter genericclioptions.RESTClientGetter) *Client { + if getter == nil { + getter = genericclioptions.NewConfigFlags(true) + } + // Add CRDs to the scheme. They are missing by default. + addToScheme.Do(func() { + if err := apiextv1.AddToScheme(scheme.Scheme); err != nil { + // This should never happen. + panic(err) + } + if err := apiextv1beta1.AddToScheme(scheme.Scheme); err != nil { + panic(err) + } + }) return &Client{ - Factory: cmdutil.NewFactory(config), - SchemaCacheDir: clientcmd.RecommendedSchemaFile, - Log: nopLogger, + Factory: cmdutil.NewFactory(getter), + Log: nopLogger, } } var nopLogger = func(_ string, _ ...interface{}) {} -// ResourceActorFunc performs an action on a single resource. -type ResourceActorFunc func(*resource.Info) error - -// Create creates Kubernetes resources from an io.reader. -// -// Namespace will set the namespace. -func (c *Client) Create(namespace string, reader io.Reader, timeout int64, shouldWait bool) error { - client, err := c.ClientSet() - if err != nil { - return err +// getKubeClient get or create a new KubernetesClientSet +func (c *Client) getKubeClient() (*kubernetes.Clientset, error) { + var err error + if c.kubeClient == nil { + c.kubeClient, err = c.Factory.KubernetesClientSet() } - if err := ensureNamespace(client, namespace); err != nil { - return err - } - c.Log("building resources from manifest") - infos, buildErr := c.BuildUnstructured(namespace, reader) - if buildErr != nil { - return buildErr + + return c.kubeClient, err +} + +// IsReachable tests connectivity to the cluster +func (c *Client) IsReachable() error { + client, err := c.getKubeClient() + if err == genericclioptions.ErrEmptyConfig { + // re-replace kubernetes ErrEmptyConfig error with a friendy error + // moar workarounds for Kubernetes API breaking. + return errors.New("Kubernetes cluster unreachable") } - c.Log("creating %d resource(s)", len(infos)) - if err := perform(infos, createResource); err != nil { - return err + if err != nil { + return errors.Wrap(err, "Kubernetes cluster unreachable") } - if shouldWait { - return c.waitForResources(time.Duration(timeout)*time.Second, infos) + if _, err := client.ServerVersion(); err != nil { + return errors.Wrap(err, "Kubernetes cluster unreachable") } return nil } -func (c *Client) newBuilder(namespace string, reader io.Reader) *resource.Result { - return c.NewBuilder(). - Internal(). - ContinueOnError(). - Schema(c.validator()). - NamespaceParam(namespace). - DefaultNamespace(). - Stream(reader, ""). - Flatten(). - Do() +// Create creates Kubernetes resources specified in the resource list. +func (c *Client) Create(resources ResourceList) (*Result, error) { + c.Log("creating %d resource(s)", len(resources)) + if err := perform(resources, createResource); err != nil { + return nil, err + } + return &Result{Created: resources}, nil } -func (c *Client) validator() validation.Schema { - schema, err := c.Validator(true) +// Wait up to the given timeout for the specified resources to be ready +func (c *Client) Wait(resources ResourceList, timeout time.Duration) error { + cs, err := c.getKubeClient() if err != nil { - c.Log("warning: failed to load schema: %s", err) + return err } - return schema -} - -// BuildUnstructured validates for Kubernetes objects and returns unstructured infos. -func (c *Client) BuildUnstructured(namespace string, reader io.Reader) (Result, error) { - var result Result - - result, err := c.NewBuilder(). - Unstructured(). - ContinueOnError(). - NamespaceParam(namespace). - DefaultNamespace(). - Stream(reader, ""). - Flatten(). - Do().Infos() - return result, scrubValidationError(err) -} - -// Build validates for Kubernetes objects and returns resource Infos from a io.Reader. -func (c *Client) Build(namespace string, reader io.Reader) (Result, error) { - var result Result - result, err := c.newBuilder(namespace, reader).Infos() - return result, scrubValidationError(err) + w := waiter{ + c: cs, + log: c.Log, + timeout: timeout, + } + return w.waitForResources(resources, false) } -// Get gets Kubernetes resources as pretty-printed string. -// -// Namespace will set the namespace. -func (c *Client) Get(namespace string, reader io.Reader) (string, error) { - // Since we don't know what order the objects come in, let's group them by the types, so - // that when we print them, they come out looking good (headers apply to subgroups, etc.). - objs := make(map[string][]runtime.Object) - infos, err := c.BuildUnstructured(namespace, reader) +// WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs. +func (c *Client) WaitWithJobs(resources ResourceList, timeout time.Duration) error { + cs, err := c.getKubeClient() if err != nil { - return "", err + return err } - - var objPods = make(map[string][]core.Pod) - - missing := []string{} - err = perform(infos, func(info *resource.Info) error { - c.Log("Doing get for %s: %q", info.Mapping.GroupVersionKind.Kind, info.Name) - if err := info.Get(); err != nil { - c.Log("WARNING: Failed Get for resource %q: %s", info.Name, err) - missing = append(missing, fmt.Sprintf("%v\t\t%s", info.Mapping.Resource, info.Name)) - return nil - } - - // Use APIVersion/Kind as grouping mechanism. I'm not sure if you can have multiple - // versions per cluster, but this certainly won't hurt anything, so let's be safe. - gvk := info.ResourceMapping().GroupVersionKind - vk := gvk.Version + "/" + gvk.Kind - objs[vk] = append(objs[vk], info.Object) - - //Get the relation pods - objPods, err = c.getSelectRelationPod(info, objPods) - if err != nil { - c.Log("Warning: get the relation pod is failed, err:%s", err.Error()) - } - - return nil - }) - if err != nil { - return "", err + w := waiter{ + c: cs, + log: c.Log, + timeout: timeout, } + return w.waitForResources(resources, true) +} - //here, we will add the objPods to the objs - for key, podItems := range objPods { - for i := range podItems { - objs[key+"(related)"] = append(objs[key+"(related)"], &podItems[i]) - } +func (c *Client) namespace() string { + if c.Namespace != "" { + return c.Namespace } - - // Ok, now we have all the objects grouped by types (say, by v1/Pod, v1/Service, etc.), so - // spin through them and print them. Printer is cool since it prints the header only when - // an object type changes, so we can just rely on that. Problem is it doesn't seem to keep - // track of tab widths. - buf := new(bytes.Buffer) - p, err := cmdutil.PrinterForOptions(&printers.PrintOptions{}) - if err != nil { - return "", err + if ns, _, err := c.Factory.ToRawKubeConfigLoader().Namespace(); err == nil { + return ns } - for t, ot := range objs { - if _, err = buf.WriteString("==> " + t + "\n"); err != nil { - return "", err - } - for _, o := range ot { - if err := p.PrintObj(o, buf); err != nil { - c.Log("failed to print object type %s, object: %q :\n %v", t, o, err) - return "", err - } - } - if _, err := buf.WriteString("\n"); err != nil { - return "", err - } - } - if len(missing) > 0 { - buf.WriteString(MissingGetHeader) - for _, s := range missing { - fmt.Fprintln(buf, s) - } - } - return buf.String(), nil + return v1.NamespaceDefault } -// Update reads in the current configuration and a target configuration from io.reader -// and creates resources that don't already exists, updates resources that have been modified -// in the target configuration and deletes resources from the current configuration that are -// not present in the target configuration. -// -// Namespace will set the namespaces. -func (c *Client) Update(namespace string, originalReader, targetReader io.Reader, force bool, recreate bool, timeout int64, shouldWait bool) error { - original, err := c.BuildUnstructured(namespace, originalReader) - if err != nil { - return fmt.Errorf("failed decoding reader into objects: %s", err) - } +// newBuilder returns a new resource builder for structured api objects. +func (c *Client) newBuilder() *resource.Builder { + return c.Factory.NewBuilder(). + ContinueOnError(). + NamespaceParam(c.namespace()). + DefaultNamespace(). + Flatten() +} - c.Log("building resources from updated manifest") - target, err := c.BuildUnstructured(namespace, targetReader) +// Build validates for Kubernetes objects and returns unstructured infos. +func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) { + schema, err := c.Factory.Validator(validate) if err != nil { - return fmt.Errorf("failed decoding reader into objects: %s", err) + return nil, err } + result, err := c.newBuilder(). + Unstructured(). + Schema(schema). + Stream(reader, ""). + Do().Infos() + return result, scrubValidationError(err) +} +// Update takes the current list of objects and target list of objects and +// creates resources that don't already exist, updates resources that have been +// modified in the target configuration, and deletes resources from the current +// configuration that are not present in the target configuration. If an error +// occurs, a Result will still be returned with the error, containing all +// resource updates, creations, and deletions that were attempted. These can be +// used for cleanup or other logging purposes. +func (c *Client) Update(original, target ResourceList, force bool) (*Result, error) { updateErrors := []string{} + res := &Result{} c.Log("checking %d resources for changes", len(target)) - err = target.Visit(func(info *resource.Info, err error) error { + err := target.Visit(func(info *resource.Info, err error) error { if err != nil { return err } helper := resource.NewHelper(info.Client, info.Mapping) - if _, err := helper.Get(info.Namespace, info.Name, info.Export); err != nil { - if !errors.IsNotFound(err) { - return fmt.Errorf("Could not get information about the resource: %s", err) + if _, err := helper.Get(info.Namespace, info.Name); err != nil { + if !apierrors.IsNotFound(err) { + return errors.Wrap(err, "could not get information about the resource") } + // Append the created resource to the results, even if something fails + res.Created = append(res.Created, info) + // Since the resource does not exist, create it. if err := createResource(info); err != nil { - return fmt.Errorf("failed to create resource: %s", err) + return errors.Wrap(err, "failed to create resource") } kind := info.Mapping.GroupVersionKind.Kind - c.Log("Created a new %s called %q\n", kind, info.Name) + c.Log("Created a new %s called %q in %s\n", kind, info.Name, info.Namespace) return nil } originalInfo := original.Get(info) if originalInfo == nil { kind := info.Mapping.GroupVersionKind.Kind - return fmt.Errorf("no %s with the name %q found", kind, info.Name) + return errors.Errorf("no %s with the name %q found", kind, info.Name) } - if err := updateResource(c, info, originalInfo.Object, force, recreate); err != nil { + if err := updateResource(c, info, originalInfo.Object, force); err != nil { c.Log("error updating the resource %q:\n\t %v", info.Name, err) updateErrors = append(updateErrors, err.Error()) } + // Because we check for errors later, append the info regardless + res.Updated = append(res.Updated, info) return nil }) switch { case err != nil: - return err + return res, err case len(updateErrors) != 0: - return fmt.Errorf(strings.Join(updateErrors, " && ")) + return res, errors.Errorf(strings.Join(updateErrors, " && ")) } for _, info := range original.Difference(target) { c.Log("Deleting %q in %s...", info.Name, info.Namespace) - if err := deleteResource(c, info); err != nil { - c.Log("Failed to delete %q, err: %s", info.Name, err) + + if err := info.Get(); err != nil { + c.Log("Unable to get obj %q, err: %s", info.Name, err) + continue } + annotations, err := metadataAccessor.Annotations(info.Object) + if err != nil { + c.Log("Unable to get annotations on %q, err: %s", info.Name, err) + } + if annotations != nil && annotations[ResourcePolicyAnno] == KeepPolicy { + c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy) + continue + } + if err := deleteResource(info); err != nil { + c.Log("Failed to delete %q, err: %s", info.ObjectName(), err) + continue + } + res.Deleted = append(res.Deleted, info) } - if shouldWait { - return c.waitForResources(time.Duration(timeout)*time.Second, target) - } - return nil + return res, nil } -// Delete deletes Kubernetes resources from an io.reader. -// -// Namespace will set the namespace. -func (c *Client) Delete(namespace string, reader io.Reader) error { - infos, err := c.BuildUnstructured(namespace, reader) - if err != nil { - return err - } - return perform(infos, func(info *resource.Info) error { +// Delete deletes Kubernetes resources specified in the resources list. It will +// attempt to delete all resources even if one or more fail and collect any +// errors. All successfully deleted items will be returned in the `Deleted` +// ResourceList that is part of the result. +func (c *Client) Delete(resources ResourceList) (*Result, []error) { + var errs []error + res := &Result{} + mtx := sync.Mutex{} + err := perform(resources, func(info *resource.Info) error { c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind) - err := deleteResource(c, info) - return c.skipIfNotFound(err) + if err := c.skipIfNotFound(deleteResource(info)); err != nil { + mtx.Lock() + defer mtx.Unlock() + // Collect the error and continue on + errs = append(errs, err) + } else { + mtx.Lock() + defer mtx.Unlock() + res.Deleted = append(res.Deleted, info) + } + return nil }) + if err != nil { + // Rewrite the message from "no objects visited" if that is what we got + // back + if err == ErrNoObjectsVisited { + err = errors.New("object not found, skipping delete") + } + errs = append(errs, err) + } + if errs != nil { + return nil, errs + } + return res, nil } func (c *Client) skipIfNotFound(err error) error { - if errors.IsNotFound(err) { + if apierrors.IsNotFound(err) { c.Log("%v", err) return nil } return err } -func (c *Client) watchTimeout(t time.Duration) ResourceActorFunc { +func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error { return func(info *resource.Info) error { return c.watchUntilReady(t, info) } } -// WatchUntilReady watches the resource given in the reader, and waits until it is ready. +// WatchUntilReady watches the resources given 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. @@ -348,31 +330,50 @@ func (c *Client) watchTimeout(t time.Duration) ResourceActorFunc { // // - Jobs: A job is marked "Ready" when it has successfully completed. This is // ascertained by watching the Status fields in a job's output. +// - Pods: A pod is marked "Ready" when it has successfully completed. This is +// ascertained by watching the status.phase field in a pod's output. // // Handling for other kinds will be added as necessary. -func (c *Client) WatchUntilReady(namespace string, reader io.Reader, timeout int64, shouldWait bool) error { - infos, err := c.Build(namespace, reader) - if err != nil { - return err - } +func (c *Client) WatchUntilReady(resources ResourceList, timeout time.Duration) 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(infos, c.watchTimeout(time.Duration(timeout)*time.Second)) + return perform(resources, c.watchTimeout(timeout)) } -func perform(infos Result, fn ResourceActorFunc) error { +func perform(infos ResourceList, fn func(*resource.Info) error) error { if len(infos) == 0 { return ErrNoObjectsVisited } - for _, info := range infos { - if err := fn(info); err != nil { + errs := make(chan error) + go batchPerform(infos, fn, errs) + + for range infos { + err := <-errs + if err != nil { return err } } return nil } +func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<- error) { + var kind string + var wg sync.WaitGroup + for _, info := range infos { + currentKind := info.Object.GetObjectKind().GroupVersionKind().Kind + if kind != currentKind { + wg.Wait() + kind = currentKind + } + wg.Add(1) + go func(i *resource.Info) { + errs <- fn(i) + wg.Done() + }(info) + } +} + func createResource(info *resource.Info) error { obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) if err != nil { @@ -381,39 +382,38 @@ func createResource(info *resource.Info) error { return info.Refresh(obj, true) } -func deleteResource(c *Client, info *resource.Info) error { - reaper, err := c.Reaper(info.Mapping) - if err != nil { - // If there is no reaper for this resources, delete it. - if kubectl.IsNoSuchReaperError(err) { - return resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name) - } - return err - } - c.Log("Using reaper for deleting %q", info.Name) - return reaper.Stop(info.Namespace, info.Name, 0, nil) +func deleteResource(info *resource.Info) error { + policy := metav1.DeletePropagationBackground + opts := &metav1.DeleteOptions{PropagationPolicy: &policy} + _, err := resource.NewHelper(info.Client, info.Mapping).DeleteWithOptions(info.Namespace, info.Name, opts) + return err } -func createPatch(mapping *meta.RESTMapping, target, current runtime.Object) ([]byte, types.PatchType, error) { +func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.PatchType, error) { oldData, err := json.Marshal(current) if err != nil { - return nil, types.StrategicMergePatchType, fmt.Errorf("serializing current configuration: %s", err) + return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing current configuration") } - newData, err := json.Marshal(target) + newData, err := json.Marshal(target.Object) if err != nil { - return nil, types.StrategicMergePatchType, fmt.Errorf("serializing target configuration: %s", err) + return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing target configuration") } - // While different objects need different merge types, the parent function - // that calls this does not try to create a patch when the data (first - // returned object) is nil. We can skip calculating the merge type as - // the returned merge type is ignored. - if apiequality.Semantic.DeepEqual(oldData, newData) { - return nil, types.StrategicMergePatchType, nil + // Fetch the current object for the three way merge + helper := resource.NewHelper(target.Client, target.Mapping) + currentObj, err := helper.Get(target.Namespace, target.Name) + if err != nil && !apierrors.IsNotFound(err) { + return nil, types.StrategicMergePatchType, errors.Wrapf(err, "unable to get data for current object %s/%s", target.Namespace, target.Name) + } + + // Even if currentObj is nil (because it was not found), it will marshal just fine + currentData, err := json.Marshal(currentObj) + if err != nil { + return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing live configuration") } // Get a versioned object - versionedObject, err := mapping.ConvertToVersion(target, mapping.GroupVersionKind.GroupVersion()) + versionedObject := AsVersioned(target) // Unstructured objects, such as CRDs, may not have an not registered error // returned from ConvertToVersion. Anything that's unstructured should @@ -421,158 +421,82 @@ func createPatch(mapping *meta.RESTMapping, target, current runtime.Object) ([]b // on objects like CRDs. _, isUnstructured := versionedObject.(runtime.Unstructured) - switch { - case runtime.IsNotRegisteredError(err), isUnstructured: + // On newer K8s versions, CRDs aren't unstructured but has this dedicated type + _, isCRD := versionedObject.(*apiextv1beta1.CustomResourceDefinition) + + if isUnstructured || isCRD { // fall back to generic JSON merge patch patch, err := jsonpatch.CreateMergePatch(oldData, newData) return patch, types.MergePatchType, err - case err != nil: - return nil, types.StrategicMergePatchType, fmt.Errorf("failed to get versionedObject: %s", err) - default: - patch, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, versionedObject) - return patch, types.StrategicMergePatchType, err } -} -func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool, recreate bool) error { - patch, patchType, err := createPatch(target.Mapping, target.Object, currentObj) + patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject) if err != nil { - return fmt.Errorf("failed to create patch: %s", err) - } - if patch == nil { - c.Log("Looks like there are no changes for %s %q", target.Mapping.GroupVersionKind.Kind, target.Name) - // This needs to happen to make sure that tiller has the latest info from the API - // Otherwise there will be no labels and other functions that use labels will panic - if err := target.Get(); err != nil { - return fmt.Errorf("error trying to refresh resource information: %v", err) + return nil, types.StrategicMergePatchType, errors.Wrap(err, "unable to create patch metadata from object") + } + + patch, err := strategicpatch.CreateThreeWayMergePatch(oldData, newData, currentData, patchMeta, true) + return patch, types.StrategicMergePatchType, err +} + +func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool) error { + var ( + obj runtime.Object + helper = resource.NewHelper(target.Client, target.Mapping) + kind = target.Mapping.GroupVersionKind.Kind + ) + + // if --force is applied, attempt to replace the existing resource with the new object. + if force { + var err error + obj, err = helper.Replace(target.Namespace, target.Name, true, target.Object) + if err != nil { + return errors.Wrap(err, "failed to replace object") } + c.Log("Replaced %q with kind %s for kind %s", target.Name, currentObj.GetObjectKind().GroupVersionKind().Kind, kind) } else { - // send patch to server - helper := resource.NewHelper(target.Client, target.Mapping) - - obj, err := helper.Patch(target.Namespace, target.Name, patchType, patch) + patch, patchType, err := createPatch(target, currentObj) if err != nil { - kind := target.Mapping.GroupVersionKind.Kind - log.Printf("Cannot patch %s: %q (%v)", kind, target.Name, err) - - if force { - // Attempt to delete... - if err := deleteResource(c, target); err != nil { - return err - } - log.Printf("Deleted %s: %q", kind, target.Name) - - // ... and recreate - if err := createResource(target); err != nil { - return fmt.Errorf("Failed to recreate resource: %s", err) - } - log.Printf("Created a new %s called %q\n", kind, target.Name) - - // No need to refresh the target, as we recreated the resource based - // on it. In addition, it might not exist yet and a call to `Refresh` - // may fail. - } else { - log.Print("Use --force to force recreation of the resource") - return err - } - } else { - // When patch succeeds without needing to recreate, refresh target. - target.Refresh(obj, true) + return errors.Wrap(err, "failed to create patch") } - } - - if !recreate { - return nil - } - - versioned, err := c.AsVersionedObject(target.Object) - if runtime.IsNotRegisteredError(err) { - return nil - } - if err != nil { - return err - } - selector, err := getSelectorFromObject(versioned) - if err != nil { - return nil - } - - client, err := c.ClientSet() - if err != nil { - return err - } - - pods, err := client.Core().Pods(target.Namespace).List(metav1.ListOptions{ - FieldSelector: fields.Everything().String(), - LabelSelector: labels.Set(selector).AsSelector().String(), - }) - if err != nil { - return err - } - - // Restart pods - for _, pod := range pods.Items { - c.Log("Restarting pod: %v/%v", pod.Namespace, pod.Name) - - // Delete each pod for get them restarted with changed spec. - if err := client.Core().Pods(pod.Namespace).Delete(pod.Name, metav1.NewPreconditionDeleteOptions(string(pod.UID))); err != nil { - return err + if patch == nil || string(patch) == "{}" { + c.Log("Looks like there are no changes for %s %q", target.Mapping.GroupVersionKind.Kind, target.Name) + // This needs to happen to make sure that Helm has the latest info from the API + // Otherwise there will be no labels and other functions that use labels will panic + if err := target.Get(); err != nil { + return errors.Wrap(err, "failed to refresh resource information") + } + return nil + } + // send patch to server + obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil) + if err != nil { + return errors.Wrapf(err, "cannot patch %q with kind %s", target.Name, kind) } } + + target.Refresh(obj, true) return nil } -func getSelectorFromObject(obj runtime.Object) (map[string]string, error) { - switch typed := obj.(type) { - - case *v1.ReplicationController: - return typed.Spec.Selector, nil - - case *extv1beta1.ReplicaSet: - return typed.Spec.Selector.MatchLabels, nil - case *appsv1.ReplicaSet: - return typed.Spec.Selector.MatchLabels, nil - - case *extv1beta1.Deployment: - return typed.Spec.Selector.MatchLabels, nil - case *appsv1beta1.Deployment: - return typed.Spec.Selector.MatchLabels, nil - case *appsv1beta2.Deployment: - return typed.Spec.Selector.MatchLabels, nil - case *appsv1.Deployment: - return typed.Spec.Selector.MatchLabels, nil - - case *extv1beta1.DaemonSet: - return typed.Spec.Selector.MatchLabels, nil - case *appsv1beta2.DaemonSet: - return typed.Spec.Selector.MatchLabels, nil - case *appsv1.DaemonSet: - return typed.Spec.Selector.MatchLabels, nil - - case *batch.Job: - return typed.Spec.Selector.MatchLabels, nil - - case *appsv1beta1.StatefulSet: - return typed.Spec.Selector.MatchLabels, nil - case *appsv1beta2.StatefulSet: - return typed.Spec.Selector.MatchLabels, nil - case *appsv1.StatefulSet: - return typed.Spec.Selector.MatchLabels, nil - +func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) error { + kind := info.Mapping.GroupVersionKind.Kind + switch kind { + case "Job", "Pod": default: - return nil, fmt.Errorf("Unsupported kind when getting selector: %v", obj) + return nil } -} -func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) error { - w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion) + c.Log("Watching for changes to %s %s with timeout of %v", kind, info.Name, timeout) + + // Use a selector on the name of the resource. This should be unique for the + // given version and kind + selector, err := fields.ParseSelector(fmt.Sprintf("metadata.name=%s", info.Name)) if err != nil { return err } - - kind := info.Mapping.GroupVersionKind.Kind - c.Log("Watching for changes to %s %s with timeout of %v", kind, info.Name, timeout) + lw := cachetools.NewListWatchFromClient(info.Client, info.Mapping.Resource.Resource, info.Namespace, selector) // What we watch for depends on the Kind. // - For a Job, we watch for completion. @@ -580,7 +504,12 @@ func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) err // 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) { + ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout) + defer cancel() + _, err = watchtools.UntilWithSync(ctx, lw, &unstructured.Unstructured{}, nil, func(e watch.Event) (bool, error) { + // Make sure the incoming object is versioned as we use unstructured + // objects when we build manifests + obj := convertWithMapper(e.Object, info.Mapping) switch e.Type { case watch.Added, watch.Modified: // For things like a secret or a config map, this is the best indicator @@ -588,8 +517,11 @@ func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) err // the status go into a good state. For other types, like ReplicaSet // we don't really do anything to support these as hooks. c.Log("Add/Modify event for %s: %v", info.Name, e.Type) - if kind == "Job" { - return c.waitForJob(e, info.Name) + switch kind { + case "Job": + return c.waitForJob(obj, info.Name) + case "Pod": + return c.waitForPodSuccess(obj, info.Name) } return true, nil case watch.Deleted: @@ -598,7 +530,7 @@ func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) err case watch.Error: // Handle error and return with an error. c.Log("Error event for %s", info.Name) - return true, fmt.Errorf("Failed to deploy %s", info.Name) + return true, errors.Errorf("failed to deploy %s", info.Name) default: return false, nil } @@ -606,32 +538,20 @@ func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) err return err } -// AsVersionedObject converts a runtime.object to a versioned object. -func (c *Client) AsVersionedObject(obj runtime.Object) (runtime.Object, error) { - json, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj) - if err != nil { - return nil, err - } - versions := &runtime.VersionedObjects{} - decoder := unstructured.UnstructuredJSONScheme - err = runtime.DecodeInto(decoder, json, versions) - return versions.First(), err -} - // waitForJob is a helper that waits for a job to complete. // // This operates on an event returned from a watcher. -func (c *Client) waitForJob(e watch.Event, name string) (bool, error) { - o, ok := e.Object.(*batchinternal.Job) +func (c *Client) waitForJob(obj runtime.Object, name string) (bool, error) { + o, ok := obj.(*batch.Job) if !ok { - return true, fmt.Errorf("Expected %s to be a *batch.Job, got %T", name, e.Object) + return true, errors.Errorf("expected %s to be a *batch.Job, got %T", name, obj) } for _, c := range o.Status.Conditions { - if c.Type == batchinternal.JobComplete && c.Status == core.ConditionTrue { + if c.Type == batch.JobComplete && c.Status == "True" { return true, nil - } else if c.Type == batchinternal.JobFailed && c.Status == core.ConditionTrue { - return true, fmt.Errorf("Job failed: %s", c.Reason) + } else if c.Type == batch.JobFailed && c.Status == "True" { + return true, errors.Errorf("job failed: %s", c.Reason) } } @@ -639,6 +559,30 @@ func (c *Client) waitForJob(e watch.Event, name string) (bool, error) { return false, nil } +// waitForPodSuccess is a helper that waits for a pod to complete. +// +// This operates on an event returned from a watcher. +func (c *Client) waitForPodSuccess(obj runtime.Object, name string) (bool, error) { + o, ok := obj.(*v1.Pod) + if !ok { + return true, errors.Errorf("expected %s to be a *v1.Pod, got %T", name, obj) + } + + switch o.Status.Phase { + case v1.PodSucceeded: + c.Log("Pod %s succeeded", o.Name) + return true, nil + case v1.PodFailed: + return true, errors.Errorf("pod %s failed", o.Name) + case v1.PodPending: + c.Log("Pod %s pending", o.Name) + case v1.PodRunning: + c.Log("Pod %s running", o.Name) + } + + return false, nil +} + // scrubValidationError removes kubectl info from the message. func scrubValidationError(err error) error { if err == nil { @@ -647,111 +591,36 @@ func scrubValidationError(err error) error { const stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false" if strings.Contains(err.Error(), stopValidateMessage) { - return goerrors.New(strings.Replace(err.Error(), "; "+stopValidateMessage, "", -1)) + return errors.New(strings.ReplaceAll(err.Error(), "; "+stopValidateMessage, "")) } return err } // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase // and returns said phase (PodSucceeded or PodFailed qualify). -func (c *Client) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (core.PodPhase, error) { - infos, err := c.Build(namespace, reader) - if err != nil { - return core.PodUnknown, err - } - info := infos[0] - - kind := info.Mapping.GroupVersionKind.Kind - if kind != "Pod" { - return core.PodUnknown, fmt.Errorf("%s is not a Pod", info.Name) - } - - if err := c.watchPodUntilComplete(timeout, info); err != nil { - return core.PodUnknown, err - } - - if err := info.Get(); err != nil { - return core.PodUnknown, err - } - status := info.Object.(*core.Pod).Status.Phase - - return status, nil -} - -func (c *Client) watchPodUntilComplete(timeout time.Duration, info *resource.Info) error { - w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion) - if err != nil { - return err - } - - c.Log("Watching pod %s for completion with timeout of %v", info.Name, timeout) - _, err = watch.Until(timeout, w, func(e watch.Event) (bool, error) { - return conditions.PodCompleted(e) - }) - - return err -} - -//get a kubernetes resources' relation pods -// kubernetes resource used select labels to relate pods -func (c *Client) getSelectRelationPod(info *resource.Info, objPods map[string][]core.Pod) (map[string][]core.Pod, error) { - if info == nil { - return objPods, nil - } - - c.Log("get relation pod of object: %s/%s/%s", info.Namespace, info.Mapping.GroupVersionKind.Kind, info.Name) - - versioned, err := c.AsVersionedObject(info.Object) - if runtime.IsNotRegisteredError(err) { - return objPods, nil - } +func (c *Client) WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error) { + client, err := c.getKubeClient() if err != nil { - return objPods, err + return v1.PodUnknown, err } - - // We can ignore this error because it will only error if it isn't a type that doesn't - // have pods. In that case, we don't care - selector, _ := getSelectorFromObject(versioned) - - selectorString := labels.Set(selector).AsSelector().String() - - // If we have an empty selector, this likely is a service or config map, so bail out now - if selectorString == "" { - return objPods, nil - } - - client, _ := c.ClientSet() - - pods, err := client.Core().Pods(info.Namespace).List(metav1.ListOptions{ - FieldSelector: fields.Everything().String(), - LabelSelector: labels.Set(selector).AsSelector().String(), + to := int64(timeout) + watcher, err := client.CoreV1().Pods(c.namespace()).Watch(context.Background(), metav1.ListOptions{ + FieldSelector: fmt.Sprintf("metadata.name=%s", name), + TimeoutSeconds: &to, }) - if err != nil { - return objPods, err - } - - for _, pod := range pods.Items { - if pod.APIVersion == "" { - pod.APIVersion = "v1" - } - if pod.Kind == "" { - pod.Kind = "Pod" + for event := range watcher.ResultChan() { + p, ok := event.Object.(*v1.Pod) + if !ok { + return v1.PodUnknown, fmt.Errorf("%s not a pod", name) } - vk := pod.GroupVersionKind().Version + "/" + pod.GroupVersionKind().Kind - - if !isFoundPod(objPods[vk], pod) { - objPods[vk] = append(objPods[vk], pod) + switch p.Status.Phase { + case v1.PodFailed: + return v1.PodFailed, nil + case v1.PodSucceeded: + return v1.PodSucceeded, nil } } - return objPods, nil -} -func isFoundPod(podItem []core.Pod, pod core.Pod) bool { - for _, value := range podItem { - if (value.Namespace == pod.Namespace) && (value.Name == pod.Name) { - return true - } - } - return false + return v1.PodUnknown, err } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 47049810af6..de5358aee87 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,58 +23,51 @@ import ( "net/http" "strings" "testing" - "time" - "k8s.io/apimachinery/pkg/api/meta" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest/fake" - "k8s.io/kubernetes/pkg/api/legacyscheme" - "k8s.io/kubernetes/pkg/api/testapi" - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/kubectl" - cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" - cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" - "k8s.io/kubernetes/pkg/kubectl/resource" - "k8s.io/kubernetes/pkg/kubectl/scheme" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) -var unstructuredSerializer = dynamic.ContentConfig().NegotiatedSerializer +var unstructuredSerializer = resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer +var codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) -func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser { +func objBody(obj runtime.Object) io.ReadCloser { return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) } -func newPod(name string) core.Pod { - return newPodWithStatus(name, core.PodStatus{}, "") +func newPod(name string) v1.Pod { + return newPodWithStatus(name, v1.PodStatus{}, "") } -func newPodWithStatus(name string, status core.PodStatus, namespace string) core.Pod { - ns := core.NamespaceDefault +func newPodWithStatus(name string, status v1.PodStatus, namespace string) v1.Pod { + ns := v1.NamespaceDefault if namespace != "" { ns = namespace } - return core.Pod{ + return v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: ns, SelfLink: "/api/v1/namespaces/default/pods/" + name, }, - Spec: core.PodSpec{ - Containers: []core.Container{{ + Spec: v1.PodSpec{ + Containers: []v1.Container{{ Name: "app:v4", Image: "abc/app:v4", - Ports: []core.ContainerPort{{Name: "http", ContainerPort: 80}}, + Ports: []v1.ContainerPort{{Name: "http", ContainerPort: 80}}, }}, }, Status: status, } } -func newPodList(names ...string) core.PodList { - var list core.PodList +func newPodList(names ...string) v1.PodList { + var list v1.PodList for _, name := range names { list.Items = append(list.Items, newPod(name)) } @@ -94,58 +87,31 @@ func notFoundBody() *metav1.Status { func newResponse(code int, obj runtime.Object) (*http.Response, error) { header := http.Header{} header.Set("Content-Type", runtime.ContentTypeJSON) - body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(testapi.Default.Codec(), obj)))) + body := ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) return &http.Response{StatusCode: code, Header: header, Body: body}, nil } -type fakeReaper struct { - name string -} - -func (r *fakeReaper) Stop(namespace, name string, timeout time.Duration, gracePeriod *metav1.DeleteOptions) error { - r.name = name - return nil -} - -type fakeReaperFactory struct { - cmdutil.Factory - reaper kubectl.Reaper -} - -func (f *fakeReaperFactory) Reaper(mapping *meta.RESTMapping) (kubectl.Reaper, error) { - return f.reaper, nil -} +func newTestClient(t *testing.T) *Client { + testFactory := cmdtesting.NewTestFactory() + t.Cleanup(testFactory.Cleanup) -type testClient struct { - *Client - *cmdtesting.TestFactory -} - -func newTestClient() *testClient { - tf := cmdtesting.NewTestFactory() - c := &Client{ - Factory: tf, + return &Client{ + Factory: testFactory.WithNamespace("default"), Log: nopLogger, } - return &testClient{ - Client: c, - TestFactory: tf, - } } func TestUpdate(t *testing.T) { listA := newPodList("starfish", "otter", "squid") listB := newPodList("starfish", "otter", "dolphin") listC := newPodList("starfish", "otter", "dolphin") - listB.Items[0].Spec.Containers[0].Ports = []core.ContainerPort{{Name: "https", ContainerPort: 443}} - listC.Items[0].Spec.Containers[0].Ports = []core.ContainerPort{{Name: "https", ContainerPort: 443}} + listB.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} + listC.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} var actions []string - tf := cmdtesting.NewTestFactory() - defer tf.Cleanup() - tf.UnstructuredClient = &fake.RESTClient{ - GroupVersion: schema.GroupVersion{Version: "v1"}, + c := newTestClient(t) + c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{ NegotiatedSerializer: unstructuredSerializer, Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { p, m := req.URL.Path, req.Method @@ -156,6 +122,17 @@ func TestUpdate(t *testing.T) { return newResponse(200, &listA.Items[0]) case p == "/namespaces/default/pods/otter" && m == "GET": return newResponse(200, &listA.Items[1]) + case p == "/namespaces/default/pods/otter" && m == "PATCH": + data, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatalf("could not dump request: %s", err) + } + req.Body.Close() + expected := `{}` + if string(data) != expected { + t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data)) + } + return newResponse(200, &listB.Items[0]) case p == "/namespaces/default/pods/dolphin" && m == "GET": return newResponse(404, notFoundBody()) case p == "/namespaces/default/pods/starfish" && m == "PATCH": @@ -173,21 +150,38 @@ func TestUpdate(t *testing.T) { return newResponse(200, &listB.Items[1]) case p == "/namespaces/default/pods/squid" && m == "DELETE": return newResponse(200, &listB.Items[1]) + case p == "/namespaces/default/pods/squid" && m == "GET": + return newResponse(200, &listB.Items[2]) default: t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) return nil, nil } }), } + first, err := c.Build(objBody(&listA), false) + if err != nil { + t.Fatal(err) + } + second, err := c.Build(objBody(&listB), false) + if err != nil { + t.Fatal(err) + } - c := newTestClient() - reaper := &fakeReaper{} - rf := &fakeReaperFactory{Factory: tf, reaper: reaper} - c.Client.Factory = rf - codec := legacyscheme.Codecs.LegacyCodec(scheme.Versions...) - if err := c.Update(core.NamespaceDefault, objBody(codec, &listA), objBody(codec, &listB), false, false, 0, false); err != nil { + result, err := c.Update(first, second, false) + if err != nil { t.Fatal(err) } + + if len(result.Created) != 1 { + t.Errorf("expected 1 resource created, got %d", len(result.Created)) + } + if len(result.Updated) != 2 { + t.Errorf("expected 2 resource updated, got %d", len(result.Updated)) + } + if len(result.Deleted) != 1 { + t.Errorf("expected 1 resource deleted, got %d", len(result.Deleted)) + } + // TODO: Find a way to test methods that use Client Set // Test with a wait // if err := c.Update("test", objBody(codec, &listB), objBody(codec, &listC), false, 300, true); err != nil { @@ -199,27 +193,25 @@ func TestUpdate(t *testing.T) { // t.Fatal(err) // } expectedActions := []string{ + "/namespaces/default/pods/starfish:GET", "/namespaces/default/pods/starfish:GET", "/namespaces/default/pods/starfish:PATCH", "/namespaces/default/pods/otter:GET", "/namespaces/default/pods/otter:GET", + "/namespaces/default/pods/otter:GET", "/namespaces/default/pods/dolphin:GET", "/namespaces/default/pods:POST", + "/namespaces/default/pods/squid:GET", + "/namespaces/default/pods/squid:DELETE", } if len(expectedActions) != len(actions) { - t.Errorf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions)) - return + t.Fatalf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions)) } for k, v := range expectedActions { if actions[k] != v { t.Errorf("expected %s request got %s", v, actions[k]) } } - - if reaper.name != "squid" { - t.Errorf("unexpected reaper: %#v", reaper) - } - } func TestBuild(t *testing.T) { @@ -236,20 +228,18 @@ func TestBuild(t *testing.T) { reader: strings.NewReader(guestbookManifest), count: 6, }, { - name: "Invalid schema", + name: "Valid input, deploying resources into different namespaces", namespace: "test", - reader: strings.NewReader(testInvalidServiceManifest), - err: true, + reader: strings.NewReader(namespacedGuestbookManifest), + count: 1, }, } - c := newTestClient() + c := newTestClient(t) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c.Cleanup() - // Test for an invalid manifest - infos, err := c.Build(tt.namespace, tt.reader) + infos, err := c.Build(tt.reader, false) if err != nil && !tt.err { t.Errorf("Got error message when no error should have occurred: %v", err) } else if err != nil && strings.Contains(err.Error(), "--validate=false") { @@ -263,66 +253,20 @@ func TestBuild(t *testing.T) { } } -func TestGet(t *testing.T) { - list := newPodList("starfish", "otter") - c := newTestClient() - defer c.Cleanup() - c.TestFactory.UnstructuredClient = &fake.RESTClient{ - GroupVersion: schema.GroupVersion{Version: "v1"}, - NegotiatedSerializer: unstructuredSerializer, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - p, m := req.URL.Path, req.Method - t.Logf("got request %s %s", p, m) - switch { - case p == "/namespaces/default/pods/starfish" && m == "GET": - return newResponse(404, notFoundBody()) - case p == "/namespaces/default/pods/otter" && m == "GET": - return newResponse(200, &list.Items[1]) - default: - t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) - return nil, nil - } - }), - } - - // Test Success - data := strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: otter") - o, err := c.Get("default", data) - if err != nil { - t.Errorf("Expected missing results, got %q", err) - } - if !strings.Contains(o, "==> v1/Pod") && !strings.Contains(o, "otter") { - t.Errorf("Expected v1/Pod otter, got %s", o) - } - - // Test failure - data = strings.NewReader("kind: Pod\napiVersion: v1\nmetadata:\n name: starfish") - o, err = c.Get("default", data) - if err != nil { - t.Errorf("Expected missing results, got %q", err) - } - if !strings.Contains(o, "MISSING") && !strings.Contains(o, "pods\t\tstarfish") { - t.Errorf("Expected missing starfish, got %s", o) - } -} - func TestPerform(t *testing.T) { tests := []struct { name string - namespace string reader io.Reader count int err bool errMessage string }{ { - name: "Valid input", - namespace: "test", - reader: strings.NewReader(guestbookManifest), - count: 6, + name: "Valid input", + reader: strings.NewReader(guestbookManifest), + count: 6, }, { name: "Empty manifests", - namespace: "test", reader: strings.NewReader(""), err: true, errMessage: "no objects visited", @@ -335,16 +279,11 @@ func TestPerform(t *testing.T) { fn := func(info *resource.Info) error { results = append(results, info) - - if info.Namespace != tt.namespace { - t.Errorf("expected namespace to be '%s', got %s", tt.namespace, info.Namespace) - } return nil } - c := newTestClient() - defer c.Cleanup() - infos, err := c.Build(tt.namespace, tt.reader) + c := newTestClient(t) + infos, err := c.Build(tt.reader, false) if err != nil && err.Error() != tt.errMessage { t.Errorf("Error while building manifests: %v", err) } @@ -367,24 +306,41 @@ func TestPerform(t *testing.T) { func TestReal(t *testing.T) { t.Skip("This is a live test, comment this line to run") c := New(nil) - if err := c.Create("test", strings.NewReader(guestbookManifest), 300, false); err != nil { + resources, err := c.Build(strings.NewReader(guestbookManifest), false) + if err != nil { + t.Fatal(err) + } + if _, err := c.Create(resources); err != nil { t.Fatal(err) } testSvcEndpointManifest := testServiceManifest + "\n---\n" + testEndpointManifest c = New(nil) - if err := c.Create("test-delete", strings.NewReader(testSvcEndpointManifest), 300, false); err != nil { + resources, err = c.Build(strings.NewReader(testSvcEndpointManifest), false) + if err != nil { + t.Fatal(err) + } + if _, err := c.Create(resources); err != nil { t.Fatal(err) } - if err := c.Delete("test-delete", strings.NewReader(testEndpointManifest)); err != nil { + resources, err = c.Build(strings.NewReader(testEndpointManifest), false) + if err != nil { t.Fatal(err) } - // ensures that delete does not fail if a resource is not found - if err := c.Delete("test-delete", strings.NewReader(testSvcEndpointManifest)); err != nil { + if _, errs := c.Delete(resources); errs != nil { + t.Fatal(errs) + } + + resources, err = c.Build(strings.NewReader(testSvcEndpointManifest), false) + if err != nil { t.Fatal(err) } + // ensures that delete does not fail if a resource is not found + if _, errs := c.Delete(resources); errs != nil { + t.Fatal(errs) + } } const testServiceManifest = ` @@ -401,14 +357,6 @@ spec: targetPort: 9376 ` -const testInvalidServiceManifest = ` -kind: Service -apiVersion: v1 -spec: - ports: - - port: "80" -` - const testEndpointManifest = ` kind: Endpoints apiVersion: v1 @@ -544,3 +492,31 @@ spec: ports: - containerPort: 80 ` + +const namespacedGuestbookManifest = ` +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: frontend + namespace: guestbook +spec: + replicas: 3 + template: + metadata: + labels: + app: guestbook + tier: frontend + spec: + containers: + - name: php-redis + image: gcr.io/google-samples/gb-frontend:v4 + resources: + requests: + cpu: 100m + memory: 100Mi + env: + - name: GET_HOSTS_FROM + value: dns + ports: + - containerPort: 80 +` diff --git a/pkg/kube/config.go b/pkg/kube/config.go index b6560486ef8..e00c9acb158 100644 --- a/pkg/kube/config.go +++ b/pkg/kube/config.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,19 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "k8s.io/helm/pkg/kube" +package kube // import "helm.sh/helm/v3/pkg/kube" -import "k8s.io/client-go/tools/clientcmd" +import "k8s.io/cli-runtime/pkg/genericclioptions" -// GetConfig returns a Kubernetes client config for a given context. -func GetConfig(context string) clientcmd.ClientConfig { - rules := clientcmd.NewDefaultClientConfigLoadingRules() - rules.DefaultClientConfig = &clientcmd.DefaultClientConfig - - overrides := &clientcmd.ConfigOverrides{ClusterDefaults: clientcmd.ClusterDefaults} - - if context != "" { - overrides.CurrentContext = context - } - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides) +// GetConfig returns a Kubernetes client config. +// +// Deprecated +func GetConfig(kubeconfig, context, namespace string) *genericclioptions.ConfigFlags { + cf := genericclioptions.NewConfigFlags(true) + cf.Namespace = &namespace + cf.Context = &context + cf.KubeConfig = &kubeconfig + return cf } diff --git a/pkg/kube/converter.go b/pkg/kube/converter.go new file mode 100644 index 00000000000..3bf0e358c38 --- /dev/null +++ b/pkg/kube/converter.go @@ -0,0 +1,69 @@ +/* +Copyright The Helm Authors. + +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 kube // import "helm.sh/helm/v3/pkg/kube" + +import ( + "sync" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes/scheme" +) + +var k8sNativeScheme *runtime.Scheme +var k8sNativeSchemeOnce sync.Once + +// AsVersioned converts the given info into a runtime.Object with the correct +// group and version set +func AsVersioned(info *resource.Info) runtime.Object { + return convertWithMapper(info.Object, info.Mapping) +} + +// convertWithMapper converts the given object with the optional provided +// RESTMapping. If no mapping is provided, the default schema versioner is used +func convertWithMapper(obj runtime.Object, mapping *meta.RESTMapping) runtime.Object { + s := kubernetesNativeScheme() + var gv = runtime.GroupVersioner(schema.GroupVersions(s.PrioritizedVersionsAllGroups())) + if mapping != nil { + gv = mapping.GroupVersionKind.GroupVersion() + } + if obj, err := runtime.ObjectConvertor(s).ConvertToVersion(obj, gv); err == nil { + return obj + } + return obj +} + +// kubernetesNativeScheme returns a clean *runtime.Scheme with _only_ Kubernetes +// native resources added to it. This is required to break free of custom resources +// that may have been added to scheme.Scheme due to Helm being used as a package in +// combination with e.g. a versioned kube client. If we would not do this, the client +// may attempt to perform e.g. a 3-way-merge strategy patch for custom resources. +func kubernetesNativeScheme() *runtime.Scheme { + k8sNativeSchemeOnce.Do(func() { + k8sNativeScheme = runtime.NewScheme() + scheme.AddToScheme(k8sNativeScheme) + // API extensions are not in the above scheme set, + // and must thus be added separately. + apiextensionsv1beta1.AddToScheme(k8sNativeScheme) + apiextensionsv1.AddToScheme(k8sNativeScheme) + }) + return k8sNativeScheme +} diff --git a/pkg/kube/factory.go b/pkg/kube/factory.go new file mode 100644 index 00000000000..f47f9d9f667 --- /dev/null +++ b/pkg/kube/factory.go @@ -0,0 +1,38 @@ +/* +Copyright The Helm Authors. + +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 kube // import "helm.sh/helm/v3/pkg/kube" + +import ( + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/kubectl/pkg/validation" +) + +// Factory provides abstractions that allow the Kubectl command to be extended across multiple types +// of resources and different API sets. +type Factory interface { + // ToRawKubeConfigLoader return kubeconfig loader as-is + ToRawKubeConfigLoader() clientcmd.ClientConfig + // KubernetesClientSet gives you back an external clientset + KubernetesClientSet() (*kubernetes.Clientset, error) + // NewBuilder returns an object that assists in loading objects from both disk and the server + // and which implements the common patterns for CLI interactions with generic resources. + NewBuilder() *resource.Builder + // Returns a schema that can validate objects stored on disk. + Validator(validate bool) (validation.Schema, error) +} diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go new file mode 100644 index 00000000000..ff800864c9c --- /dev/null +++ b/pkg/kube/fake/fake.go @@ -0,0 +1,107 @@ +/* +Copyright The Helm Authors. + +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 fake implements various fake KubeClients for use in testing +package fake + +import ( + "io" + "time" + + v1 "k8s.io/api/core/v1" + "k8s.io/cli-runtime/pkg/resource" + + "helm.sh/helm/v3/pkg/kube" +) + +// FailingKubeClient implements KubeClient for testing purposes. It also has +// additional errors you can set to fail different functions, otherwise it +// delegates all its calls to `PrintingKubeClient` +type FailingKubeClient struct { + PrintingKubeClient + CreateError error + WaitError error + DeleteError error + WatchUntilReadyError error + UpdateError error + BuildError error + BuildUnstructuredError error + WaitAndGetCompletedPodPhaseError error +} + +// Create returns the configured error if set or prints +func (f *FailingKubeClient) Create(resources kube.ResourceList) (*kube.Result, error) { + if f.CreateError != nil { + return nil, f.CreateError + } + return f.PrintingKubeClient.Create(resources) +} + +// Wait returns the configured error if set or prints +func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error { + if f.WaitError != nil { + return f.WaitError + } + return f.PrintingKubeClient.Wait(resources, d) +} + +// WaitWithJobs returns the configured error if set or prints +func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Duration) error { + if f.WaitError != nil { + return f.WaitError + } + return f.PrintingKubeClient.Wait(resources, d) +} + +// Delete returns the configured error if set or prints +func (f *FailingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) { + if f.DeleteError != nil { + return nil, []error{f.DeleteError} + } + return f.PrintingKubeClient.Delete(resources) +} + +// WatchUntilReady returns the configured error if set or prints +func (f *FailingKubeClient) WatchUntilReady(resources kube.ResourceList, d time.Duration) error { + if f.WatchUntilReadyError != nil { + return f.WatchUntilReadyError + } + return f.PrintingKubeClient.WatchUntilReady(resources, d) +} + +// Update returns the configured error if set or prints +func (f *FailingKubeClient) Update(r, modified kube.ResourceList, ignoreMe bool) (*kube.Result, error) { + if f.UpdateError != nil { + return &kube.Result{}, f.UpdateError + } + return f.PrintingKubeClient.Update(r, modified, ignoreMe) +} + +// Build returns the configured error if set or prints +func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error) { + if f.BuildError != nil { + return []*resource.Info{}, f.BuildError + } + return f.PrintingKubeClient.Build(r, false) +} + +// WaitAndGetCompletedPodPhase returns the configured error if set or prints +func (f *FailingKubeClient) WaitAndGetCompletedPodPhase(s string, d time.Duration) (v1.PodPhase, error) { + if f.WaitAndGetCompletedPodPhaseError != nil { + return v1.PodSucceeded, f.WaitAndGetCompletedPodPhaseError + } + return f.PrintingKubeClient.WaitAndGetCompletedPodPhase(s, d) +} diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go new file mode 100644 index 00000000000..e8bd1845b96 --- /dev/null +++ b/pkg/kube/fake/printer.go @@ -0,0 +1,105 @@ +/* +Copyright The Helm Authors. + +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 fake + +import ( + "io" + "strings" + "time" + + v1 "k8s.io/api/core/v1" + "k8s.io/cli-runtime/pkg/resource" + + "helm.sh/helm/v3/pkg/kube" +) + +// PrintingKubeClient implements KubeClient, but simply prints the reader to +// the given output. +type PrintingKubeClient struct { + Out io.Writer +} + +// IsReachable checks if the cluster is reachable +func (p *PrintingKubeClient) IsReachable() error { + return nil +} + +// Create prints the values of what would be created with a real KubeClient. +func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result, error) { + _, err := io.Copy(p.Out, bufferize(resources)) + if err != nil { + return nil, err + } + return &kube.Result{Created: resources}, nil +} + +func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error { + _, err := io.Copy(p.Out, bufferize(resources)) + return err +} + +func (p *PrintingKubeClient) WaitWithJobs(resources kube.ResourceList, _ time.Duration) error { + _, err := io.Copy(p.Out, bufferize(resources)) + return err +} + +// Delete implements KubeClient delete. +// +// It only prints out the content to be deleted. +func (p *PrintingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) { + _, err := io.Copy(p.Out, bufferize(resources)) + if err != nil { + return nil, []error{err} + } + return &kube.Result{Deleted: resources}, nil +} + +// WatchUntilReady implements KubeClient WatchUntilReady. +func (p *PrintingKubeClient) WatchUntilReady(resources kube.ResourceList, _ time.Duration) error { + _, err := io.Copy(p.Out, bufferize(resources)) + return err +} + +// Update implements KubeClient Update. +func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, _ bool) (*kube.Result, error) { + _, err := io.Copy(p.Out, bufferize(modified)) + if err != nil { + return nil, err + } + // TODO: This doesn't completely mock out have some that get created, + // updated, and deleted. I don't think these are used in any unit tests, but + // we may want to refactor a way to handle future tests + return &kube.Result{Updated: modified}, nil +} + +// Build implements KubeClient Build. +func (p *PrintingKubeClient) Build(_ io.Reader, _ bool) (kube.ResourceList, error) { + return []*resource.Info{}, nil +} + +// WaitAndGetCompletedPodPhase implements KubeClient WaitAndGetCompletedPodPhase. +func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Duration) (v1.PodPhase, error) { + return v1.PodSucceeded, nil +} + +func bufferize(resources kube.ResourceList) io.Reader { + var builder strings.Builder + for _, info := range resources { + builder.WriteString(info.String() + "\n") + } + return strings.NewReader(builder.String()) +} diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go new file mode 100644 index 00000000000..545985996cb --- /dev/null +++ b/pkg/kube/interface.go @@ -0,0 +1,68 @@ +/* +Copyright The Helm Authors. + +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 kube + +import ( + "io" + "time" + + v1 "k8s.io/api/core/v1" +) + +// Interface represents a client capable of communicating with the Kubernetes API. +// +// A KubernetesClient must be concurrency safe. +type Interface interface { + // Create creates one or more resources. + Create(resources ResourceList) (*Result, error) + + Wait(resources ResourceList, timeout time.Duration) error + + WaitWithJobs(resources ResourceList, timeout time.Duration) error + + // Delete destroys one or more resources. + Delete(resources ResourceList) (*Result, []error) + + // Watch the resource in reader until it is "ready". This method + // + // For Jobs, "ready" means the Job ran to completion (exited without error). + // For Pods, "ready" means the Pod phase is marked "succeeded". + // For all other kinds, it means the kind was created or modified without + // error. + WatchUntilReady(resources ResourceList, timeout time.Duration) error + + // Update updates one or more resources or creates the resource + // if it doesn't exist. + Update(original, target ResourceList, force bool) (*Result, error) + + // Build creates a resource list from a Reader + // + // reader must contain a YAML stream (one or more YAML documents separated + // by "\n---\n") + // + // Validates against OpenAPI schema if validate is true. + Build(reader io.Reader, validate bool) (ResourceList, error) + + // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase + // and returns said phase (PodSucceeded or PodFailed qualify). + WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error) + + // isReachable checks whether the client is able to connect to the cluster + IsReachable() error +} + +var _ Interface = (*Client)(nil) diff --git a/pkg/kube/log.go b/pkg/kube/log.go deleted file mode 100644 index fbe51823a4e..00000000000 --- a/pkg/kube/log.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -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 kube - -import ( - "flag" - "fmt" - "os" -) - -func init() { - if level := os.Getenv("KUBE_LOG_LEVEL"); level != "" { - flag.Set("vmodule", fmt.Sprintf("loader=%s,round_trippers=%s,request=%s", level, level, level)) - flag.Set("logtostderr", "true") - } -} diff --git a/pkg/kube/namespace.go b/pkg/kube/namespace.go deleted file mode 100644 index 9d2793d875e..00000000000 --- a/pkg/kube/namespace.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -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 kube // import "k8s.io/helm/pkg/kube" - -import ( - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" -) - -func createNamespace(client internalclientset.Interface, namespace string) error { - ns := &core.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace, - }, - } - _, err := client.Core().Namespaces().Create(ns) - return err -} - -func getNamespace(client internalclientset.Interface, namespace string) (*core.Namespace, error) { - return client.Core().Namespaces().Get(namespace, metav1.GetOptions{}) -} - -func ensureNamespace(client internalclientset.Interface, namespace string) error { - _, err := getNamespace(client, namespace) - if err != nil && errors.IsNotFound(err) { - return createNamespace(client, namespace) - } - return err -} diff --git a/pkg/kube/namespace_test.go b/pkg/kube/namespace_test.go deleted file mode 100644 index eb96557d0de..00000000000 --- a/pkg/kube/namespace_test.go +++ /dev/null @@ -1,37 +0,0 @@ -/* -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 kube // import "k8s.io/helm/pkg/kube" - -import ( - "testing" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" -) - -func TestEnsureNamespace(t *testing.T) { - client := fake.NewSimpleClientset() - if err := ensureNamespace(client, "foo"); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if err := ensureNamespace(client, "foo"); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if _, err := client.Core().Namespaces().Get("foo", metav1.GetOptions{}); err != nil { - t.Fatalf("unexpected error: %s", err) - } -} diff --git a/pkg/kube/resource.go b/pkg/kube/resource.go new file mode 100644 index 00000000000..ee8f83a25e7 --- /dev/null +++ b/pkg/kube/resource.go @@ -0,0 +1,85 @@ +/* +Copyright The Helm Authors. + +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 kube // import "helm.sh/helm/v3/pkg/kube" + +import "k8s.io/cli-runtime/pkg/resource" + +// ResourceList provides convenience methods for comparing collections of Infos. +type ResourceList []*resource.Info + +// Append adds an Info to the Result. +func (r *ResourceList) Append(val *resource.Info) { + *r = append(*r, val) +} + +// Visit implements resource.Visitor. +func (r ResourceList) Visit(fn resource.VisitorFunc) error { + for _, i := range r { + if err := fn(i, nil); err != nil { + return err + } + } + return nil +} + +// Filter returns a new Result with Infos that satisfy the predicate fn. +func (r ResourceList) Filter(fn func(*resource.Info) bool) ResourceList { + var result ResourceList + for _, i := range r { + if fn(i) { + result.Append(i) + } + } + return result +} + +// Get returns the Info from the result that matches the name and kind. +func (r ResourceList) Get(info *resource.Info) *resource.Info { + for _, i := range r { + if isMatchingInfo(i, info) { + return i + } + } + return nil +} + +// Contains checks to see if an object exists. +func (r ResourceList) Contains(info *resource.Info) bool { + for _, i := range r { + if isMatchingInfo(i, info) { + return true + } + } + return false +} + +// Difference will return a new Result with objects not contained in rs. +func (r ResourceList) Difference(rs ResourceList) ResourceList { + return r.Filter(func(info *resource.Info) bool { + return !rs.Contains(info) + }) +} + +// Intersect will return a new Result with objects contained in both Results. +func (r ResourceList) Intersect(rs ResourceList) ResourceList { + return r.Filter(rs.Contains) +} + +// isMatchingInfo returns true if infos match on Name and GroupVersionKind. +func isMatchingInfo(a, b *resource.Info) bool { + return a.Name == b.Name && a.Namespace == b.Namespace && a.Mapping.GroupVersionKind.Kind == b.Mapping.GroupVersionKind.Kind +} diff --git a/pkg/kube/resource_policy.go b/pkg/kube/resource_policy.go new file mode 100644 index 00000000000..5f391eb503c --- /dev/null +++ b/pkg/kube/resource_policy.go @@ -0,0 +1,26 @@ +/* +Copyright The Helm Authors. + +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 kube // import "helm.sh/helm/v3/pkg/kube" + +// ResourcePolicyAnno is the annotation name for a resource policy +const ResourcePolicyAnno = "helm.sh/resource-policy" + +// KeepPolicy is the resource policy type for keep +// +// This resource policy type allows resources to skip being deleted +// during an uninstallRelease action. +const KeepPolicy = "keep" diff --git a/pkg/kube/resource_test.go b/pkg/kube/resource_test.go new file mode 100644 index 00000000000..3c906ceca51 --- /dev/null +++ b/pkg/kube/resource_test.go @@ -0,0 +1,61 @@ +/* +Copyright The Helm Authors. + +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 kube // import "helm.sh/helm/v3/pkg/kube" + +import ( + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/resource" +) + +func TestResourceList(t *testing.T) { + mapping := &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "group", Version: "version", Resource: "pod"}, + } + + info := func(name string) *resource.Info { + return &resource.Info{Name: name, Mapping: mapping} + } + + var r1, r2 ResourceList + r1 = []*resource.Info{info("foo"), info("bar")} + r2 = []*resource.Info{info("bar")} + + if r1.Get(info("bar")).Mapping.Resource.Resource != "pod" { + t.Error("expected get pod") + } + + diff := r1.Difference(r2) + if len(diff) != 1 { + t.Error("expected 1 result") + } + + if !diff.Contains(info("foo")) { + t.Error("expected diff to return foo") + } + + inter := r1.Intersect(r2) + if len(inter) != 1 { + t.Error("expected 1 result") + } + + if !inter.Contains(info("bar")) { + t.Error("expected intersect to return bar") + } +} diff --git a/pkg/kube/result.go b/pkg/kube/result.go index 87c7e6ac1c9..c3e171c2e4d 100644 --- a/pkg/kube/result.go +++ b/pkg/kube/result.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,74 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "k8s.io/helm/pkg/kube" +package kube -import "k8s.io/kubernetes/pkg/kubectl/resource" - -// Result provides convenience methods for comparing collections of Infos. -type Result []*resource.Info - -// Append adds an Info to the Result. -func (r *Result) Append(val *resource.Info) { - *r = append(*r, val) -} - -// Visit implements resource.Visitor. -func (r Result) Visit(fn resource.VisitorFunc) error { - for _, i := range r { - if err := fn(i, nil); err != nil { - return err - } - } - return nil -} - -// Filter returns a new Result with Infos that satisfy the predicate fn. -func (r Result) Filter(fn func(*resource.Info) bool) Result { - var result Result - for _, i := range r { - if fn(i) { - result.Append(i) - } - } - return result -} - -// Get returns the Info from the result that matches the name and kind. -func (r Result) Get(info *resource.Info) *resource.Info { - for _, i := range r { - if isMatchingInfo(i, info) { - return i - } - } - return nil +// Result contains the information of created, updated, and deleted resources +// for various kube API calls along with helper methods for using those +// resources +type Result struct { + Created ResourceList + Updated ResourceList + Deleted ResourceList } -// Contains checks to see if an object exists. -func (r Result) Contains(info *resource.Info) bool { - for _, i := range r { - if isMatchingInfo(i, info) { - return true - } - } - return false -} - -// Difference will return a new Result with objects not contained in rs. -func (r Result) Difference(rs Result) Result { - return r.Filter(func(info *resource.Info) bool { - return !rs.Contains(info) - }) -} - -// Intersect will return a new Result with objects contained in both Results. -func (r Result) Intersect(rs Result) Result { - return r.Filter(func(info *resource.Info) bool { - return rs.Contains(info) - }) -} - -// isMatchingInfo returns true if infos match on Name and GroupVersionKind. -func isMatchingInfo(a, b *resource.Info) bool { - return a.Name == b.Name && a.Mapping.GroupVersionKind.Kind == b.Mapping.GroupVersionKind.Kind -} +// If needed, we can add methods to the Result type for things like diffing diff --git a/pkg/kube/result_test.go b/pkg/kube/result_test.go deleted file mode 100644 index 962e9042612..00000000000 --- a/pkg/kube/result_test.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -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 kube // import "k8s.io/helm/pkg/kube" - -import ( - "testing" - - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/kubernetes/pkg/api/testapi" - "k8s.io/kubernetes/pkg/kubectl/resource" -) - -func TestResult(t *testing.T) { - mapping, err := testapi.Default.RESTMapper().RESTMapping(schema.GroupKind{Kind: "Pod"}) - if err != nil { - t.Fatal(err) - } - - info := func(name string) *resource.Info { - return &resource.Info{Name: name, Mapping: mapping} - } - - var r1, r2 Result - r1 = []*resource.Info{info("foo"), info("bar")} - r2 = []*resource.Info{info("bar")} - - diff := r1.Difference(r2) - if len(diff) != 1 { - t.Error("expected 1 result") - } - - if !diff.Contains(info("foo")) { - t.Error("expected diff to return foo") - } - - inter := r1.Intersect(r2) - if len(inter) != 1 { - t.Error("expected 1 result") - } - - if !inter.Contains(info("bar")) { - t.Error("expected intersect to return bar") - } -} diff --git a/pkg/kube/tunnel.go b/pkg/kube/tunnel.go deleted file mode 100644 index 08280f25d24..00000000000 --- a/pkg/kube/tunnel.go +++ /dev/null @@ -1,122 +0,0 @@ -/* -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 kube - -import ( - "fmt" - "io" - "io/ioutil" - "net" - "net/http" - "strconv" - - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/portforward" - "k8s.io/client-go/transport/spdy" -) - -// Tunnel describes a ssh-like tunnel to a kubernetes pod -type Tunnel struct { - Local int - Remote int - Namespace string - PodName string - Out io.Writer - stopChan chan struct{} - readyChan chan struct{} - config *rest.Config - client rest.Interface -} - -// NewTunnel creates a new tunnel -func NewTunnel(client rest.Interface, config *rest.Config, namespace, podName string, remote int) *Tunnel { - return &Tunnel{ - config: config, - client: client, - Namespace: namespace, - PodName: podName, - Remote: remote, - stopChan: make(chan struct{}, 1), - readyChan: make(chan struct{}, 1), - Out: ioutil.Discard, - } -} - -// Close disconnects a tunnel connection -func (t *Tunnel) Close() { - close(t.stopChan) -} - -// ForwardPort opens a tunnel to a kubernetes pod -func (t *Tunnel) ForwardPort() error { - // Build a url to the portforward endpoint - // example: http://localhost:8080/api/v1/namespaces/helm/pods/tiller-deploy-9itlq/portforward - u := t.client.Post(). - Resource("pods"). - Namespace(t.Namespace). - Name(t.PodName). - SubResource("portforward").URL() - - transport, upgrader, err := spdy.RoundTripperFor(t.config) - if err != nil { - return err - } - dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", u) - - local, err := getAvailablePort() - if err != nil { - return fmt.Errorf("could not find an available port: %s", err) - } - t.Local = local - - ports := []string{fmt.Sprintf("%d:%d", t.Local, t.Remote)} - - pf, err := portforward.New(dialer, ports, t.stopChan, t.readyChan, t.Out, t.Out) - if err != nil { - return err - } - - errChan := make(chan error) - go func() { - errChan <- pf.ForwardPorts() - }() - - select { - case err = <-errChan: - return fmt.Errorf("forwarding ports: %v", err) - case <-pf.Ready: - return nil - } -} - -func getAvailablePort() (int, error) { - l, err := net.Listen("tcp", ":0") - if err != nil { - return 0, err - } - defer l.Close() - - _, p, err := net.SplitHostPort(l.Addr().String()) - if err != nil { - return 0, err - } - port, err := strconv.Atoi(p) - if err != nil { - return 0, err - } - return port, err -} diff --git a/pkg/kube/tunnel_test.go b/pkg/kube/tunnel_test.go deleted file mode 100644 index 264200ddfd9..00000000000 --- a/pkg/kube/tunnel_test.go +++ /dev/null @@ -1,31 +0,0 @@ -/* -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 kube - -import ( - "testing" -) - -func TestAvailablePort(t *testing.T) { - port, err := getAvailablePort() - if err != nil { - t.Fatal(err) - } - if port < 1 { - t.Fatalf("generated port should be > 1, got %d", port) - } -} diff --git a/pkg/kube/wait.go b/pkg/kube/wait.go index 27ede15db96..489dd813269 100644 --- a/pkg/kube/wait.go +++ b/pkg/kube/wait.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,253 +14,399 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "k8s.io/helm/pkg/kube" +package kube // import "helm.sh/helm/v3/pkg/kube" import ( + "context" + "fmt" "time" + "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta2 "k8s.io/api/apps/v1beta2" - "k8s.io/api/core/v1" - extensions "k8s.io/api/extensions/v1beta1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" - podutil "k8s.io/kubernetes/pkg/api/v1/pod" - "k8s.io/kubernetes/pkg/apis/core/v1/helper" - deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util" + "k8s.io/client-go/kubernetes/scheme" + + deploymentutil "helm.sh/helm/v3/internal/third_party/k8s.io/kubernetes/deployment/util" ) -// deployment holds associated replicaSets for a deployment -type deployment struct { - replicaSets *extensions.ReplicaSet - deployment *extensions.Deployment +type waiter struct { + c kubernetes.Interface + timeout time.Duration + log func(string, ...interface{}) } -// waitForResources polls to get the current status of all pods, PVCs, and Services -// until all are ready or a timeout is reached -func (c *Client) waitForResources(timeout time.Duration, created Result) error { - c.Log("beginning wait for %d resources with timeout of %v", len(created), timeout) +// waitForResources polls to get the current status of all pods, PVCs, Services and +// Jobs(optional) until all are ready or a timeout is reached +func (w *waiter) waitForResources(created ResourceList, waitForJobsEnabled bool) error { + w.log("beginning wait for %d resources with timeout of %v", len(created), w.timeout) - kcs, err := c.KubernetesClientSet() - if err != nil { - return err - } - return wait.Poll(2*time.Second, timeout, func() (bool, error) { - pods := []v1.Pod{} - services := []v1.Service{} - pvc := []v1.PersistentVolumeClaim{} - deployments := []deployment{} + return wait.Poll(2*time.Second, w.timeout, func() (bool, error) { for _, v := range created { - obj, err := c.AsVersionedObject(v.Object) - if err != nil && !runtime.IsNotRegisteredError(err) { - return false, err - } - switch value := obj.(type) { - case *v1.ReplicationController: - list, err := getPods(kcs, value.Namespace, value.Spec.Selector) - if err != nil { + var ( + // This defaults to true, otherwise we get to a point where + // things will always return false unless one of the objects + // that manages pods has been hit + ok = true + err error + ) + switch value := AsVersioned(v).(type) { + case *corev1.Pod: + pod, err := w.c.CoreV1().Pods(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) + if err != nil || !w.isPodReady(pod) { return false, err } - pods = append(pods, list...) - case *v1.Pod: - pod, err := kcs.CoreV1().Pods(value.Namespace).Get(value.Name, metav1.GetOptions{}) - if err != nil { - return false, err + case *batchv1.Job: + if waitForJobsEnabled { + job, err := w.c.BatchV1().Jobs(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) + if err != nil || !w.jobReady(job) { + return false, err + } } - pods = append(pods, *pod) - case *appsv1.Deployment: - currentDeployment, err := kcs.ExtensionsV1beta1().Deployments(value.Namespace).Get(value.Name, metav1.GetOptions{}) + case *appsv1.Deployment, *appsv1beta1.Deployment, *appsv1beta2.Deployment, *extensionsv1beta1.Deployment: + currentDeployment, err := w.c.AppsV1().Deployments(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return false, err } - // Find RS associated with deployment - newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, kcs.ExtensionsV1beta1()) - if err != nil || newReplicaSet == nil { - return false, err - } - newDeployment := deployment{ - newReplicaSet, - currentDeployment, - } - deployments = append(deployments, newDeployment) - case *appsv1beta1.Deployment: - currentDeployment, err := kcs.ExtensionsV1beta1().Deployments(value.Namespace).Get(value.Name, metav1.GetOptions{}) - if err != nil { - return false, err + // If paused deployment will never be ready + if currentDeployment.Spec.Paused { + continue } // Find RS associated with deployment - newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, kcs.ExtensionsV1beta1()) + newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, w.c.AppsV1()) if err != nil || newReplicaSet == nil { return false, err } - newDeployment := deployment{ - newReplicaSet, - currentDeployment, + if !w.deploymentReady(newReplicaSet, currentDeployment) { + return false, nil } - deployments = append(deployments, newDeployment) - case *appsv1beta2.Deployment: - currentDeployment, err := kcs.ExtensionsV1beta1().Deployments(value.Namespace).Get(value.Name, metav1.GetOptions{}) + case *corev1.PersistentVolumeClaim: + claim, err := w.c.CoreV1().PersistentVolumeClaims(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return false, err } - // Find RS associated with deployment - newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, kcs.ExtensionsV1beta1()) - if err != nil || newReplicaSet == nil { - return false, err - } - newDeployment := deployment{ - newReplicaSet, - currentDeployment, + if !w.volumeReady(claim) { + return false, nil } - deployments = append(deployments, newDeployment) - case *extensions.Deployment: - currentDeployment, err := kcs.ExtensionsV1beta1().Deployments(value.Namespace).Get(value.Name, metav1.GetOptions{}) + case *corev1.Service: + svc, err := w.c.CoreV1().Services(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return false, err } - // Find RS associated with deployment - newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, kcs.ExtensionsV1beta1()) - if err != nil || newReplicaSet == nil { - return false, err - } - newDeployment := deployment{ - newReplicaSet, - currentDeployment, - } - deployments = append(deployments, newDeployment) - case *extensions.DaemonSet: - list, err := getPods(kcs, value.Namespace, value.Spec.Selector.MatchLabels) - if err != nil { - return false, err + if !w.serviceReady(svc) { + return false, nil } - pods = append(pods, list...) - case *appsv1.DaemonSet: - list, err := getPods(kcs, value.Namespace, value.Spec.Selector.MatchLabels) + case *extensionsv1beta1.DaemonSet, *appsv1.DaemonSet, *appsv1beta2.DaemonSet: + ds, err := w.c.AppsV1().DaemonSets(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return false, err } - pods = append(pods, list...) - case *appsv1beta2.DaemonSet: - list, err := getPods(kcs, value.Namespace, value.Spec.Selector.MatchLabels) - if err != nil { - return false, err + if !w.daemonSetReady(ds) { + return false, nil } - pods = append(pods, list...) - case *appsv1.StatefulSet: - list, err := getPods(kcs, value.Namespace, value.Spec.Selector.MatchLabels) - if err != nil { + case *apiextv1beta1.CustomResourceDefinition: + if err := v.Get(); err != nil { return false, err } - pods = append(pods, list...) - case *appsv1beta1.StatefulSet: - list, err := getPods(kcs, value.Namespace, value.Spec.Selector.MatchLabels) - if err != nil { + crd := &apiextv1beta1.CustomResourceDefinition{} + if err := scheme.Scheme.Convert(v.Object, crd, nil); err != nil { return false, err } - pods = append(pods, list...) - case *appsv1beta2.StatefulSet: - list, err := getPods(kcs, value.Namespace, value.Spec.Selector.MatchLabels) - if err != nil { - return false, err + if !w.crdBetaReady(*crd) { + return false, nil } - pods = append(pods, list...) - case *extensions.ReplicaSet: - list, err := getPods(kcs, value.Namespace, value.Spec.Selector.MatchLabels) - if err != nil { + case *apiextv1.CustomResourceDefinition: + if err := v.Get(); err != nil { return false, err } - pods = append(pods, list...) - case *appsv1beta2.ReplicaSet: - list, err := getPods(kcs, value.Namespace, value.Spec.Selector.MatchLabels) - if err != nil { + crd := &apiextv1.CustomResourceDefinition{} + if err := scheme.Scheme.Convert(v.Object, crd, nil); err != nil { return false, err } - pods = append(pods, list...) - case *appsv1.ReplicaSet: - list, err := getPods(kcs, value.Namespace, value.Spec.Selector.MatchLabels) - if err != nil { - return false, err + if !w.crdReady(*crd) { + return false, nil } - pods = append(pods, list...) - case *v1.PersistentVolumeClaim: - claim, err := kcs.CoreV1().PersistentVolumeClaims(value.Namespace).Get(value.Name, metav1.GetOptions{}) + case *appsv1.StatefulSet, *appsv1beta1.StatefulSet, *appsv1beta2.StatefulSet: + sts, err := w.c.AppsV1().StatefulSets(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return false, err } - pvc = append(pvc, *claim) - case *v1.Service: - svc, err := kcs.CoreV1().Services(value.Namespace).Get(value.Name, metav1.GetOptions{}) - if err != nil { - return false, err + if !w.statefulSetReady(sts) { + return false, nil } - services = append(services, *svc) + case *corev1.ReplicationController, *extensionsv1beta1.ReplicaSet, *appsv1beta2.ReplicaSet, *appsv1.ReplicaSet: + ok, err = w.podsReadyForObject(v.Namespace, value) + } + if !ok || err != nil { + return false, err } } - isReady := c.podsReady(pods) && c.servicesReady(services) && c.volumesReady(pvc) && c.deploymentsReady(deployments) - return isReady, nil + return true, nil }) } -func (c *Client) podsReady(pods []v1.Pod) bool { +func (w *waiter) podsReadyForObject(namespace string, obj runtime.Object) (bool, error) { + pods, err := w.podsforObject(namespace, obj) + if err != nil { + return false, err + } for _, pod := range pods { - if !podutil.IsPodReady(&pod) { - c.Log("Pod is not ready: %s/%s", pod.GetNamespace(), pod.GetName()) - return false + if !w.isPodReady(&pod) { + return false, nil } } - return true + return true, nil } -func (c *Client) servicesReady(svc []v1.Service) bool { - for _, s := range svc { - // ExternalName Services are external to cluster so helm shouldn't be checking to see if they're 'ready' (i.e. have an IP Set) - if s.Spec.Type == v1.ServiceTypeExternalName { - continue +func (w *waiter) podsforObject(namespace string, obj runtime.Object) ([]corev1.Pod, error) { + selector, err := SelectorsForObject(obj) + if err != nil { + return nil, err + } + list, err := getPods(w.c, namespace, selector.String()) + return list, err +} + +// isPodReady returns true if a pod is ready; false otherwise. +func (w *waiter) isPodReady(pod *corev1.Pod) bool { + for _, c := range pod.Status.Conditions { + if c.Type == corev1.PodReady && c.Status == corev1.ConditionTrue { + return true } + } + w.log("Pod is not ready: %s/%s", pod.GetNamespace(), pod.GetName()) + return false +} - // Make sure the service is not explicitly set to "None" before checking the IP - if s.Spec.ClusterIP != v1.ClusterIPNone && !helper.IsServiceIPSet(&s) { - c.Log("Service is not ready: %s/%s", s.GetNamespace(), s.GetName()) - return false +func (w *waiter) jobReady(job *batchv1.Job) bool { + if job.Status.Failed > *job.Spec.BackoffLimit { + w.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName()) + return false + } + if job.Status.Succeeded < *job.Spec.Completions { + w.log("Job is not completed: %s/%s", job.GetNamespace(), job.GetName()) + return false + } + return true +} + +func (w *waiter) serviceReady(s *corev1.Service) bool { + // ExternalName Services are external to cluster so helm shouldn't be checking to see if they're 'ready' (i.e. have an IP Set) + if s.Spec.Type == corev1.ServiceTypeExternalName { + return true + } + + // Ensure that the service cluster IP is not empty + if s.Spec.ClusterIP == "" { + w.log("Service does not have cluster IP address: %s/%s", s.GetNamespace(), s.GetName()) + return false + } + + // This checks if the service has a LoadBalancer and that balancer has an Ingress defined + if s.Spec.Type == corev1.ServiceTypeLoadBalancer { + // do not wait when at least 1 external IP is set + if len(s.Spec.ExternalIPs) > 0 { + w.log("Service %s/%s has external IP addresses (%v), marking as ready", s.GetNamespace(), s.GetName(), s.Spec.ExternalIPs) + return true } - // This checks if the service has a LoadBalancer and that balancer has an Ingress defined - if s.Spec.Type == v1.ServiceTypeLoadBalancer && s.Status.LoadBalancer.Ingress == nil { - c.Log("Service is not ready: %s/%s", s.GetNamespace(), s.GetName()) + + if s.Status.LoadBalancer.Ingress == nil { + w.log("Service does not have load balancer ingress IP address: %s/%s", s.GetNamespace(), s.GetName()) return false } } + return true } -func (c *Client) volumesReady(vols []v1.PersistentVolumeClaim) bool { - for _, v := range vols { - if v.Status.Phase != v1.ClaimBound { - c.Log("PersistentVolumeClaim is not ready: %s/%s", v.GetNamespace(), v.GetName()) - return false - } +func (w *waiter) volumeReady(v *corev1.PersistentVolumeClaim) bool { + if v.Status.Phase != corev1.ClaimBound { + w.log("PersistentVolumeClaim is not bound: %s/%s", v.GetNamespace(), v.GetName()) + return false } return true } -func (c *Client) deploymentsReady(deployments []deployment) bool { - for _, v := range deployments { - if !(v.replicaSets.Status.ReadyReplicas >= *v.deployment.Spec.Replicas-deploymentutil.MaxUnavailable(*v.deployment)) { - c.Log("Deployment is not ready: %s/%s", v.deployment.GetNamespace(), v.deployment.GetName()) - return false +func (w *waiter) deploymentReady(rs *appsv1.ReplicaSet, dep *appsv1.Deployment) bool { + expectedReady := *dep.Spec.Replicas - deploymentutil.MaxUnavailable(*dep) + if !(rs.Status.ReadyReplicas >= expectedReady) { + w.log("Deployment is not ready: %s/%s. %d out of %d expected pods are ready", dep.Namespace, dep.Name, rs.Status.ReadyReplicas, expectedReady) + return false + } + return true +} + +func (w *waiter) daemonSetReady(ds *appsv1.DaemonSet) bool { + // If the update strategy is not a rolling update, there will be nothing to wait for + if ds.Spec.UpdateStrategy.Type != appsv1.RollingUpdateDaemonSetStrategyType { + return true + } + + // Make sure all the updated pods have been scheduled + if ds.Status.UpdatedNumberScheduled != ds.Status.DesiredNumberScheduled { + w.log("DaemonSet is not ready: %s/%s. %d out of %d expected pods have been scheduled", ds.Namespace, ds.Name, ds.Status.UpdatedNumberScheduled, ds.Status.DesiredNumberScheduled) + return false + } + maxUnavailable, err := intstr.GetValueFromIntOrPercent(ds.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable, int(ds.Status.DesiredNumberScheduled), true) + if err != nil { + // If for some reason the value is invalid, set max unavailable to the + // number of desired replicas. This is the same behavior as the + // `MaxUnavailable` function in deploymentutil + maxUnavailable = int(ds.Status.DesiredNumberScheduled) + } + + expectedReady := int(ds.Status.DesiredNumberScheduled) - maxUnavailable + if !(int(ds.Status.NumberReady) >= expectedReady) { + w.log("DaemonSet is not ready: %s/%s. %d out of %d expected pods are ready", ds.Namespace, ds.Name, ds.Status.NumberReady, expectedReady) + return false + } + return true +} + +// Because the v1 extensions API is not available on all supported k8s versions +// yet and because Go doesn't support generics, we need to have a duplicate +// function to support the v1beta1 types +func (w *waiter) crdBetaReady(crd apiextv1beta1.CustomResourceDefinition) bool { + for _, cond := range crd.Status.Conditions { + switch cond.Type { + case apiextv1beta1.Established: + if cond.Status == apiextv1beta1.ConditionTrue { + return true + } + case apiextv1beta1.NamesAccepted: + if cond.Status == apiextv1beta1.ConditionFalse { + // This indicates a naming conflict, but it's probably not the + // job of this function to fail because of that. Instead, + // we treat it as a success, since the process should be able to + // continue. + return true + } } } + return false +} + +func (w *waiter) crdReady(crd apiextv1.CustomResourceDefinition) bool { + for _, cond := range crd.Status.Conditions { + switch cond.Type { + case apiextv1.Established: + if cond.Status == apiextv1.ConditionTrue { + return true + } + case apiextv1.NamesAccepted: + if cond.Status == apiextv1.ConditionFalse { + // This indicates a naming conflict, but it's probably not the + // job of this function to fail because of that. Instead, + // we treat it as a success, since the process should be able to + // continue. + return true + } + } + } + return false +} + +func (w *waiter) statefulSetReady(sts *appsv1.StatefulSet) bool { + // If the update strategy is not a rolling update, there will be nothing to wait for + if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType { + return true + } + + // Dereference all the pointers because StatefulSets like them + var partition int + // 1 is the default for replicas if not set + var replicas = 1 + // For some reason, even if the update strategy is a rolling update, the + // actual rollingUpdate field can be nil. If it is, we can safely assume + // there is no partition value + if sts.Spec.UpdateStrategy.RollingUpdate != nil && sts.Spec.UpdateStrategy.RollingUpdate.Partition != nil { + partition = int(*sts.Spec.UpdateStrategy.RollingUpdate.Partition) + } + if sts.Spec.Replicas != nil { + replicas = int(*sts.Spec.Replicas) + } + + // Because an update strategy can use partitioning, we need to calculate the + // number of updated replicas we should have. For example, if the replicas + // is set to 3 and the partition is 2, we'd expect only one pod to be + // updated + expectedReplicas := replicas - partition + + // Make sure all the updated pods have been scheduled + if int(sts.Status.UpdatedReplicas) != expectedReplicas { + w.log("StatefulSet is not ready: %s/%s. %d out of %d expected pods have been scheduled", sts.Namespace, sts.Name, sts.Status.UpdatedReplicas, expectedReplicas) + return false + } + + if int(sts.Status.ReadyReplicas) != replicas { + w.log("StatefulSet is not ready: %s/%s. %d out of %d expected pods are ready", sts.Namespace, sts.Name, sts.Status.ReadyReplicas, replicas) + return false + } return true } -func getPods(client kubernetes.Interface, namespace string, selector map[string]string) ([]v1.Pod, error) { - list, err := client.CoreV1().Pods(namespace).List(metav1.ListOptions{ - FieldSelector: fields.Everything().String(), - LabelSelector: labels.Set(selector).AsSelector().String(), +func getPods(client kubernetes.Interface, namespace, selector string) ([]corev1.Pod, error) { + list, err := client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: selector, }) return list.Items, err } + +// SelectorsForObject returns the pod label selector for a given object +// +// Modified version of https://github.com/kubernetes/kubernetes/blob/v1.14.1/pkg/kubectl/polymorphichelpers/helpers.go#L84 +func SelectorsForObject(object runtime.Object) (selector labels.Selector, err error) { + switch t := object.(type) { + case *extensionsv1beta1.ReplicaSet: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *appsv1.ReplicaSet: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *appsv1beta2.ReplicaSet: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *corev1.ReplicationController: + selector = labels.SelectorFromSet(t.Spec.Selector) + case *appsv1.StatefulSet: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *appsv1beta1.StatefulSet: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *appsv1beta2.StatefulSet: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *extensionsv1beta1.DaemonSet: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *appsv1.DaemonSet: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *appsv1beta2.DaemonSet: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *extensionsv1beta1.Deployment: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *appsv1.Deployment: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *appsv1beta1.Deployment: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *appsv1beta2.Deployment: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *batchv1.Job: + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + case *corev1.Service: + if t.Spec.Selector == nil || len(t.Spec.Selector) == 0 { + return nil, fmt.Errorf("invalid service '%s': Service is defined without a selector", t.Name) + } + selector = labels.SelectorFromSet(t.Spec.Selector) + + default: + return nil, fmt.Errorf("selector for %T not implemented", object) + } + + return selector, errors.Wrap(err, "invalid label selector") +} diff --git a/pkg/kube/wait_test.go b/pkg/kube/wait_test.go new file mode 100644 index 00000000000..7864f5e007e --- /dev/null +++ b/pkg/kube/wait_test.go @@ -0,0 +1,535 @@ +/* +Copyright The Helm Authors. + +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 kube // import "helm.sh/helm/v3/pkg/kube" + +import ( + "context" + "testing" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes/fake" +) + +const defaultNamespace = metav1.NamespaceDefault + +func Test_waiter_deploymentReady(t *testing.T) { + type args struct { + rs *appsv1.ReplicaSet + dep *appsv1.Deployment + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "deployment is ready", + args: args{ + rs: newReplicaSet("foo", 1, 1), + dep: newDeployment("foo", 1, 1, 0), + }, + want: true, + }, + { + name: "deployment is not ready", + args: args{ + rs: newReplicaSet("foo", 0, 0), + dep: newDeployment("foo", 1, 1, 0), + }, + want: false, + }, + { + name: "deployment is ready when maxUnavailable is set", + args: args{ + rs: newReplicaSet("foo", 2, 1), + dep: newDeployment("foo", 2, 1, 1), + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &waiter{ + c: fake.NewSimpleClientset(), + log: nopLogger, + } + if got := w.deploymentReady(tt.args.rs, tt.args.dep); got != tt.want { + t.Errorf("deploymentReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_waiter_daemonSetReady(t *testing.T) { + type args struct { + ds *appsv1.DaemonSet + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "daemonset is ready", + args: args{ + ds: newDaemonSet("foo", 0, 1, 1, 1), + }, + want: true, + }, + { + name: "daemonset is not ready", + args: args{ + ds: newDaemonSet("foo", 0, 0, 1, 1), + }, + want: false, + }, + { + name: "daemonset pods have not been scheduled successfully", + args: args{ + ds: newDaemonSet("foo", 0, 0, 1, 0), + }, + want: false, + }, + { + name: "daemonset is ready when maxUnavailable is set", + args: args{ + ds: newDaemonSet("foo", 1, 1, 2, 2), + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &waiter{ + c: fake.NewSimpleClientset(), + log: nopLogger, + } + if got := w.daemonSetReady(tt.args.ds); got != tt.want { + t.Errorf("daemonSetReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_waiter_statefulSetReady(t *testing.T) { + type args struct { + sts *appsv1.StatefulSet + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "statefulset is ready", + args: args{ + sts: newStatefulSet("foo", 1, 0, 1, 1), + }, + want: true, + }, + { + name: "statefulset is not ready", + args: args{ + sts: newStatefulSet("foo", 1, 0, 0, 1), + }, + want: false, + }, + { + name: "statefulset is ready when partition is specified", + args: args{ + sts: newStatefulSet("foo", 2, 1, 2, 1), + }, + want: true, + }, + { + name: "statefulset is not ready when partition is set", + args: args{ + sts: newStatefulSet("foo", 1, 1, 1, 1), + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &waiter{ + c: fake.NewSimpleClientset(), + log: nopLogger, + } + if got := w.statefulSetReady(tt.args.sts); got != tt.want { + t.Errorf("statefulSetReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_waiter_podsReadyForObject(t *testing.T) { + type args struct { + namespace string + obj runtime.Object + } + tests := []struct { + name string + args args + existPods []corev1.Pod + want bool + wantErr bool + }{ + { + name: "pods ready for a replicaset", + args: args{ + namespace: defaultNamespace, + obj: newReplicaSet("foo", 1, 1), + }, + existPods: []corev1.Pod{ + *newPodWithCondition("foo", corev1.ConditionTrue), + }, + want: true, + wantErr: false, + }, + { + name: "pods not ready for a replicaset", + args: args{ + namespace: defaultNamespace, + obj: newReplicaSet("foo", 1, 1), + }, + existPods: []corev1.Pod{ + *newPodWithCondition("foo", corev1.ConditionFalse), + }, + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &waiter{ + c: fake.NewSimpleClientset(), + log: nopLogger, + } + for _, pod := range tt.existPods { + if _, err := w.c.CoreV1().Pods(defaultNamespace).Create(context.TODO(), &pod, metav1.CreateOptions{}); err != nil { + t.Errorf("Failed to create Pod error: %v", err) + return + } + } + got, err := w.podsReadyForObject(tt.args.namespace, tt.args.obj) + if (err != nil) != tt.wantErr { + t.Errorf("podsReadyForObject() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("podsReadyForObject() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_waiter_jobReady(t *testing.T) { + type args struct { + job *batchv1.Job + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "job is completed", + args: args{job: newJob("foo", 1, 1, 1, 0)}, + want: true, + }, + { + name: "job is incomplete", + args: args{job: newJob("foo", 1, 1, 0, 0)}, + want: false, + }, + { + name: "job is failed", + args: args{job: newJob("foo", 1, 1, 0, 1)}, + want: false, + }, + { + name: "job is completed with retry", + args: args{job: newJob("foo", 1, 1, 1, 1)}, + want: true, + }, + { + name: "job is failed with retry", + args: args{job: newJob("foo", 1, 1, 0, 2)}, + want: false, + }, + { + name: "job is completed single run", + args: args{job: newJob("foo", 0, 1, 1, 0)}, + want: true, + }, + { + name: "job is failed single run", + args: args{job: newJob("foo", 0, 1, 0, 1)}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &waiter{ + c: fake.NewSimpleClientset(), + log: nopLogger, + } + if got := w.jobReady(tt.args.job); got != tt.want { + t.Errorf("jobReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_waiter_volumeReady(t *testing.T) { + type args struct { + v *corev1.PersistentVolumeClaim + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "pvc is bound", + args: args{ + v: newPersistentVolumeClaim("foo", corev1.ClaimBound), + }, + want: true, + }, + { + name: "pvc is not ready", + args: args{ + v: newPersistentVolumeClaim("foo", corev1.ClaimPending), + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &waiter{ + c: fake.NewSimpleClientset(), + log: nopLogger, + } + if got := w.volumeReady(tt.args.v); got != tt.want { + t.Errorf("volumeReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func newDaemonSet(name string, maxUnavailable, numberReady, desiredNumberScheduled, updatedNumberScheduled int) *appsv1.DaemonSet { + return &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: defaultNamespace, + }, + Spec: appsv1.DaemonSetSpec{ + UpdateStrategy: appsv1.DaemonSetUpdateStrategy{ + Type: appsv1.RollingUpdateDaemonSetStrategyType, + RollingUpdate: &appsv1.RollingUpdateDaemonSet{ + MaxUnavailable: func() *intstr.IntOrString { i := intstr.FromInt(maxUnavailable); return &i }(), + }, + }, + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"name": name}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"name": name}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx", + }, + }, + }, + }, + }, + Status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: int32(desiredNumberScheduled), + NumberReady: int32(numberReady), + UpdatedNumberScheduled: int32(updatedNumberScheduled), + }, + } +} + +func newStatefulSet(name string, replicas, partition, readyReplicas, updatedReplicas int) *appsv1.StatefulSet { + return &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: defaultNamespace, + }, + Spec: appsv1.StatefulSetSpec{ + UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.RollingUpdateStatefulSetStrategyType, + RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ + Partition: intToInt32(partition), + }, + }, + Replicas: intToInt32(replicas), + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"name": name}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"name": name}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx", + }, + }, + }, + }, + }, + Status: appsv1.StatefulSetStatus{ + UpdatedReplicas: int32(updatedReplicas), + ReadyReplicas: int32(readyReplicas), + }, + } +} + +func newDeployment(name string, replicas, maxSurge, maxUnavailable int) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: defaultNamespace, + }, + Spec: appsv1.DeploymentSpec{ + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxUnavailable: func() *intstr.IntOrString { i := intstr.FromInt(maxUnavailable); return &i }(), + MaxSurge: func() *intstr.IntOrString { i := intstr.FromInt(maxSurge); return &i }(), + }, + }, + Replicas: intToInt32(replicas), + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"name": name}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"name": name}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx", + }, + }, + }, + }, + }, + } +} + +func newReplicaSet(name string, replicas int, readyReplicas int) *appsv1.ReplicaSet { + d := newDeployment(name, replicas, 0, 0) + return &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: defaultNamespace, + Labels: d.Spec.Selector.MatchLabels, + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(d, d.GroupVersionKind())}, + }, + Spec: appsv1.ReplicaSetSpec{ + Selector: d.Spec.Selector, + Replicas: intToInt32(replicas), + Template: d.Spec.Template, + }, + Status: appsv1.ReplicaSetStatus{ + ReadyReplicas: int32(readyReplicas), + }, + } +} + +func newPodWithCondition(name string, podReadyCondition corev1.ConditionStatus) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: defaultNamespace, + Labels: map[string]string{"name": name}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx", + }, + }, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: podReadyCondition, + }, + }, + }, + } +} + +func newPersistentVolumeClaim(name string, phase corev1.PersistentVolumeClaimPhase) *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: defaultNamespace, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: phase, + }, + } +} + +func newJob(name string, backoffLimit, completions, succeeded, failed int) *batchv1.Job { + return &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: defaultNamespace, + }, + Spec: batchv1.JobSpec{ + BackoffLimit: intToInt32(backoffLimit), + Completions: intToInt32(completions), + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"name": name}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "nginx", + }, + }, + }, + }, + }, + Status: batchv1.JobStatus{ + Succeeded: int32(succeeded), + Failed: int32(failed), + }, + } +} + +func intToInt32(i int) *int32 { + i32 := int32(i) + return &i32 +} diff --git a/pkg/lint/lint.go b/pkg/lint/lint.go index 256eab90612..67e76bd3d4d 100644 --- a/pkg/lint/lint.go +++ b/pkg/lint/lint.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,23 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -package lint // import "k8s.io/helm/pkg/lint" +package lint // import "helm.sh/helm/v3/pkg/lint" import ( "path/filepath" - "k8s.io/helm/pkg/lint/rules" - "k8s.io/helm/pkg/lint/support" + "helm.sh/helm/v3/pkg/lint/rules" + "helm.sh/helm/v3/pkg/lint/support" ) // All runs all of the available linters on the given base directory. -func All(basedir string, values []byte, namespace string, strict bool) support.Linter { +func All(basedir string, values map[string]interface{}, namespace string, strict bool) support.Linter { // Using abs path to get directory context chartDir, _ := filepath.Abs(basedir) linter := support.Linter{ChartDir: chartDir} rules.Chartfile(&linter) - rules.Values(&linter) + rules.ValuesWithOverrides(&linter, values) rules.Templates(&linter, values, namespace, strict) + rules.Dependencies(&linter) return linter } diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go index d84faa10b8e..29ed67026fa 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,14 +17,16 @@ limitations under the License. package lint import ( + "io/ioutil" + "os" "strings" - - "k8s.io/helm/pkg/lint/support" - "testing" + + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/lint/support" ) -var values = []byte{} +var values map[string]interface{} const namespace = "testNamespace" const strict = false @@ -36,12 +38,12 @@ const goodChartDir = "rules/testdata/goodone" func TestBadChart(t *testing.T) { m := All(badChartDir, values, namespace, strict).Messages - if len(m) != 5 { + if len(m) != 8 { t.Errorf("Number of errors %v", len(m)) t.Errorf("All didn't fail with expected errors, got %#v", m) } - // There should be one INFO, 2 WARNINGs and one ERROR messages, check for them - var i, w, e, e2, e3 bool + // There should be one INFO, 2 WARNINGs and 2 ERROR messages, check for them + var i, w, e, e2, e3, e4, e5, e6 bool for _, msg := range m { if msg.Severity == support.InfoSev { if strings.Contains(msg.Err.Error(), "icon is recommended") { @@ -54,18 +56,31 @@ func TestBadChart(t *testing.T) { } } if msg.Severity == support.ErrorSev { - if strings.Contains(msg.Err.Error(), "version 0.0.0 is less than or equal to 0") { + if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVer") { e = true } if strings.Contains(msg.Err.Error(), "name is required") { e2 = true } - if strings.Contains(msg.Err.Error(), "directory name (badchartfile) and chart name () must be the same") { + + if strings.Contains(msg.Err.Error(), "apiVersion is required. The value must be either \"v1\" or \"v2\"") { e3 = true } + + if strings.Contains(msg.Err.Error(), "chart type is not valid in apiVersion") { + e4 = true + } + + if strings.Contains(msg.Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { + e5 = true + } + // This comes from the dependency check, which loads dependency info from the Chart.yaml + if strings.Contains(msg.Err.Error(), "unable to load chart") { + e6 = true + } } } - if !e || !e2 || !e3 || !w || !i { + if !e || !e2 || !e3 || !e4 || !e5 || !w || !i || !e6 { t.Errorf("Didn't find all the expected errors, got %#v", m) } } @@ -82,10 +97,10 @@ func TestInvalidYaml(t *testing.T) { func TestBadValues(t *testing.T) { m := All(badValuesFileDir, values, namespace, strict).Messages - if len(m) != 1 { + if len(m) < 1 { t.Fatalf("All didn't fail with expected errors, got %#v", m) } - if !strings.Contains(m[0].Err.Error(), "cannot unmarshal") { + if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") { t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err) } } @@ -93,6 +108,39 @@ func TestBadValues(t *testing.T) { func TestGoodChart(t *testing.T) { m := All(goodChartDir, values, namespace, strict).Messages if len(m) != 0 { - t.Errorf("All failed but shouldn't have: %#v", m) + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// TestHelmCreateChart tests that a `helm create` always passes a `helm lint` test. +// +// See https://github.com/helm/helm/issues/7923 +func TestHelmCreateChart(t *testing.T) { + dir, err := ioutil.TempDir("", "-helm-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir) + if err != nil { + t.Error(err) + // Fatal is bad because of the defer. + return + } + + // Note: we test with strict=true here, even though others have + // strict = false. + m := All(createdChart, values, namespace, true).Messages + if ll := len(m); ll != 1 { + t.Errorf("All should have had exactly 1 error. Got %d", ll) + for i, msg := range m { + t.Logf("Message %d: %s", i, msg.Error()) + } + } else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") { + t.Errorf("Unexpected lint error: %s", msg) } } diff --git a/pkg/lint/rules/chartfile.go b/pkg/lint/rules/chartfile.go index 0dab0d250c7..b49f2cec0b6 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/lint/rules/chartfile.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,21 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rules // import "k8s.io/helm/pkg/lint/rules" +package rules // import "helm.sh/helm/v3/pkg/lint/rules" import ( - "errors" "fmt" + "io/ioutil" "os" "path/filepath" - "strings" - - "github.com/Masterminds/semver" + "github.com/Masterminds/semver/v3" "github.com/asaskevich/govalidator" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/lint/support" - "k8s.io/helm/pkg/proto/hapi/chart" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/lint/support" ) // Chartfile runs a set of linter rules related to Chart.yaml file @@ -41,21 +42,49 @@ func Chartfile(linter *support.Linter) { chartFile, err := chartutil.LoadChartfile(chartPath) validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err)) - // Guard clause. Following linter rules require a parseable ChartFile + // Guard clause. Following linter rules require a parsable ChartFile if !validChartFile { return } + // type check for Chart.yaml . ignoring error as any parse + // errors would already be caught in the above load function + chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile)) - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartNameDirMatch(linter.ChartDir, chartFile)) // Chart metadata + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile)) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile)) - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartEngine(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile)) linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile)) +} + +func validateChartVersionType(data map[string]interface{}) error { + return isStringValue(data, "version") +} + +func validateChartAppVersionType(data map[string]interface{}) error { + return isStringValue(data, "appVersion") +} + +func isStringValue(data map[string]interface{}, key string) error { + value, ok := data[key] + if !ok { + return nil + } + valueType := fmt.Sprintf("%T", value) + if valueType != "string" { + return errors.Errorf("%s should be of type string but it's of type %s", key, valueType) + } + return nil } func validateChartYamlNotDirectory(chartPath string) error { @@ -69,7 +98,7 @@ func validateChartYamlNotDirectory(chartPath string) error { func validateChartYamlFormat(chartFileError error) error { if chartFileError != nil { - return fmt.Errorf("unable to parse YAML\n\t%s", chartFileError.Error()) + return errors.Errorf("unable to parse YAML\n\t%s", chartFileError.Error()) } return nil } @@ -81,10 +110,15 @@ func validateChartName(cf *chart.Metadata) error { return nil } -func validateChartNameDirMatch(chartDir string, cf *chart.Metadata) error { - if cf.Name != filepath.Base(chartDir) { - return fmt.Errorf("directory name (%s) and chart name (%s) must be the same", filepath.Base(chartDir), cf.Name) +func validateChartAPIVersion(cf *chart.Metadata) error { + if cf.APIVersion == "" { + return errors.New("apiVersion is required. The value must be either \"v1\" or \"v2\"") + } + + if cf.APIVersion != chart.APIVersionV1 && cf.APIVersion != chart.APIVersionV2 { + return fmt.Errorf("apiVersion '%s' is not valid. The value must be either \"v1\" or \"v2\"", cf.APIVersion) } + return nil } @@ -96,53 +130,30 @@ func validateChartVersion(cf *chart.Metadata) error { version, err := semver.NewVersion(cf.Version) if err != nil { - return fmt.Errorf("version '%s' is not a valid SemVer", cf.Version) + return errors.Errorf("version '%s' is not a valid SemVer", cf.Version) } - c, err := semver.NewConstraint("> 0") + c, err := semver.NewConstraint(">0.0.0-0") if err != nil { return err } valid, msg := c.Validate(version) if !valid && len(msg) > 0 { - return fmt.Errorf("version %v", msg[0]) + return errors.Errorf("version %v", msg[0]) } return nil } -func validateChartEngine(cf *chart.Metadata) error { - if cf.Engine == "" { - return nil - } - - keys := make([]string, 0, len(chart.Metadata_Engine_value)) - for engine := range chart.Metadata_Engine_value { - str := strings.ToLower(engine) - - if str == "unknown" { - continue - } - - if str == cf.Engine { - return nil - } - - keys = append(keys, str) - } - - return fmt.Errorf("engine '%v' not valid. Valid options are %v", cf.Engine, keys) -} - func validateChartMaintainer(cf *chart.Metadata) error { for _, maintainer := range cf.Maintainers { if maintainer.Name == "" { return errors.New("each maintainer requires a name") } else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) { - return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name) - } else if maintainer.Url != "" && !govalidator.IsURL(maintainer.Url) { - return fmt.Errorf("invalid url '%s' for maintainer '%s'", maintainer.Url, maintainer.Name) + return errors.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name) + } else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) { + return errors.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name) } } return nil @@ -151,7 +162,7 @@ func validateChartMaintainer(cf *chart.Metadata) error { func validateChartSources(cf *chart.Metadata) error { for _, source := range cf.Sources { if source == "" || !govalidator.IsRequestURL(source) { - return fmt.Errorf("invalid source URL '%s'", source) + return errors.Errorf("invalid source URL '%s'", source) } } return nil @@ -166,7 +177,34 @@ func validateChartIconPresence(cf *chart.Metadata) error { func validateChartIconURL(cf *chart.Metadata) error { if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) { - return fmt.Errorf("invalid icon URL '%s'", cf.Icon) + return errors.Errorf("invalid icon URL '%s'", cf.Icon) + } + return nil +} + +func validateChartDependencies(cf *chart.Metadata) error { + if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV2 { + return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV2) + } + return nil +} + +func validateChartType(cf *chart.Metadata) error { + if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV2 { + return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV2) } return nil } + +// loadChartFileForTypeCheck loads the Chart.yaml +// in a generic form of a map[string]interface{}, so that the type +// of the values can be checked +func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + y := make(map[string]interface{}) + err = yaml.Unmarshal(b, &y) + return y, err +} diff --git a/pkg/lint/rules/chartfile_test.go b/pkg/lint/rules/chartfile_test.go index 99dc4de0f51..087cda047c0 100644 --- a/pkg/lint/rules/chartfile_test.go +++ b/pkg/lint/rules/chartfile_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,30 +17,29 @@ limitations under the License. package rules import ( - "errors" "os" "path/filepath" "strings" "testing" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/lint/support" - "k8s.io/helm/pkg/proto/hapi/chart" + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/lint/support" ) const ( - badChartDir = "testdata/badchartfile" - goodChartDir = "testdata/goodone" + badChartDir = "testdata/badchartfile" + anotherBadChartDir = "testdata/anotherbadchartfile" ) 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) +var badChart, _ = chartutil.LoadChartfile(badChartFilePath) // Validation functions Test func TestValidateChartYamlNotDirectory(t *testing.T) { @@ -72,31 +71,13 @@ func TestValidateChartName(t *testing.T) { } } -func TestValidateChartNameDirMatch(t *testing.T) { - err := validateChartNameDirMatch(goodChartDir, goodChart) - if err != nil { - t.Errorf("validateChartNameDirMatch to return no error, gor a linter error") - } - // It has not name - err = validateChartNameDirMatch(badChartDir, badChart) - if err == nil { - t.Errorf("validatechartnamedirmatch to return a linter error, got no error") - } - - // Wrong path - err = validateChartNameDirMatch(badChartDir, goodChart) - if err == nil { - t.Errorf("validatechartnamedirmatch to return a linter error, got no error") - } -} - func TestValidateChartVersion(t *testing.T) { var failTest = []struct { Version string ErrorMsg string }{ {"", "version is required"}, - {"0", "0 is less than or equal to 0"}, + {"1.2.3.4", "version '1.2.3.4' is not a valid SemVer"}, {"waps", "'waps' is not a valid SemVer"}, {"-3", "'-3' is not a valid SemVer"}, } @@ -120,24 +101,6 @@ func TestValidateChartVersion(t *testing.T) { } } -func TestValidateChartEngine(t *testing.T) { - var successTest = []string{"", "gotpl"} - - for _, engine := range successTest { - badChart.Engine = engine - err := validateChartEngine(badChart) - if err != nil { - t.Errorf("validateChartEngine(%s) to return no error, got a linter error %s", engine, err.Error()) - } - } - - badChart.Engine = "foobar" - err := validateChartEngine(badChart) - if err == nil || !strings.Contains(err.Error(), "not valid. Valid options are [gotpl") { - t.Errorf("validateChartEngine(%s) to return an error, got no error", badChart.Engine) - } -} - func TestValidateChartMaintainer(t *testing.T) { var failTest = []struct { Name string @@ -222,28 +185,63 @@ func TestValidateChartIconURL(t *testing.T) { } func TestChartfile(t *testing.T) { - linter := support.Linter{ChartDir: badChartDir} - Chartfile(&linter) - msgs := linter.Messages + t.Run("Chart.yaml basic validity issues", func(t *testing.T) { + linter := support.Linter{ChartDir: badChartDir} + Chartfile(&linter) + msgs := linter.Messages + expectedNumberOfErrorMessages := 6 + + if len(msgs) != expectedNumberOfErrorMessages { + t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) + return + } - if len(msgs) != 4 { - t.Errorf("Expected 3 errors, got %d", len(msgs)) - } + if !strings.Contains(msgs[0].Err.Error(), "name is required") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) + } - if !strings.Contains(msgs[0].Err.Error(), "name is required") { - t.Errorf("Unexpected message 0: %s", msgs[0].Err) - } + if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be either \"v1\" or \"v2\"") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) + } - 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].Err.Error(), "version '0.0.0.0' is not a valid SemVer") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) + } - 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) - } + if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") { + t.Errorf("Unexpected message 3: %s", msgs[3].Err) + } - if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") { - t.Errorf("Unexpected message 3: %s", msgs[3].Err) - } + if !strings.Contains(msgs[4].Err.Error(), "chart type is not valid in apiVersion") { + t.Errorf("Unexpected message 4: %s", msgs[4].Err) + } + if !strings.Contains(msgs[5].Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { + t.Errorf("Unexpected message 5: %s", msgs[5].Err) + } + }) + + t.Run("Chart.yaml validity issues due to type mismatch", func(t *testing.T) { + linter := support.Linter{ChartDir: anotherBadChartDir} + Chartfile(&linter) + msgs := linter.Messages + expectedNumberOfErrorMessages := 3 + + if len(msgs) != expectedNumberOfErrorMessages { + t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) + return + } + + if !strings.Contains(msgs[0].Err.Error(), "version should be of type string") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) + } + + if !strings.Contains(msgs[1].Err.Error(), "version '7.2445e+06' is not a valid SemVer") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) + } + + if !strings.Contains(msgs[2].Err.Error(), "appVersion should be of type string") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) + } + }) } diff --git a/pkg/lint/rules/dependencies.go b/pkg/lint/rules/dependencies.go new file mode 100644 index 00000000000..abecd1feb63 --- /dev/null +++ b/pkg/lint/rules/dependencies.go @@ -0,0 +1,82 @@ +/* +Copyright The Helm Authors. + +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 rules // import "helm.sh/helm/v3/pkg/lint/rules" + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/lint/support" +) + +// Dependencies runs lints against a chart's dependencies +// +// See https://github.com/helm/helm/issues/7910 +func Dependencies(linter *support.Linter) { + c, err := loader.LoadDir(linter.ChartDir) + if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) { + return + } + + linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c)) + linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c)) +} + +func validateChartFormat(chartError error) error { + if chartError != nil { + return errors.Errorf("unable to load chart\n\t%s", chartError) + } + return nil +} + +func validateDependencyInChartsDir(c *chart.Chart) (err error) { + dependencies := map[string]struct{}{} + missing := []string{} + for _, dep := range c.Dependencies() { + dependencies[dep.Metadata.Name] = struct{}{} + } + for _, dep := range c.Metadata.Dependencies { + if _, ok := dependencies[dep.Name]; !ok { + missing = append(missing, dep.Name) + } + } + if len(missing) > 0 { + err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ",")) + } + return err +} + +func validateDependencyInMetadata(c *chart.Chart) (err error) { + dependencies := map[string]struct{}{} + missing := []string{} + for _, dep := range c.Metadata.Dependencies { + dependencies[dep.Name] = struct{}{} + } + for _, dep := range c.Dependencies() { + if _, ok := dependencies[dep.Metadata.Name]; !ok { + missing = append(missing, dep.Metadata.Name) + } + } + if len(missing) > 0 { + err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ",")) + } + return err +} diff --git a/pkg/lint/rules/dependencies_test.go b/pkg/lint/rules/dependencies_test.go new file mode 100644 index 00000000000..075190eacd6 --- /dev/null +++ b/pkg/lint/rules/dependencies_test.go @@ -0,0 +1,99 @@ +/* +Copyright The Helm Authors. + +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 rules + +import ( + "os" + "path/filepath" + "testing" + + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/lint/support" +) + +func chartWithBadDependencies() chart.Chart { + badChartDeps := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "sub2", + }, + { + Name: "sub3", + }, + }, + }, + } + + badChartDeps.SetDependencies( + &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "sub1", + Version: "0.1.0", + APIVersion: "v2", + }, + }, + &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "sub2", + Version: "0.1.0", + APIVersion: "v2", + }, + }, + ) + return badChartDeps +} + +func TestValidateDependencyInChartsDir(t *testing.T) { + c := chartWithBadDependencies() + + if err := validateDependencyInChartsDir(&c); err == nil { + t.Error("chart should have been flagged for missing deps in chart directory") + } +} + +func TestValidateDependencyInMetadata(t *testing.T) { + c := chartWithBadDependencies() + + if err := validateDependencyInMetadata(&c); err == nil { + t.Errorf("chart should have been flagged for missing deps in chart metadata") + } +} + +func TestDependencies(t *testing.T) { + tmp := ensure.TempDir(t) + defer os.RemoveAll(tmp) + + c := chartWithBadDependencies() + err := chartutil.SaveDir(&c, tmp) + if err != nil { + t.Fatal(err) + } + linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)} + + Dependencies(&linter) + if l := len(linter.Messages); l != 2 { + t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l) + for i, msg := range linter.Messages { + t.Logf("Message: %d, Error: %#v", i, msg) + } + } +} diff --git a/pkg/lint/rules/deprecations.go b/pkg/lint/rules/deprecations.go new file mode 100644 index 00000000000..384c179736f --- /dev/null +++ b/pkg/lint/rules/deprecations.go @@ -0,0 +1,84 @@ +/* +Copyright The Helm Authors. + +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 rules // import "helm.sh/helm/v3/pkg/lint/rules" + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/endpoints/deprecation" + kscheme "k8s.io/client-go/kubernetes/scheme" +) + +const ( + // This should be set in the Makefile based on the version of client-go being imported. + // These constants will be overwritten with LDFLAGS + k8sVersionMajor = 1 + k8sVersionMinor = 20 +) + +// deprecatedAPIError indicates than an API is deprecated in Kubernetes +type deprecatedAPIError struct { + Deprecated string + Message string +} + +func (e deprecatedAPIError) Error() string { + msg := e.Message + return msg +} + +func validateNoDeprecations(resource *K8sYamlStruct) error { + // if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation + if resource.APIVersion == "" { + return nil + } + if resource.Kind == "" { + return nil + } + + runtimeObject, err := resourceToRuntimeObject(resource) + if err != nil { + // do not error for non-kubernetes resources + if runtime.IsNotRegisteredError(err) { + return nil + } + return err + } + if !deprecation.IsDeprecated(runtimeObject, k8sVersionMajor, k8sVersionMinor) { + return nil + } + gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind) + return deprecatedAPIError{ + Deprecated: gvk, + Message: deprecation.WarningMessage(runtimeObject), + } +} + +func resourceToRuntimeObject(resource *K8sYamlStruct) (runtime.Object, error) { + scheme := runtime.NewScheme() + kscheme.AddToScheme(scheme) + + gvk := schema.FromAPIVersionAndKind(resource.APIVersion, resource.Kind) + out, err := scheme.New(gvk) + if err != nil { + return nil, err + } + out.GetObjectKind().SetGroupVersionKind(gvk) + return out, nil +} diff --git a/pkg/lint/rules/deprecations_test.go b/pkg/lint/rules/deprecations_test.go new file mode 100644 index 00000000000..96e072d1422 --- /dev/null +++ b/pkg/lint/rules/deprecations_test.go @@ -0,0 +1,41 @@ +/* +Copyright The Helm Authors. + +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 rules // import "helm.sh/helm/v3/pkg/lint/rules" + +import "testing" + +func TestValidateNoDeprecations(t *testing.T) { + deprecated := &K8sYamlStruct{ + APIVersion: "extensions/v1beta1", + Kind: "Deployment", + } + err := validateNoDeprecations(deprecated) + if err == nil { + t.Fatal("Expected deprecated extension to be flagged") + } + depErr := err.(deprecatedAPIError) + if depErr.Message == "" { + t.Fatalf("Expected error message to be non-blank: %v", err) + } + + if err := validateNoDeprecations(&K8sYamlStruct{ + APIVersion: "v1", + Kind: "Pod", + }); err != nil { + t.Errorf("Expected a v1 Pod to not be deprecated") + } +} diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index a8b6a675708..f70c997cf4e 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,71 +17,73 @@ limitations under the License. package rules import ( - "errors" + "bufio" + "bytes" "fmt" + "io" "os" + "path" "path/filepath" + "regexp" + "strings" - "github.com/ghodss/yaml" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/engine" - "k8s.io/helm/pkg/lint/support" - cpb "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/timeconv" - tversion "k8s.io/helm/pkg/version" + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/api/validation" + apipath "k8s.io/apimachinery/pkg/api/validation/path" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/yaml" + + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/engine" + "helm.sh/helm/v3/pkg/lint/support" +) + +var ( + crdHookSearch = regexp.MustCompile(`"?helm\.sh/hook"?:\s+crd-install`) + releaseTimeSearch = regexp.MustCompile(`\.Release\.Time`) ) // Templates lints the templates in the Linter. -func Templates(linter *support.Linter, values []byte, namespace string, strict bool) { - path := "templates/" - templatesPath := filepath.Join(linter.ChartDir, path) +func Templates(linter *support.Linter, values map[string]interface{}, namespace string, strict bool) { + fpath := "templates/" + templatesPath := filepath.Join(linter.ChartDir, fpath) - templatesDirExist := linter.RunLinterRule(support.WarningSev, path, validateTemplatesDir(templatesPath)) + templatesDirExist := linter.RunLinterRule(support.WarningSev, fpath, validateTemplatesDir(templatesPath)) // Templates directory is optional for now if !templatesDirExist { return } - // Load chart and parse templates, based on tiller/release_server - chart, err := chartutil.Load(linter.ChartDir) + // Load chart and parse templates + chart, err := loader.Load(linter.ChartDir) - chartLoaded := linter.RunLinterRule(support.ErrorSev, path, err) + chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) if !chartLoaded { return } - options := chartutil.ReleaseOptions{Name: "testRelease", Time: timeconv.Now(), Namespace: namespace} - caps := &chartutil.Capabilities{ - APIVersions: chartutil.DefaultVersionSet, - KubeVersion: chartutil.DefaultKubeVersion, - TillerVersion: tversion.GetVersionProto(), - } - cvals, err := chartutil.CoalesceValues(chart, &cpb.Config{Raw: string(values)}) - if err != nil { - return + options := chartutil.ReleaseOptions{ + Name: "test-release", + Namespace: namespace, } - // convert our values back into config - yvals, err := cvals.YAML() + + cvals, err := chartutil.CoalesceValues(chart, values) if err != nil { return } - cc := &cpb.Config{Raw: yvals} - valuesToRender, err := chartutil.ToRenderValuesCaps(chart, cc, options, caps) + valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, nil) 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) + linter.RunLinterRule(support.ErrorSev, fpath, err) return } - e := engine.New() - if strict { - e.Strict = true - } + var e engine.Engine + e.LintMode = true renderedContentMap, err := e.Render(chart, valuesToRender) - renderOk := linter.RunLinterRule(support.ErrorSev, path, err) + renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err) if !renderOk { return @@ -95,42 +97,92 @@ func Templates(linter *support.Linter, values []byte, namespace string, strict b - Metadata.Namespace is not set */ for _, template := range chart.Templates { - fileName, _ := template.Name, template.Data - path = fileName + fileName, data := template.Name, template.Data + fpath = fileName - linter.RunLinterRule(support.ErrorSev, path, validateAllowedExtension(fileName)) + linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName)) + // These are v3 specific checks to make sure and warn people if their + // chart is not compatible with v3 + linter.RunLinterRule(support.WarningSev, fpath, validateNoCRDHooks(data)) + linter.RunLinterRule(support.ErrorSev, fpath, validateNoReleaseTime(data)) // We only apply the following lint rules to yaml files if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" { continue } - // NOTE: disabled for now, Refs https://github.com/kubernetes/helm/issues/1463 + // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1463 // Check that all the templates have a matching value - //linter.RunLinterRule(support.WarningSev, path, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) + //linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) + + // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1037 + // linter.RunLinterRule(support.WarningSev, fpath, validateQuotes(string(preExecutedTemplate))) + + renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)] + if strings.TrimSpace(renderedContent) != "" { + linter.RunLinterRule(support.WarningSev, fpath, validateTopIndentLevel(renderedContent)) + + decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096) - // NOTE: disabled for now, Refs https://github.com/kubernetes/helm/issues/1037 - // linter.RunLinterRule(support.WarningSev, path, validateQuotes(string(preExecutedTemplate))) + // Lint all resources if the file contains multiple documents separated by --- + for { + // Even though K8sYamlStruct only defines a few fields, an error in any other + // key will be raised as well + var yamlStruct *K8sYamlStruct - renderedContent := renderedContentMap[filepath.Join(chart.GetMetadata().Name, fileName)] - var yamlStruct K8sYamlStruct - // Even though K8sYamlStruct only defines Metadata namespace, an error in any other - // key will be raised as well - err := yaml.Unmarshal([]byte(renderedContent), &yamlStruct) + err := decoder.Decode(&yamlStruct) + if err == io.EOF { + break + } - validYaml := linter.RunLinterRule(support.ErrorSev, path, validateYamlContent(err)) + // If YAML linting fails, we sill progress. So we don't capture the returned state + // on this linter run. + linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) - if !validYaml { + if yamlStruct != nil { + // NOTE: set to warnings to allow users to support out-of-date kubernetes + // Refs https://github.com/helm/helm/issues/8596 + linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct)) + linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct)) + + linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) + } + } + } + } +} + +// validateTopIndentLevel checks that the content does not start with an indent level > 0. +// +// This error can occur when a template accidentally inserts space. It can cause +// unpredictable errors depending on whether the text is normalized before being passed +// into the YAML parser. So we trap it here. +// +// See https://github.com/helm/helm/issues/8467 +func validateTopIndentLevel(content string) error { + // Read lines until we get to a non-empty one + scanner := bufio.NewScanner(bytes.NewBufferString(content)) + for scanner.Scan() { + line := scanner.Text() + // If line is empty, skip + if strings.TrimSpace(line) == "" { continue } + // If it starts with one or more spaces, this is an error + if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { + return fmt.Errorf("document starts with an illegal indent: %q, which may cause parsing problems", line) + } + // Any other condition passes. + return nil } + return scanner.Err() } // Validation functions func validateTemplatesDir(templatesPath string) error { if fi, err := os.Stat(templatesPath); err != nil { return errors.New("directory not found") - } else if err == nil && !fi.IsDir() { + } else if !fi.IsDir() { return errors.New("not a directory") } return nil @@ -146,20 +198,113 @@ func validateAllowedExtension(fileName string) error { } } - return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .yml, .tpl, or .txt", ext) + return errors.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .yml, .tpl, or .txt", ext) } func validateYamlContent(err error) error { - if err != nil { - return fmt.Errorf("unable to parse YAML\n\t%s", err) + return errors.Wrap(err, "unable to parse YAML") +} + +// validateMetadataName uses the correct validation function for the object +// Kind, or if not set, defaults to the standard definition of a subdomain in +// DNS (RFC 1123), used by most resources. +func validateMetadataName(obj *K8sYamlStruct) error { + fn := validateMetadataNameFunc(obj) + allErrs := field.ErrorList{} + for _, msg := range fn(obj.Metadata.Name, false) { + allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), obj.Metadata.Name, msg)) + } + if len(allErrs) > 0 { + return errors.Wrapf(allErrs.ToAggregate(), "object name does not conform to Kubernetes naming requirements: %q", obj.Metadata.Name) + } + return nil +} + +// validateMetadataNameFunc will return a name validation function for the +// object kind, if defined below. +// +// Rules should match those set in the various api validations: +// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/core/validation/validation.go#L205-L274 +// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/apps/validation/validation.go#L39 +// ... +// +// Implementing here to avoid importing k/k. +// +// If no mapping is defined, returns NameIsDNSSubdomain. This is used by object +// kinds that don't have special requirements, so is the most likely to work if +// new kinds are added. +func validateMetadataNameFunc(obj *K8sYamlStruct) validation.ValidateNameFunc { + switch strings.ToLower(obj.Kind) { + case "pod", "node", "secret", "endpoints", "resourcequota", // core + "controllerrevision", "daemonset", "deployment", "replicaset", "statefulset", // apps + "autoscaler", // autoscaler + "cronjob", "job", // batch + "lease", // coordination + "endpointslice", // discovery + "networkpolicy", "ingress", // networking + "podsecuritypolicy", // policy + "priorityclass", // scheduling + "podpreset", // settings + "storageclass", "volumeattachment", "csinode": // storage + return validation.NameIsDNSSubdomain + case "service": + return validation.NameIsDNS1035Label + case "namespace": + return validation.ValidateNamespaceName + case "serviceaccount": + return validation.ValidateServiceAccountName + case "certificatesigningrequest": + // No validation. + // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/certificates/validation/validation.go#L137-L140 + return func(name string, prefix bool) []string { return nil } + case "role", "clusterrole", "rolebinding", "clusterrolebinding": + // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 + return func(name string, prefix bool) []string { + return apipath.IsValidPathSegmentName(name) + } + default: + return validation.NameIsDNSSubdomain + } +} + +func validateNoCRDHooks(manifest []byte) error { + if crdHookSearch.Match(manifest) { + return errors.New("manifest is a crd-install hook. This hook is no longer supported in v3 and all CRDs should also exist the crds/ directory at the top level of the chart") + } + return nil +} + +func validateNoReleaseTime(manifest []byte) error { + if releaseTimeSearch.Match(manifest) { + return errors.New(".Release.Time has been removed in v3, please replace with the `now` function in your templates") + } + return nil +} + +// validateMatchSelector ensures that template specs have a selector declared. +// See https://github.com/helm/helm/issues/1990 +func validateMatchSelector(yamlStruct *K8sYamlStruct, manifest string) error { + switch yamlStruct.Kind { + case "Deployment", "ReplicaSet", "DaemonSet", "StatefulSet": + // verify that matchLabels or matchExpressions is present + if !(strings.Contains(manifest, "matchLabels") || strings.Contains(manifest, "matchExpressions")) { + return fmt.Errorf("a %s must contain matchLabels or matchExpressions, and %q does not", yamlStruct.Kind, yamlStruct.Metadata.Name) + } } return nil } // K8sYamlStruct stubs a Kubernetes YAML file. -// Need to access for now to Namespace only +// +// DEPRECATED: In Helm 4, this will be made a private type, as it is for use only within +// the rules package. type K8sYamlStruct struct { - Metadata struct { - Namespace string - } + APIVersion string `json:"apiVersion"` + Kind string + Metadata k8sYamlMetadata +} + +type k8sYamlMetadata struct { + Namespace string + Name string } diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index cb1be94a2d0..bc38445f829 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,12 +17,16 @@ limitations under the License. package rules import ( + "fmt" "os" "path/filepath" "strings" "testing" - "k8s.io/helm/pkg/lint/support" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/lint/support" ) const templateTestBasedir = "./testdata/albatross" @@ -44,7 +48,7 @@ func TestValidateAllowedExtension(t *testing.T) { } } -var values = []byte("nameOverride: ''\nhttpPort: 80") +var values = map[string]interface{}{"nameOverride": "", "httpPort": 80} const namespace = "testNamespace" const strict = false @@ -81,3 +85,342 @@ func TestTemplateIntegrationHappyPath(t *testing.T) { t.Fatalf("Expected no error, got %d, %v", len(res), res) } } + +func TestV3Fail(t *testing.T) { + linter := support.Linter{ChartDir: "./testdata/v3-fail"} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 3 { + t.Fatalf("Expected 3 errors, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), ".Release.Time has been removed in v3") { + t.Errorf("Unexpected error: %s", res[0].Err) + } + if !strings.Contains(res[1].Err.Error(), "manifest is a crd-install hook") { + t.Errorf("Unexpected error: %s", res[1].Err) + } + if !strings.Contains(res[2].Err.Error(), "manifest is a crd-install hook") { + t.Errorf("Unexpected error: %s", res[2].Err) + } +} + +func TestMultiTemplateFail(t *testing.T) { + linter := support.Linter{ChartDir: "./testdata/multi-template-fail"} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 1 { + t.Fatalf("Expected 1 error, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), "object name does not conform to Kubernetes naming requirements") { + t.Errorf("Unexpected error: %s", res[0].Err) + } +} + +func TestValidateMetadataName(t *testing.T) { + tests := []struct { + obj *K8sYamlStruct + wantErr bool + }{ + // Most kinds use IsDNS1123Subdomain. + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: ""}}, true}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, + {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + {&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "operator:sa"}}, true}, + + // Service uses IsDNS1035Label. + {&K8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&K8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "123baz"}}, true}, + {&K8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, + + // Namespace uses IsDNS1123Label. + {&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, + {&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo-bar"}}, false}, + + // CertificateSigningRequest has no validation. + {&K8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: ""}}, false}, + {&K8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&K8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, false}, + + // RBAC uses path validation. + {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, + {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, + {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, + {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, + {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, + {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, + {&K8sYamlStruct{Kind: "RoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&K8sYamlStruct{Kind: "ClusterRoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + + // Unknown Kind + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: ""}}, true}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, + {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + + // No kind + {&K8sYamlStruct{Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&K8sYamlStruct{Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("%s/%s", tt.obj.Kind, tt.obj.Metadata.Name), func(t *testing.T) { + if err := validateMetadataName(tt.obj); (err != nil) != tt.wantErr { + t.Errorf("validateMetadataName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestDeprecatedAPIFails(t *testing.T) { + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "failapi", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*chart.File{ + { + Name: "templates/baddeployment.yaml", + Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), + }, + { + Name: "templates/goodsecret.yaml", + Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), + }, + }, + } + tmpdir := ensure.TempDir(t) + defer os.RemoveAll(tmpdir) + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates(&linter, values, namespace, strict) + if l := len(linter.Messages); l != 1 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 1 lint error, got %d", l) + } + + err := linter.Messages[0].Err.(deprecatedAPIError) + if err.Deprecated != "apps/v1beta1 Deployment" { + t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated) + } +} + +const manifest = `apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + myval1: {{default "val" .Values.mymap.key1 }} + myval2: {{default "val" .Values.mymap.key2 }} +` + +// TestStrictTemplateParsingMapError is a regression test. +// +// The template engine should not produce an error when a map in values.yaml does +// not contain all possible keys. +// +// See https://github.com/helm/helm/issues/7483 +func TestStrictTemplateParsingMapError(t *testing.T) { + + ch := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "regression7483", + APIVersion: "v2", + Version: "0.1.0", + }, + Values: map[string]interface{}{ + "mymap": map[string]string{ + "key1": "val1", + }, + }, + Templates: []*chart.File{ + { + Name: "templates/configmap.yaml", + Data: []byte(manifest), + }, + }, + } + dir := ensure.TempDir(t) + defer os.RemoveAll(dir) + if err := chartutil.SaveDir(&ch, dir); err != nil { + t.Fatal(err) + } + linter := &support.Linter{ + ChartDir: filepath.Join(dir, ch.Metadata.Name), + } + Templates(linter, ch.Values, namespace, strict) + if len(linter.Messages) != 0 { + t.Errorf("expected zero messages, got %d", len(linter.Messages)) + for i, msg := range linter.Messages { + t.Logf("Message %d: %q", i, msg) + } + } +} + +func TestValidateMatchSelector(t *testing.T) { + md := &K8sYamlStruct{ + APIVersion: "apps/v1", + Kind: "Deployment", + Metadata: k8sYamlMetadata{ + Name: "mydeployment", + }, + } + manifest := ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err != nil { + t.Error(err) + } + manifest = ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchExpressions: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err != nil { + t.Error(err) + } + manifest = ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err == nil { + t.Error("expected Deployment with no selector to fail") + } +} + +func TestValidateTopIndentLevel(t *testing.T) { + for doc, shouldFail := range map[string]bool{ + // Should not fail + "\n\n\n\t\n \t\n": false, + "apiVersion:foo\n bar:baz": false, + "\n\n\napiVersion:foo\n\n\n": false, + // Should fail + " apiVersion:foo": true, + "\n\n apiVersion:foo\n\n": true, + } { + if err := validateTopIndentLevel(doc); (err == nil) == shouldFail { + t.Errorf("Expected %t for %q", shouldFail, doc) + } + } + +} + +// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments +// See https://github.com/helm/helm/issues/8621 +func TestEmptyWithCommentsManifests(t *testing.T) { + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "emptymanifests", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*chart.File{ + { + Name: "templates/empty-with-comments.yaml", + Data: []byte("#@formatter:off\n"), + }, + }, + } + tmpdir := ensure.TempDir(t) + defer os.RemoveAll(tmpdir) + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates(&linter, values, namespace, strict) + if l := len(linter.Messages); l > 0 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 0 lint errors, got %d", l) + } +} diff --git a/pkg/lint/rules/testdata/albatross/Chart.yaml b/pkg/lint/rules/testdata/albatross/Chart.yaml index c108fa5e5a0..21124acfc66 100644 --- a/pkg/lint/rules/testdata/albatross/Chart.yaml +++ b/pkg/lint/rules/testdata/albatross/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: albatross description: testing chart version: 199.44.12345-Alpha.1+cafe009 diff --git a/pkg/lint/rules/testdata/albatross/templates/svc.yaml b/pkg/lint/rules/testdata/albatross/templates/svc.yaml index 1671481125c..16bb27d55f9 100644 --- a/pkg/lint/rules/testdata/albatross/templates/svc.yaml +++ b/pkg/lint/rules/testdata/albatross/templates/svc.yaml @@ -5,11 +5,10 @@ kind: Service metadata: name: "{{ .Values.name }}" labels: - heritage: {{ .Release.Service | quote }} - release: {{ .Release.Name | quote }} - chart: "{{.Chart.Name}}-{{.Chart.Version}}" + app.kubernetes.io/managed-by: {{ .Release.Service | quote }} + app.kubernetes.io/instance: {{ .Release.Name | quote }} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" kubeVersion: {{ .Capabilities.KubeVersion.Major }} - tillerVersion: {{ .Capabilities.TillerVersion }} spec: ports: - port: {{default 80 .Values.httpPort | quote}} @@ -17,4 +16,4 @@ spec: protocol: TCP name: http selector: - app: {{template "fullname" .}} + app.kubernetes.io/name: {{template "fullname" .}} diff --git a/pkg/lint/rules/testdata/anotherbadchartfile/Chart.yaml b/pkg/lint/rules/testdata/anotherbadchartfile/Chart.yaml new file mode 100644 index 00000000000..e6bac76939f --- /dev/null +++ b/pkg/lint/rules/testdata/anotherbadchartfile/Chart.yaml @@ -0,0 +1,15 @@ +name: "some-chart" +apiVersion: v2 +description: A Helm chart for Kubernetes +version: 72445e2 +home: "" +type: application +appVersion: 72225e2 +icon: "https://some-url.com/icon.jpeg" +dependencies: + - name: mariadb + version: 5.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - database diff --git a/pkg/lint/rules/testdata/badchartfile/Chart.yaml b/pkg/lint/rules/testdata/badchartfile/Chart.yaml index dbb4a1501b6..3564ede3ee7 100644 --- a/pkg/lint/rules/testdata/badchartfile/Chart.yaml +++ b/pkg/lint/rules/testdata/badchartfile/Chart.yaml @@ -1,3 +1,11 @@ description: A Helm chart for Kubernetes -version: 0.0.0 +version: 0.0.0.0 home: "" +type: application +dependencies: +- name: mariadb + version: 5.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - database diff --git a/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml b/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml index bed845249cb..632919d0333 100644 --- a/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml +++ b/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: badvaluesfile description: A Helm chart for Kubernetes version: 0.0.1 diff --git a/pkg/lint/rules/testdata/goodone/Chart.yaml b/pkg/lint/rules/testdata/goodone/Chart.yaml index de05463ca51..cb7a4bf20c0 100644 --- a/pkg/lint/rules/testdata/goodone/Chart.yaml +++ b/pkg/lint/rules/testdata/goodone/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: goodone description: good testing chart version: 199.44.12345-Alpha.1+cafe009 diff --git a/pkg/lint/rules/testdata/goodone/templates/goodone.yaml b/pkg/lint/rules/testdata/goodone/templates/goodone.yaml index 0e77f46f2f1..cd46f62c705 100644 --- a/pkg/lint/rules/testdata/goodone/templates/goodone.yaml +++ b/pkg/lint/rules/testdata/goodone/templates/goodone.yaml @@ -1,2 +1,2 @@ metadata: - name: {{.Values.name | default "foo" | title}} + name: {{ .Values.name | default "foo" | lower }} diff --git a/pkg/lint/rules/testdata/goodone/values.yaml b/pkg/lint/rules/testdata/goodone/values.yaml index fe9abd983be..92c3d9bb9f8 100644 --- a/pkg/lint/rules/testdata/goodone/values.yaml +++ b/pkg/lint/rules/testdata/goodone/values.yaml @@ -1 +1 @@ -name: "goodone here" +name: "goodone-here" diff --git a/pkg/lint/rules/testdata/multi-template-fail/Chart.yaml b/pkg/lint/rules/testdata/multi-template-fail/Chart.yaml new file mode 100644 index 00000000000..b57427de906 --- /dev/null +++ b/pkg/lint/rules/testdata/multi-template-fail/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: multi-template-fail +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application and it is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml b/pkg/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml new file mode 100644 index 00000000000..835be07bee4 --- /dev/null +++ b/pkg/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: game-config +data: + game.properties: cheat +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: -this:name-is-not_valid$ +data: + game.properties: empty diff --git a/pkg/lint/rules/testdata/v3-fail/Chart.yaml b/pkg/lint/rules/testdata/v3-fail/Chart.yaml new file mode 100644 index 00000000000..7097e17d866 --- /dev/null +++ b/pkg/lint/rules/testdata/v3-fail/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v2 +name: v3-fail +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application and it is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/lint/rules/testdata/v3-fail/templates/_helpers.tpl b/pkg/lint/rules/testdata/v3-fail/templates/_helpers.tpl new file mode 100644 index 00000000000..0b89e723b46 --- /dev/null +++ b/pkg/lint/rules/testdata/v3-fail/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "v3-fail.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "v3-fail.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "v3-fail.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "v3-fail.labels" -}} +helm.sh/chart: {{ include "v3-fail.chart" . }} +{{ include "v3-fail.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels +*/}} +{{- define "v3-fail.selectorLabels" -}} +app.kubernetes.io/name: {{ include "v3-fail.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "v3-fail.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "v3-fail.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} diff --git a/pkg/lint/rules/testdata/v3-fail/templates/deployment.yaml b/pkg/lint/rules/testdata/v3-fail/templates/deployment.yaml new file mode 100644 index 00000000000..6d651ab8e8f --- /dev/null +++ b/pkg/lint/rules/testdata/v3-fail/templates/deployment.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "v3-fail.fullname" . }} + labels: + nope: {{ .Release.Time }} + {{- include "v3-fail.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "v3-fail.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "v3-fail.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "v3-fail.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/pkg/lint/rules/testdata/v3-fail/templates/ingress.yaml b/pkg/lint/rules/testdata/v3-fail/templates/ingress.yaml new file mode 100644 index 00000000000..b2e78d99a76 --- /dev/null +++ b/pkg/lint/rules/testdata/v3-fail/templates/ingress.yaml @@ -0,0 +1,42 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "v3-fail.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "v3-fail.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + "helm.sh/hook": crd-install + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/pkg/lint/rules/testdata/v3-fail/templates/service.yaml b/pkg/lint/rules/testdata/v3-fail/templates/service.yaml new file mode 100644 index 00000000000..79a0f40b0e9 --- /dev/null +++ b/pkg/lint/rules/testdata/v3-fail/templates/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "v3-fail.fullname" . }} + annotations: + helm.sh/hook: crd-install + labels: + {{- include "v3-fail.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "v3-fail.selectorLabels" . | nindent 4 }} diff --git a/pkg/lint/rules/testdata/v3-fail/values.yaml b/pkg/lint/rules/testdata/v3-fail/values.yaml new file mode 100644 index 00000000000..01d99b4e612 --- /dev/null +++ b/pkg/lint/rules/testdata/v3-fail/values.yaml @@ -0,0 +1,66 @@ +# Default values for v3-fail. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pkg/lint/rules/values.go b/pkg/lint/rules/values.go index 9b97598f0f8..79a294326bb 100644 --- a/pkg/lint/rules/values.go +++ b/pkg/lint/rules/values.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,39 +17,71 @@ limitations under the License. package rules import ( - "fmt" + "io/ioutil" "os" "path/filepath" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/lint/support" + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/lint/support" ) // Values lints a chart's values.yaml file. +// +// This function is deprecated and will be removed in Helm 4. func Values(linter *support.Linter) { + ValuesWithOverrides(linter, map[string]interface{}{}) +} + +// ValuesWithOverrides tests the values.yaml file. +// +// If a schema is present in the chart, values are tested against that. Otherwise, +// they are only tested for well-formedness. +// +// If additional values are supplied, they are coalesced into the values in values.yaml. +func ValuesWithOverrides(linter *support.Linter, values map[string]interface{}) { file := "values.yaml" vf := filepath.Join(linter.ChartDir, file) - fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(linter, vf)) + fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf)) if !fileExists { return } - linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(linter, vf)) + linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, values)) } -func validateValuesFileExistence(linter *support.Linter, valuesPath string) error { +func validateValuesFileExistence(valuesPath string) error { _, err := os.Stat(valuesPath) if err != nil { - return fmt.Errorf("file does not exist") + return errors.Errorf("file does not exist") } return nil } -func validateValuesFile(linter *support.Linter, valuesPath string) error { - _, err := chartutil.ReadValuesFile(valuesPath) +func validateValuesFile(valuesPath string, overrides map[string]interface{}) error { + values, err := chartutil.ReadValuesFile(valuesPath) if err != nil { - return fmt.Errorf("unable to parse YAML\n\t%s", err) + return errors.Wrap(err, "unable to parse YAML") } - return nil + + // Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top + // level values against the top-level expectations. Subchart values are not linted. + // We could change that. For now, though, we retain that strategy, and thus can + // coalesce tables (like reuse-values does) instead of doing the full chart + // CoalesceValues + coalescedValues := chartutil.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) + coalescedValues = chartutil.CoalesceTables(coalescedValues, values) + + ext := filepath.Ext(valuesPath) + schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" + schema, err := ioutil.ReadFile(schemaPath) + if len(schema) == 0 { + return nil + } + if err != nil { + return err + } + return chartutil.ValidateAgainstSingleSchema(coalescedValues, schema) } diff --git a/pkg/lint/rules/values_test.go b/pkg/lint/rules/values_test.go new file mode 100644 index 00000000000..23335cc01b8 --- /dev/null +++ b/pkg/lint/rules/values_test.go @@ -0,0 +1,175 @@ +/* +Copyright The Helm Authors. + +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 rules + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v3/internal/test/ensure" +) + +var nonExistingValuesFilePath = filepath.Join("/fake/dir", "values.yaml") + +const testSchema = ` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "helm values test schema", + "type": "object", + "additionalProperties": false, + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "description": "Your username", + "type": "string" + }, + "password": { + "description": "Your password", + "type": "string" + } + } +} +` + +func TestValidateValuesYamlNotDirectory(t *testing.T) { + _ = os.Mkdir(nonExistingValuesFilePath, os.ModePerm) + defer os.Remove(nonExistingValuesFilePath) + + err := validateValuesFileExistence(nonExistingValuesFilePath) + if err == nil { + t.Errorf("validateValuesFileExistence to return a linter error, got no error") + } +} + +func TestValidateValuesFileWellFormed(t *testing.T) { + badYaml := ` + not:well[]{}formed + ` + tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) + defer os.RemoveAll(tmpdir) + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, map[string]interface{}{}); err == nil { + t.Fatal("expected values file to fail parsing") + } +} + +func TestValidateValuesFileSchema(t *testing.T) { + yaml := "username: admin\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + defer os.RemoveAll(tmpdir) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, map[string]interface{}{}); err != nil { + t.Fatalf("Failed validation with %s", err) + } +} + +func TestValidateValuesFileSchemaFailure(t *testing.T) { + // 1234 is an int, not a string. This should fail. + yaml := "username: 1234\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + defer os.RemoveAll(tmpdir) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, map[string]interface{}{}) + if err == nil { + t.Fatal("expected values file to fail parsing") + } + + assert.Contains(t, err.Error(), "Expected: string, given: integer", "integer should be caught by schema") +} + +func TestValidateValuesFileSchemaOverrides(t *testing.T) { + yaml := "username: admin" + overrides := map[string]interface{}{ + "password": "swordfish", + } + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + defer os.RemoveAll(tmpdir) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, overrides); err != nil { + t.Fatalf("Failed validation with %s", err) + } +} + +func TestValidateValuesFile(t *testing.T) { + tests := []struct { + name string + yaml string + overrides map[string]interface{} + errorMessage string + }{ + { + name: "value added", + yaml: "username: admin", + overrides: map[string]interface{}{"password": "swordfish"}, + }, + { + name: "value not overridden", + yaml: "username: admin\npassword:", + overrides: map[string]interface{}{"username": "anotherUser"}, + errorMessage: "Expected: string, given: null", + }, + { + name: "value overridden", + yaml: "username: admin\npassword:", + overrides: map[string]interface{}{"username": "anotherUser", "password": "swordfish"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpdir := ensure.TempFile(t, "values.yaml", []byte(tt.yaml)) + defer os.RemoveAll(tmpdir) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, tt.overrides) + + switch { + case err != nil && tt.errorMessage == "": + t.Errorf("Failed validation with %s", err) + case err == nil && tt.errorMessage != "": + t.Error("expected values file to fail parsing") + case err != nil && tt.errorMessage != "": + assert.Contains(t, err.Error(), tt.errorMessage, "Failed with unexpected error") + } + }) + } +} + +func createTestingSchema(t *testing.T, dir string) string { + t.Helper() + schemafile := filepath.Join(dir, "values.schema.json") + if err := ioutil.WriteFile(schemafile, []byte(testSchema), 0700); err != nil { + t.Fatalf("Failed to write schema to tmpdir: %s", err) + } + return schemafile +} diff --git a/pkg/lint/support/doc.go b/pkg/lint/support/doc.go index 4cf7272e4cf..b9a9d09189a 100644 --- a/pkg/lint/support/doc.go +++ b/pkg/lint/support/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -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 // import "k8s.io/helm/pkg/lint/support" +package support // import "helm.sh/helm/v3/pkg/lint/support" diff --git a/pkg/lint/support/message.go b/pkg/lint/support/message.go index 6a878031a9c..5efbc7a61ca 100644 --- a/pkg/lint/support/message.go +++ b/pkg/lint/support/message.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ package support import "fmt" -// Severity indicatest the severity of a Message. +// Severity indicates the severity of a Message. const ( // UnknownSev indicates that the severity of the error is unknown, and should not stop processing. UnknownSev = iota diff --git a/pkg/lint/support/message_test.go b/pkg/lint/support/message_test.go index 4a9c33c34e4..9e12a638b67 100644 --- a/pkg/lint/support/message_test.go +++ b/pkg/lint/support/message_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,8 +17,9 @@ limitations under the License. package support import ( - "errors" "testing" + + "github.com/pkg/errors" ) var linter = Linter{} diff --git a/pkg/plugin/cache/cache.go b/pkg/plugin/cache/cache.go index a1d3224c8b4..5f3345b6309 100644 --- a/pkg/plugin/cache/cache.go +++ b/pkg/plugin/cache/cache.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -14,7 +14,7 @@ limitations under the License. */ // Package cache provides a key generator for vcs urls. -package cache // import "k8s.io/helm/pkg/plugin/cache" +package cache // import "helm.sh/helm/v3/pkg/plugin/cache" import ( "net/url" @@ -30,21 +30,19 @@ var scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`) // Key generates a cache key based on a url or scp string. The key is file // system safe. func Key(repo string) (string, error) { - - var u *url.URL - var err error - var strip bool + var ( + u *url.URL + err error + ) if m := scpSyntaxRe.FindStringSubmatch(repo); m != nil { // Match SCP-like syntax and convert it to a URL. // Eg, "git@github.com:user/repo" becomes // "ssh://git@github.com/user/repo". u = &url.URL{ - Scheme: "ssh", - User: url.User(m[1]), - Host: m[2], - Path: "/" + m[3], + User: url.User(m[1]), + Host: m[2], + Path: "/" + m[3], } - strip = true } else { u, err = url.Parse(repo) if err != nil { @@ -52,23 +50,18 @@ func Key(repo string) (string, error) { } } - if strip { - u.Scheme = "" - } - - var key string + var key strings.Builder if u.Scheme != "" { - key = u.Scheme + "-" + key.WriteString(u.Scheme) + key.WriteString("-") } if u.User != nil && u.User.Username() != "" { - key = key + u.User.Username() + "-" + key.WriteString(u.User.Username()) + key.WriteString("-") } - key = key + u.Host + key.WriteString(u.Host) if u.Path != "" { - key = key + strings.Replace(u.Path, "/", "-", -1) + key.WriteString(strings.ReplaceAll(u.Path, "/", "-")) } - - key = strings.Replace(key, ":", "-", -1) - - return key, nil + return strings.ReplaceAll(key.String(), ":", "-"), nil } diff --git a/pkg/plugin/hooks.go b/pkg/plugin/hooks.go index b5ca032ac3d..e3481515f06 100644 --- a/pkg/plugin/hooks.go +++ b/pkg/plugin/hooks.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package plugin // import "k8s.io/helm/pkg/plugin" +package plugin // import "helm.sh/helm/v3/pkg/plugin" // Types of hooks const ( @@ -27,9 +27,3 @@ const ( // Hooks is a map of events to commands. type Hooks map[string]string - -// Get returns a hook for an event. -func (hooks Hooks) Get(event string) string { - h, _ := hooks[event] - return h -} diff --git a/pkg/plugin/installer/base.go b/pkg/plugin/installer/base.go index 0664dae763e..dcc3ad644ce 100644 --- a/pkg/plugin/installer/base.go +++ b/pkg/plugin/installer/base.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -13,36 +13,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -package installer // import "k8s.io/helm/pkg/plugin/installer" +package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( - "os" "path/filepath" - "k8s.io/helm/pkg/helm/helmpath" + "helm.sh/helm/v3/pkg/helmpath" ) type base struct { // Source is the reference to a plugin Source string - // HelmHome is the $HELM_HOME directory - HelmHome helmpath.Home } -func newBase(source string, home helmpath.Home) base { - return base{source, home} +func newBase(source string) base { + return base{source} } -// link creates a symlink from the plugin source to $HELM_HOME. -func (b *base) link(from string) error { - debug("symlinking %s to %s", from, b.Path()) - return os.Symlink(from, b.Path()) -} - -// Path is where the plugin will be symlinked to. +// Path is where the plugin will be installed. func (b *base) Path() string { if b.Source == "" { return "" } - return filepath.Join(b.HelmHome.Plugins(), filepath.Base(b.Source)) + return helmpath.DataPath("plugins", filepath.Base(b.Source)) } diff --git a/pkg/plugin/installer/doc.go b/pkg/plugin/installer/doc.go index a2a66f3e1dc..3e3b2ebeb85 100644 --- a/pkg/plugin/installer/doc.go +++ b/pkg/plugin/installer/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -14,4 +14,4 @@ limitations under the License. */ // Package installer provides an interface for installing Helm plugins. -package installer // import "k8s.io/helm/pkg/plugin/installer" +package installer // import "helm.sh/helm/v3/pkg/plugin/installer" diff --git a/pkg/plugin/installer/http_installer.go b/pkg/plugin/installer/http_installer.go index 91d49765100..bcbcbde932e 100644 --- a/pkg/plugin/installer/http_installer.go +++ b/pkg/plugin/installer/http_installer.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -13,22 +13,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -package installer // import "k8s.io/helm/pkg/plugin/installer" +package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( "archive/tar" "bytes" "compress/gzip" - "fmt" "io" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/plugin/cache" "os" + "path" "path/filepath" "regexp" "strings" + + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/pkg/errors" + + "helm.sh/helm/v3/internal/third_party/dep/fs" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/plugin/cache" ) // HTTPInstaller installs plugins from an archive served by a web server. @@ -54,6 +59,18 @@ var Extractors = map[string]Extractor{ ".tgz": &TarGzExtractor{}, } +// Convert a media type to an extractor extension. +// +// This should be refactored in Helm 4, combined with the extension-based mechanism. +func mediaTypeToExtension(mt string) (string, bool) { + switch strings.ToLower(mt) { + case "application/gzip", "application/x-gzip", "application/x-tgz", "application/x-gtar": + return ".tgz", true + default: + return "", false + } +} + // NewExtractor creates a new extractor matching the source file name func NewExtractor(source string) (Extractor, error) { for suffix, extractor := range Extractors { @@ -61,12 +78,11 @@ func NewExtractor(source string) (Extractor, error) { return extractor, nil } } - return nil, fmt.Errorf("no extractor implemented yet for %s", source) + return nil, errors.Errorf("no extractor implemented yet for %s", source) } // NewHTTPInstaller creates a new HttpInstaller. -func NewHTTPInstaller(source string, home helmpath.Home) (*HTTPInstaller, error) { - +func NewHTTPInstaller(source string) (*HTTPInstaller, error) { key, err := cache.Key(source) if err != nil { return nil, err @@ -77,20 +93,15 @@ func NewHTTPInstaller(source string, home helmpath.Home) (*HTTPInstaller, error) return nil, err } - getConstructor, err := getter.ByScheme("http", environment.EnvSettings{}) - if err != nil { - return nil, err - } - - get, err := getConstructor.New(source, "", "", "") + get, err := getter.All(new(cli.EnvSettings)).ByScheme("http") if err != nil { return nil, err } i := &HTTPInstaller{ - CacheDir: home.Path("cache", "plugins", key), + CacheDir: helmpath.CachePath("plugins", key), PluginName: stripPluginName(filepath.Base(source)), - base: newBase(source, home), + base: newBase(source), extractor: extractor, getter: get, } @@ -110,19 +121,18 @@ func stripPluginName(name string) string { return re.ReplaceAllString(strippedName, `$1`) } -// Install downloads and extracts the tarball into the cache directory and creates a symlink to the plugin directory in $HELM_HOME. +// Install downloads and extracts the tarball into the cache directory +// and installs into the plugin directory. // // Implements Installer. func (i *HTTPInstaller) Install() error { - pluginData, err := i.getter.Get(i.Source) if err != nil { return err } - err = i.extractor.Extract(pluginData, i.CacheDir) - if err != nil { - return err + if err := i.extractor.Extract(pluginData, i.CacheDir); err != nil { + return errors.Wrap(err, "extracting files from archive") } if !isPlugin(i.CacheDir) { @@ -134,19 +144,14 @@ func (i *HTTPInstaller) Install() error { return err } - return i.link(src) + debug("copying %s to %s", src, i.Path()) + return fs.CopyDir(src, i.Path()) } // Update updates a local repository // Not implemented for now since tarball most likely will be packaged by version func (i *HTTPInstaller) Update() error { - return fmt.Errorf("method Update() not implemented for HttpInstaller") -} - -// Override link because we want to use HttpInstaller.Path() not base.Path() -func (i *HTTPInstaller) link(from string) error { - debug("symlinking %s to %s", from, i.Path()) - return os.Symlink(from, i.Path()) + return errors.Errorf("method Update() not implemented for HttpInstaller") } // Path is overridden because we want to join on the plugin name not the file name @@ -154,7 +159,59 @@ func (i HTTPInstaller) Path() string { if i.base.Source == "" { return "" } - return filepath.Join(i.base.HelmHome.Plugins(), i.PluginName) + return helmpath.DataPath("plugins", i.PluginName) +} + +// CleanJoin resolves dest as a subpath of root. +// +// This function runs several security checks on the path, generating an error if +// the supplied `dest` looks suspicious or would result in dubious behavior on the +// filesystem. +// +// CleanJoin assumes that any attempt by `dest` to break out of the CWD is an attempt +// to be malicious. (If you don't care about this, use the securejoin-filepath library.) +// It will emit an error if it detects paths that _look_ malicious, operating on the +// assumption that we don't actually want to do anything with files that already +// appear to be nefarious. +// +// - The character `:` is considered illegal because it is a separator on UNIX and a +// drive designator on Windows. +// - The path component `..` is considered suspicions, and therefore illegal +// - The character \ (backslash) is treated as a path separator and is converted to /. +// - Beginning a path with a path separator is illegal +// - Rudimentary symlink protects are offered by SecureJoin. +func cleanJoin(root, dest string) (string, error) { + + // On Windows, this is a drive separator. On UNIX-like, this is the path list separator. + // In neither case do we want to trust a TAR that contains these. + if strings.Contains(dest, ":") { + return "", errors.New("path contains ':', which is illegal") + } + + // The Go tar library does not convert separators for us. + // We assume here, as we do elsewhere, that `\\` means a Windows path. + dest = strings.ReplaceAll(dest, "\\", "/") + + // We want to alert the user that something bad was attempted. Cleaning it + // is not a good practice. + for _, part := range strings.Split(dest, "/") { + if part == ".." { + return "", errors.New("path contains '..', which is illegal") + } + } + + // If a path is absolute, the creator of the TAR is doing something shady. + if path.IsAbs(dest) { + return "", errors.New("path is absolute, which is illegal") + } + + // SecureJoin will do some cleaning, as well as some rudimentary checking of symlinks. + newpath, err := securejoin.SecureJoin(root, dest) + if err != nil { + return "", err + } + + return filepath.ToSlash(newpath), nil } // Extract extracts compressed archives @@ -166,22 +223,24 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error { return err } - tarReader := tar.NewReader(uncompressedStream) - - os.MkdirAll(targetDir, 0755) + if err := os.MkdirAll(targetDir, 0755); err != nil { + return err + } - for true { + tarReader := tar.NewReader(uncompressedStream) + for { header, err := tarReader.Next() - if err == io.EOF { break } - if err != nil { return err } - path := filepath.Join(targetDir, header.Name) + path, err := cleanJoin(targetDir, header.Name) + if err != nil { + return err + } switch header.Typeflag { case tar.TypeDir: @@ -189,7 +248,7 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error { return err } case tar.TypeReg: - outFile, err := os.Create(path) + outFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) if err != nil { return err } @@ -198,11 +257,12 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error { return err } outFile.Close() + // We don't want to process these extension header files. + case tar.TypeXGlobalHeader, tar.TypeXHeader: + continue default: - return fmt.Errorf("unknown type: %b in %s", header.Typeflag, header.Name) + return errors.Errorf("unknown type: %b in %s", header.Typeflag, header.Name) } } - return nil - } diff --git a/pkg/plugin/installer/http_installer_test.go b/pkg/plugin/installer/http_installer_test.go index ca1a71e3e43..e89fea29d10 100644 --- a/pkg/plugin/installer/http_installer_test.go +++ b/pkg/plugin/installer/http_installer_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -13,16 +13,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -package installer // import "k8s.io/helm/pkg/plugin/installer" +package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( + "archive/tar" "bytes" + "compress/gzip" "encoding/base64" "fmt" "io/ioutil" - "k8s.io/helm/pkg/helm/helmpath" + "net/http" + "net/http/httptest" "os" + "path/filepath" + "strings" + "syscall" "testing" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/helmpath" ) var _ Installer = new(HTTPInstaller) @@ -33,7 +45,9 @@ type TestHTTPGetter struct { MockError error } -func (t *TestHTTPGetter) Get(href string) (*bytes.Buffer, error) { return t.MockResponse, t.MockError } +func (t *TestHTTPGetter) Get(href string, _ ...getter.Option) (*bytes.Buffer, error) { + return t.MockResponse, t.MockError +} // Fake plugin tarball data var fakePluginB64 = "H4sIAKRj51kAA+3UX0vCUBgGcC9jn+Iwuk3Peza3GeyiUlJQkcogCOzgli7dJm4TvYk+a5+k479UqquUCJ/fLs549sLO2TnvWnJa9aXnjwujYdYLovxMhsPcfnHOLdNkOXthM/IVQQYjg2yyLLJ4kXGhLp5j0z3P41tZksqxmspL3B/O+j/XtZu1y8rdYzkOZRCxduKPk53ny6Wwz/GfIIf1As8lxzGJSmoHNLJZphKHG4YpTCE0wVk3DULfpSJ3DMMqkj3P5JfMYLdX1Vr9Ie/5E5cstcdC8K04iGLX5HaJuKpWL17F0TCIBi5pf/0pjtLhun5j3f9v6r7wfnI/H0eNp9d1/5P6Gez0vzo7wsoxfrAZbTny/o9k6J8z/VkO/LPlWdC1iVpbEEcq5nmeJ13LEtmbV0k2r2PrOs9PuuNglC5rL1Y5S/syXRQmutaNw1BGnnp8Wq3UG51WvX1da3bKtZtCN/R09DwAAAAAAAAAAAAAAAAAAADAb30AoMczDwAoAAA=" @@ -53,28 +67,38 @@ func TestStripName(t *testing.T) { } } +func mockArchiveServer() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasSuffix(r.URL.Path, ".tar.gz") { + w.Header().Add("Content-Type", "text/html") + fmt.Fprintln(w, "broken") + return + } + w.Header().Add("Content-Type", "application/gzip") + fmt.Fprintln(w, "test") + })) +} + func TestHTTPInstaller(t *testing.T) { - source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz" - hh, err := ioutil.TempDir("", "helm-home-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(hh) + defer ensure.HelmHome(t)() + + srv := mockArchiveServer() + defer srv.Close() + source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" - home := helmpath.Home(hh) - if err := os.MkdirAll(home.Plugins(), 0755); err != nil { - t.Fatalf("Could not create %s: %s", home.Plugins(), err) + if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { + t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) } - i, err := NewForSource(source, "0.0.1", home) + i, err := NewForSource(source, "0.0.1") if err != nil { - t.Errorf("unexpected error: %s", err) + t.Fatalf("unexpected error: %s", err) } // ensure a HTTPInstaller was returned httpInstaller, ok := i.(*HTTPInstaller) if !ok { - t.Error("expected a HTTPInstaller") + t.Fatal("expected a HTTPInstaller") } // inject fake http client responding with minimal plugin tarball @@ -89,79 +113,73 @@ func TestHTTPInstaller(t *testing.T) { // install the plugin if err := Install(i); err != nil { - t.Error(err) + t.Fatal(err) } - if i.Path() != home.Path("plugins", "fake-plugin") { - t.Errorf("expected path '$HELM_HOME/plugins/fake-plugin', got %q", i.Path()) + if i.Path() != helmpath.DataPath("plugins", "fake-plugin") { + t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path()) } // Install again to test plugin exists error if err := Install(i); err == nil { - t.Error("expected error for plugin exists, got none") + t.Fatal("expected error for plugin exists, got none") } else if err.Error() != "plugin already exists" { - t.Errorf("expected error for plugin exists, got (%v)", err) + t.Fatalf("expected error for plugin exists, got (%v)", err) } } func TestHTTPInstallerNonExistentVersion(t *testing.T) { - source := "https://repo.localdomain/plugins/fake-plugin-0.0.2.tar.gz" - hh, err := ioutil.TempDir("", "helm-home-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(hh) + defer ensure.HelmHome(t)() + srv := mockArchiveServer() + defer srv.Close() + source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" - home := helmpath.Home(hh) - if err := os.MkdirAll(home.Plugins(), 0755); err != nil { - t.Fatalf("Could not create %s: %s", home.Plugins(), err) + if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { + t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) } - i, err := NewForSource(source, "0.0.2", home) + i, err := NewForSource(source, "0.0.2") if err != nil { - t.Errorf("unexpected error: %s", err) + t.Fatalf("unexpected error: %s", err) } // ensure a HTTPInstaller was returned httpInstaller, ok := i.(*HTTPInstaller) if !ok { - t.Error("expected a HTTPInstaller") + t.Fatal("expected a HTTPInstaller") } // inject fake http client responding with error httpInstaller.getter = &TestHTTPGetter{ - MockError: fmt.Errorf("failed to download plugin for some reason"), + MockError: errors.Errorf("failed to download plugin for some reason"), } // attempt to install the plugin if err := Install(i); err == nil { - t.Error("expected error from http client") + t.Fatal("expected error from http client") } } func TestHTTPInstallerUpdate(t *testing.T) { - source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz" - hh, err := ioutil.TempDir("", "helm-home-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(hh) + srv := mockArchiveServer() + defer srv.Close() + source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" + defer ensure.HelmHome(t)() - home := helmpath.Home(hh) - if err := os.MkdirAll(home.Plugins(), 0755); err != nil { - t.Fatalf("Could not create %s: %s", home.Plugins(), err) + if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { + t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) } - i, err := NewForSource(source, "0.0.1", home) + i, err := NewForSource(source, "0.0.1") if err != nil { - t.Errorf("unexpected error: %s", err) + t.Fatalf("unexpected error: %s", err) } // ensure a HTTPInstaller was returned httpInstaller, ok := i.(*HTTPInstaller) if !ok { - t.Error("expected a HTTPInstaller") + t.Fatal("expected a HTTPInstaller") } // inject fake http client responding with minimal plugin tarball @@ -176,14 +194,162 @@ func TestHTTPInstallerUpdate(t *testing.T) { // install the plugin before updating if err := Install(i); err != nil { - t.Error(err) + t.Fatal(err) } - if i.Path() != home.Path("plugins", "fake-plugin") { - t.Errorf("expected path '$HELM_HOME/plugins/fake-plugin', got %q", i.Path()) + if i.Path() != helmpath.DataPath("plugins", "fake-plugin") { + t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path()) } // Update plugin, should fail because it is not implemented if err := Update(i); err == nil { - t.Error("update method not implemented for http installer") + t.Fatal("update method not implemented for http installer") + } +} + +func TestExtract(t *testing.T) { + source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz" + + tempDir, err := ioutil.TempDir("", "") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tempDir) + + // Set the umask to default open permissions so we can actually test + oldmask := syscall.Umask(0000) + defer func() { + syscall.Umask(oldmask) + }() + + // Write a tarball to a buffer for us to extract + var tarbuf bytes.Buffer + tw := tar.NewWriter(&tarbuf) + var files = []struct { + Name, Body string + Mode int64 + }{ + {"plugin.yaml", "plugin metadata", 0600}, + {"README.md", "some text", 0777}, + } + for _, file := range files { + hdr := &tar.Header{ + Name: file.Name, + Typeflag: tar.TypeReg, + Mode: file.Mode, + Size: int64(len(file.Body)), + } + if err := tw.WriteHeader(hdr); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte(file.Body)); err != nil { + t.Fatal(err) + } + } + + // Add pax global headers. This should be ignored. + // Note the PAX header that isn't global cannot be written using WriteHeader. + // Details are in the internal Go function for the tar packaged named + // allowedFormats. For a TypeXHeader it will return a message stating + // "cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headers" + if err := tw.WriteHeader(&tar.Header{ + Name: "pax_global_header", + Typeflag: tar.TypeXGlobalHeader, + }); err != nil { + t.Fatal(err) + } + + if err := tw.Close(); err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + gz := gzip.NewWriter(&buf) + if _, err := gz.Write(tarbuf.Bytes()); err != nil { + t.Fatal(err) + } + gz.Close() + // END tarball creation + + extractor, err := NewExtractor(source) + if err != nil { + t.Fatal(err) + } + + if err = extractor.Extract(&buf, tempDir); err != nil { + t.Fatalf("Did not expect error but got error: %v", err) + } + + pluginYAMLFullPath := filepath.Join(tempDir, "plugin.yaml") + if info, err := os.Stat(pluginYAMLFullPath); err != nil { + if os.IsNotExist(err) { + t.Fatalf("Expected %s to exist but doesn't", pluginYAMLFullPath) + } + t.Fatal(err) + } else if info.Mode().Perm() != 0600 { + t.Fatalf("Expected %s to have 0600 mode it but has %o", pluginYAMLFullPath, info.Mode().Perm()) + } + + readmeFullPath := filepath.Join(tempDir, "README.md") + if info, err := os.Stat(readmeFullPath); err != nil { + if os.IsNotExist(err) { + t.Fatalf("Expected %s to exist but doesn't", readmeFullPath) + } + t.Fatal(err) + } else if info.Mode().Perm() != 0777 { + t.Fatalf("Expected %s to have 0777 mode it but has %o", readmeFullPath, info.Mode().Perm()) + } + +} + +func TestCleanJoin(t *testing.T) { + for i, fixture := range []struct { + path string + expect string + expectError bool + }{ + {"foo/bar.txt", "/tmp/foo/bar.txt", false}, + {"/foo/bar.txt", "", true}, + {"./foo/bar.txt", "/tmp/foo/bar.txt", false}, + {"./././././foo/bar.txt", "/tmp/foo/bar.txt", false}, + {"../../../../foo/bar.txt", "", true}, + {"foo/../../../../bar.txt", "", true}, + {"c:/foo/bar.txt", "/tmp/c:/foo/bar.txt", true}, + {"foo\\bar.txt", "/tmp/foo/bar.txt", false}, + {"c:\\foo\\bar.txt", "", true}, + } { + out, err := cleanJoin("/tmp", fixture.path) + if err != nil { + if !fixture.expectError { + t.Errorf("Test %d: Path was not cleaned: %s", i, err) + } + continue + } + if fixture.expect != out { + t.Errorf("Test %d: Expected %q but got %q", i, fixture.expect, out) + } + } + +} + +func TestMediaTypeToExtension(t *testing.T) { + + for mt, shouldPass := range map[string]bool{ + "": false, + "application/gzip": true, + "application/x-gzip": true, + "application/x-tgz": true, + "application/x-gtar": true, + "application/json": false, + } { + ext, ok := mediaTypeToExtension(mt) + if ok != shouldPass { + t.Errorf("Media type %q failed test", mt) + } + if shouldPass && ext == "" { + t.Errorf("Expected an extension but got empty string") + } + if !shouldPass && len(ext) != 0 { + t.Error("Expected extension to be empty for unrecognized type") + } } } diff --git a/pkg/plugin/installer/installer.go b/pkg/plugin/installer/installer.go index 02aee9f461d..6f01494e58f 100644 --- a/pkg/plugin/installer/installer.go +++ b/pkg/plugin/installer/installer.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,14 +16,16 @@ limitations under the License. package installer import ( - "errors" "fmt" + "log" + "net/http" "os" - "path" "path/filepath" "strings" - "k8s.io/helm/pkg/helm/helmpath" + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/plugin" ) // ErrMissingMetadata indicates that plugin.yaml is missing. @@ -34,50 +36,47 @@ var Debug bool // Installer provides an interface for installing helm client plugins. type Installer interface { - // Install adds a plugin to $HELM_HOME. + // Install adds a plugin. Install() error // Path is the directory of the installed plugin. Path() string - // Update updates a plugin to $HELM_HOME. + // Update updates a plugin. Update() error } -// Install installs a plugin to $HELM_HOME. +// Install installs a plugin. func Install(i Installer) error { - if _, pathErr := os.Stat(path.Dir(i.Path())); os.IsNotExist(pathErr) { - return errors.New(`plugin home "$HELM_HOME/plugins" does not exist`) + if err := os.MkdirAll(filepath.Dir(i.Path()), 0755); err != nil { + return err } - if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) { return errors.New("plugin already exists") } - return i.Install() } -// Update updates a plugin in $HELM_HOME. +// Update updates a plugin. func Update(i Installer) error { if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) { return errors.New("plugin does not exist") } - return i.Update() } // NewForSource determines the correct Installer for the given source. -func NewForSource(source, version string, home helmpath.Home) (Installer, error) { +func NewForSource(source, version string) (Installer, error) { // Check if source is a local directory if isLocalReference(source) { - return NewLocalInstaller(source, home) + return NewLocalInstaller(source) } else if isRemoteHTTPArchive(source) { - return NewHTTPInstaller(source, home) + return NewHTTPInstaller(source) } - return NewVCSInstaller(source, version, home) + return NewVCSInstaller(source, version) } // FindSource determines the correct Installer for the given source. -func FindSource(location string, home helmpath.Home) (Installer, error) { - installer, err := existingVCSRepo(location, home) +func FindSource(location string) (Installer, error) { + installer, err := existingVCSRepo(location) if err != nil && err.Error() == "Cannot detect VCS" { return installer, errors.New("cannot get information about plugin source") } @@ -91,10 +90,29 @@ func isLocalReference(source string) bool { } // isRemoteHTTPArchive checks if the source is a http/https url and is an archive +// +// It works by checking whether the source looks like a URL and, if it does, running a +// HEAD operation to see if the remote resource is a file that we understand. func isRemoteHTTPArchive(source string) bool { if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { + res, err := http.Head(source) + if err != nil { + // If we get an error at the network layer, we can't install it. So + // we return false. + return false + } + + // Next, we look for the content type or content disposition headers to see + // if they have matching extractors. + contentType := res.Header.Get("content-type") + foundSuffix, ok := mediaTypeToExtension(contentType) + if !ok { + // Media type not recognized + return false + } + for suffix := range Extractors { - if strings.HasSuffix(source, suffix) { + if strings.HasSuffix(foundSuffix, suffix) { return true } } @@ -104,13 +122,14 @@ func isRemoteHTTPArchive(source string) bool { // isPlugin checks if the directory contains a plugin.yaml file. func isPlugin(dirname string) bool { - _, err := os.Stat(filepath.Join(dirname, "plugin.yaml")) + _, err := os.Stat(filepath.Join(dirname, plugin.PluginFileName)) return err == nil } +var logger = log.New(os.Stderr, "[debug] ", log.Lshortfile) + func debug(format string, args ...interface{}) { if Debug { - format = fmt.Sprintf("[debug] %s\n", format) - fmt.Printf(format, args...) + logger.Output(2, fmt.Sprintf(format, args...)) } } diff --git a/pkg/plugin/installer/installer_test.go b/pkg/plugin/installer/installer_test.go new file mode 100644 index 00000000000..a11464924ac --- /dev/null +++ b/pkg/plugin/installer/installer_test.go @@ -0,0 +1,40 @@ +/* +Copyright The Helm Authors. +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 installer + +import "testing" + +func TestIsRemoteHTTPArchive(t *testing.T) { + srv := mockArchiveServer() + defer srv.Close() + source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" + + if isRemoteHTTPArchive("/not/a/URL") { + t.Errorf("Expected non-URL to return false") + } + + if isRemoteHTTPArchive("https://127.0.0.1:123/fake/plugin-1.2.3.tgz") { + t.Errorf("Bad URL should not have succeeded.") + } + + if !isRemoteHTTPArchive(source) { + t.Errorf("Expected %q to be a valid archive URL", source) + } + + if isRemoteHTTPArchive(source + "-not-an-extension") { + t.Error("Expected media type match to fail") + } +} diff --git a/pkg/plugin/installer/local_installer.go b/pkg/plugin/installer/local_installer.go index 3cf6bb42296..c92bc3fb084 100644 --- a/pkg/plugin/installer/local_installer.go +++ b/pkg/plugin/installer/local_installer.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -13,13 +13,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package installer // import "k8s.io/helm/pkg/plugin/installer" +package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( - "fmt" + "os" "path/filepath" - "k8s.io/helm/pkg/helm/helmpath" + "github.com/pkg/errors" ) // LocalInstaller installs plugins from the filesystem. @@ -28,25 +28,26 @@ type LocalInstaller struct { } // NewLocalInstaller creates a new LocalInstaller. -func NewLocalInstaller(source string, home helmpath.Home) (*LocalInstaller, error) { +func NewLocalInstaller(source string) (*LocalInstaller, error) { src, err := filepath.Abs(source) if err != nil { - return nil, fmt.Errorf("unable to get absolute path to plugin: %v", err) + return nil, errors.Wrap(err, "unable to get absolute path to plugin") } i := &LocalInstaller{ - base: newBase(src, home), + base: newBase(src), } return i, nil } -// Install creates a symlink to the plugin directory in $HELM_HOME. +// Install creates a symlink to the plugin directory. // // Implements Installer. func (i *LocalInstaller) Install() error { if !isPlugin(i.Source) { return ErrMissingMetadata } - return i.link(i.Source) + debug("symlinking %s to %s", i.Source, i.Path()) + return os.Symlink(i.Source, i.Path()) } // Update updates a local repository diff --git a/pkg/plugin/installer/local_installer_test.go b/pkg/plugin/installer/local_installer_test.go index 6a7c957d687..0d3de11d1be 100644 --- a/pkg/plugin/installer/local_installer_test.go +++ b/pkg/plugin/installer/local_installer_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package installer // import "k8s.io/helm/pkg/plugin/installer" +package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( "io/ioutil" @@ -21,23 +21,12 @@ import ( "path/filepath" "testing" - "k8s.io/helm/pkg/helm/helmpath" + "helm.sh/helm/v3/pkg/helmpath" ) var _ Installer = new(LocalInstaller) func TestLocalInstaller(t *testing.T) { - hh, err := ioutil.TempDir("", "helm-home-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(hh) - - home := helmpath.Home(hh) - if err := os.MkdirAll(home.Plugins(), 0755); err != nil { - t.Fatalf("Could not create %s: %s", home.Plugins(), err) - } - // Make a temp dir tdir, err := ioutil.TempDir("", "helm-installer-") if err != nil { @@ -48,17 +37,18 @@ func TestLocalInstaller(t *testing.T) { t.Fatal(err) } - source := "../testdata/plugdir/echo" - i, err := NewForSource(source, "", home) + source := "../testdata/plugdir/good/echo" + i, err := NewForSource(source, "") if err != nil { - t.Errorf("unexpected error: %s", err) + t.Fatalf("unexpected error: %s", err) } if err := Install(i); err != nil { - t.Error(err) + t.Fatal(err) } - if i.Path() != home.Path("plugins", "echo") { - t.Errorf("expected path '$HELM_HOME/plugins/helm-env', got %q", i.Path()) + if i.Path() != helmpath.DataPath("plugins", "echo") { + t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path()) } + defer os.RemoveAll(filepath.Dir(helmpath.DataPath())) // helmpath.DataPath is like /tmp/helm013130971/helm } diff --git a/pkg/plugin/installer/vcs_installer.go b/pkg/plugin/installer/vcs_installer.go index 0a373a9718c..f7df5b32278 100644 --- a/pkg/plugin/installer/vcs_installer.go +++ b/pkg/plugin/installer/vcs_installer.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -13,19 +13,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package installer // import "k8s.io/helm/pkg/plugin/installer" +package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( - "errors" - "fmt" "os" "sort" - "github.com/Masterminds/semver" + "github.com/Masterminds/semver/v3" "github.com/Masterminds/vcs" + "github.com/pkg/errors" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/plugin/cache" + "helm.sh/helm/v3/internal/third_party/dep/fs" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/plugin/cache" ) // VCSInstaller installs plugins from remote a repository. @@ -35,25 +35,25 @@ type VCSInstaller struct { base } -func existingVCSRepo(location string, home helmpath.Home) (Installer, error) { +func existingVCSRepo(location string) (Installer, error) { repo, err := vcs.NewRepo("", location) if err != nil { return nil, err } i := &VCSInstaller{ Repo: repo, - base: newBase(repo.Remote(), home), + base: newBase(repo.Remote()), } - return i, err + return i, nil } // NewVCSInstaller creates a new VCSInstaller. -func NewVCSInstaller(source, version string, home helmpath.Home) (*VCSInstaller, error) { +func NewVCSInstaller(source, version string) (*VCSInstaller, error) { key, err := cache.Key(source) if err != nil { return nil, err } - cachedpath := home.Path("cache", "plugins", key) + cachedpath := helmpath.CachePath("plugins", key) repo, err := vcs.NewRepo(source, cachedpath) if err != nil { return nil, err @@ -61,12 +61,12 @@ func NewVCSInstaller(source, version string, home helmpath.Home) (*VCSInstaller, i := &VCSInstaller{ Repo: repo, Version: version, - base: newBase(source, home), + base: newBase(source), } return i, err } -// Install clones a remote repository and creates a symlink to the plugin directory in HELM_HOME. +// Install clones a remote repository and installs into the plugin directory. // // Implements Installer. func (i *VCSInstaller) Install() error { @@ -88,7 +88,8 @@ func (i *VCSInstaller) Install() error { return ErrMissingMetadata } - return i.link(i.Repo.LocalPath()) + debug("copying %s to %s", i.Repo.LocalPath(), i.Path()) + return fs.CopyDir(i.Repo.LocalPath(), i.Path()) } // Update updates a remote repository @@ -136,14 +137,14 @@ func (i *VCSInstaller) solveVersion(repo vcs.Repo) (string, error) { sort.Sort(sort.Reverse(semver.Collection(semvers))) for _, v := range semvers { if constraint.Check(v) { - // If the constrint passes get the original reference + // If the constraint passes get the original reference ver := v.Original() debug("setting to %s", ver) return ver, nil } } - return "", fmt.Errorf("requested version %q does not exist for plugin %q", i.Version, i.Repo.Remote()) + return "", errors.Errorf("requested version %q does not exist for plugin %q", i.Version, i.Repo.Remote()) } // setVersion attempts to checkout the version diff --git a/pkg/plugin/installer/vcs_installer_test.go b/pkg/plugin/installer/vcs_installer_test.go index 45389954361..6785264b3dc 100644 --- a/pkg/plugin/installer/vcs_installer_test.go +++ b/pkg/plugin/installer/vcs_installer_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -13,18 +13,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -package installer // import "k8s.io/helm/pkg/plugin/installer" +package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( "fmt" - "io/ioutil" "os" "path/filepath" "testing" - "k8s.io/helm/pkg/helm/helmpath" - "github.com/Masterminds/vcs" + + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/helmpath" ) var _ Installer = new(VCSInstaller) @@ -49,25 +49,20 @@ func (r *testRepo) UpdateVersion(version string) error { } func TestVCSInstaller(t *testing.T) { - hh, err := ioutil.TempDir("", "helm-home-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(hh) + defer ensure.HelmHome(t)() - home := helmpath.Home(hh) - if err := os.MkdirAll(home.Plugins(), 0755); err != nil { - t.Fatalf("Could not create %s: %s", home.Plugins(), err) + if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { + t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) } source := "https://github.com/adamreese/helm-env" - testRepoPath, _ := filepath.Abs("../testdata/plugdir/echo") + testRepoPath, _ := filepath.Abs("../testdata/plugdir/good/echo") repo := &testRepo{ local: testRepoPath, tags: []string{"0.1.0", "0.1.1"}, } - i, err := NewForSource(source, "~0.1.0", home) + i, err := NewForSource(source, "~0.1.0") if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -85,82 +80,61 @@ func TestVCSInstaller(t *testing.T) { t.Fatal(err) } if repo.current != "0.1.1" { - t.Errorf("expected version '0.1.1', got %q", repo.current) + t.Fatalf("expected version '0.1.1', got %q", repo.current) } - if i.Path() != home.Path("plugins", "helm-env") { - t.Errorf("expected path '$HELM_HOME/plugins/helm-env', got %q", i.Path()) + if i.Path() != helmpath.DataPath("plugins", "helm-env") { + t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path()) } // Install again to test plugin exists error if err := Install(i); err == nil { - t.Error("expected error for plugin exists, got none") + t.Fatalf("expected error for plugin exists, got none") } else if err.Error() != "plugin already exists" { - t.Errorf("expected error for plugin exists, got (%v)", err) + t.Fatalf("expected error for plugin exists, got (%v)", err) } - //Testing FindSource method, expect error because plugin code is not a cloned repository - if _, err := FindSource(i.Path(), home); err == nil { - t.Error("expected error for inability to find plugin source, got none") + // Testing FindSource method, expect error because plugin code is not a cloned repository + if _, err := FindSource(i.Path()); err == nil { + t.Fatalf("expected error for inability to find plugin source, got none") } else if err.Error() != "cannot get information about plugin source" { - t.Errorf("expected error for inability to find plugin source, got (%v)", err) + t.Fatalf("expected error for inability to find plugin source, got (%v)", err) } } func TestVCSInstallerNonExistentVersion(t *testing.T) { - hh, err := ioutil.TempDir("", "helm-home-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(hh) - - home := helmpath.Home(hh) - if err := os.MkdirAll(home.Plugins(), 0755); err != nil { - t.Fatalf("Could not create %s: %s", home.Plugins(), err) - } + defer ensure.HelmHome(t)() source := "https://github.com/adamreese/helm-env" version := "0.2.0" - i, err := NewForSource(source, version, home) + i, err := NewForSource(source, version) if err != nil { t.Fatalf("unexpected error: %s", err) } // ensure a VCSInstaller was returned - _, ok := i.(*VCSInstaller) - if !ok { + if _, ok := i.(*VCSInstaller); !ok { t.Fatal("expected a VCSInstaller") } if err := Install(i); err == nil { - t.Error("expected error for version does not exists, got none") + t.Fatalf("expected error for version does not exists, got none") } else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", version, source) { - t.Errorf("expected error for version does not exists, got (%v)", err) + t.Fatalf("expected error for version does not exists, got (%v)", err) } } func TestVCSInstallerUpdate(t *testing.T) { - - hh, err := ioutil.TempDir("", "helm-home-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(hh) - - home := helmpath.Home(hh) - if err := os.MkdirAll(home.Plugins(), 0755); err != nil { - t.Fatalf("Could not create %s: %s", home.Plugins(), err) - } + defer ensure.HelmHome(t)() source := "https://github.com/adamreese/helm-env" - i, err := NewForSource(source, "", home) + i, err := NewForSource(source, "") if err != nil { t.Fatalf("unexpected error: %s", err) } // ensure a VCSInstaller was returned - _, ok := i.(*VCSInstaller) - if !ok { + if _, ok := i.(*VCSInstaller); !ok { t.Fatal("expected a VCSInstaller") } @@ -176,12 +150,14 @@ func TestVCSInstallerUpdate(t *testing.T) { } // Test FindSource method for positive result - pluginInfo, err := FindSource(i.Path(), home) + pluginInfo, err := FindSource(i.Path()) if err != nil { t.Fatal(err) } - repoRemote := pluginInfo.(*VCSInstaller).Repo.Remote() + vcsInstaller := pluginInfo.(*VCSInstaller) + + repoRemote := vcsInstaller.Repo.Remote() if repoRemote != source { t.Fatalf("invalid source found, expected %q got %q", source, repoRemote) } @@ -192,12 +168,14 @@ func TestVCSInstallerUpdate(t *testing.T) { } // Test update failure - os.Remove(filepath.Join(i.Path(), "plugin.yaml")) + if err := os.Remove(filepath.Join(vcsInstaller.Repo.LocalPath(), "plugin.yaml")); err != nil { + t.Fatal(err) + } // Testing update for error - if err := Update(i); err == nil { - t.Error("expected error for plugin modified, got none") + if err := Update(vcsInstaller); err == nil { + t.Fatalf("expected error for plugin modified, got none") } else if err.Error() != "plugin repo was modified" { - t.Errorf("expected error for plugin modified, got (%v)", err) + t.Fatalf("expected error for plugin modified, got (%v)", err) } } diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index b3458c2d8c4..9ead561b2bd 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -13,20 +13,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -package plugin // import "k8s.io/helm/pkg/plugin" +package plugin // import "helm.sh/helm/v3/pkg/plugin" import ( + "fmt" "io/ioutil" "os" "path/filepath" + "regexp" + "runtime" "strings" + "unicode" - helm_env "k8s.io/helm/pkg/helm/environment" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" - "github.com/ghodss/yaml" + "helm.sh/helm/v3/pkg/cli" ) -const pluginFileName = "plugin.yaml" +const PluginFileName = "plugin.yaml" // Downloaders represents the plugins capability if it can retrieve // charts from special sources @@ -38,6 +43,13 @@ type Downloaders struct { Command string `json:"command"` } +// PlatformCommand represents a command for a particular operating system and architecture +type PlatformCommand struct { + OperatingSystem string `json:"os"` + Architecture string `json:"arch"` + Command string `json:"command"` +} + // Metadata describes a plugin. // // This is the plugin equivalent of a chart.Metadata. @@ -62,7 +74,15 @@ type Metadata struct { // // Note that command is not executed in a shell. To do so, we suggest // pointing the command to a shell script. - Command string `json:"command"` + // + // The following rules will apply to processing commands: + // - If platformCommand is present, it will be searched first + // - If both OS and Arch match the current platform, search will stop and the command will be executed + // - If OS matches and there is no more specific match, the command will be executed + // - If no OS/Arch match is found, the default command will be executed + // - If no command is present and no matches are found in platformCommand, Helm will exit with an error + PlatformCommand []PlatformCommand `json:"platformCommand"` + Command string `json:"command"` // IgnoreFlags ignores any flags passed in from Helm // @@ -71,17 +91,18 @@ type Metadata struct { // the `--debug` flag will be discarded. IgnoreFlags bool `json:"ignoreFlags"` - // UseTunnel indicates that this command needs a tunnel. - // Setting this will cause a number of side effects, such as the - // automatic setting of HELM_HOST. - UseTunnel bool `json:"useTunnel"` - // Hooks are commands that will run on events. Hooks Hooks // Downloaders field is used if the plugin supply downloader mechanism // for special protocols. Downloaders []Downloaders `json:"downloaders"` + + // UseTunnelDeprecated indicates that this command needs a tunnel. + // Setting this will cause a number of side effects, such as the + // automatic setting of HELM_HOST. + // DEPRECATED and unused, but retained for backwards compatibility with Helm 2 plugins. Remove in Helm 4 + UseTunnelDeprecated bool `json:"useTunnel,omitempty"` } // Plugin represents a plugin. @@ -92,14 +113,48 @@ type Plugin struct { Dir string } -// PrepareCommand takes a Plugin.Command and prepares it for execution. +// The following rules will apply to processing the Plugin.PlatformCommand.Command: +// - If both OS and Arch match the current platform, search will stop and the command will be prepared for execution +// - If OS matches and there is no more specific match, the command will be prepared for execution +// - If no OS/Arch match is found, return nil +func getPlatformCommand(cmds []PlatformCommand) []string { + var command []string + eq := strings.EqualFold + for _, c := range cmds { + if eq(c.OperatingSystem, runtime.GOOS) { + command = strings.Split(os.ExpandEnv(c.Command), " ") + } + if eq(c.OperatingSystem, runtime.GOOS) && eq(c.Architecture, runtime.GOARCH) { + return strings.Split(os.ExpandEnv(c.Command), " ") + } + } + return command +} + +// PrepareCommand takes a Plugin.PlatformCommand.Command, a Plugin.Command and will applying the following processing: +// - If platformCommand is present, it will be searched first +// - If both OS and Arch match the current platform, search will stop and the command will be prepared for execution +// - If OS matches and there is no more specific match, the command will be prepared for execution +// - If no OS/Arch match is found, the default command will be prepared for execution +// - If no command is present and no matches are found in platformCommand, will exit with an error // // It merges extraArgs into any arguments supplied in the plugin. It // returns the name of the command and an args array. // // The result is suitable to pass to exec.Command. -func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string) { - parts := strings.Split(os.ExpandEnv(p.Metadata.Command), " ") +func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) { + var parts []string + platCmdLen := len(p.Metadata.PlatformCommand) + if platCmdLen > 0 { + parts = getPlatformCommand(p.Metadata.PlatformCommand) + } + if platCmdLen == 0 || parts == nil { + parts = strings.Split(os.ExpandEnv(p.Metadata.Command), " ") + } + if len(parts) == 0 || parts[0] == "" { + return "", nil, fmt.Errorf("No plugin command is applicable") + } + main := parts[0] baseArgs := []string{} if len(parts) > 1 { @@ -108,21 +163,69 @@ func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string) { if !p.Metadata.IgnoreFlags { baseArgs = append(baseArgs, extraArgs...) } - return main, baseArgs + return main, baseArgs, nil +} + +// validPluginName is a regular expression that validates plugin names. +// +// Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, ​_​ and ​-. +var validPluginName = regexp.MustCompile("^[A-Za-z0-9_-]+$") + +// validatePluginData validates a plugin's YAML data. +func validatePluginData(plug *Plugin, filepath string) error { + if !validPluginName.MatchString(plug.Metadata.Name) { + return fmt.Errorf("invalid plugin name at %q", filepath) + } + plug.Metadata.Usage = sanitizeString(plug.Metadata.Usage) + + // We could also validate SemVer, executable, and other fields should we so choose. + return nil +} + +// sanitizeString normalize spaces and removes non-printable characters. +func sanitizeString(str string) string { + return strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return ' ' + } + if unicode.IsPrint(r) { + return r + } + return -1 + }, str) +} + +func detectDuplicates(plugs []*Plugin) error { + names := map[string]string{} + + for _, plug := range plugs { + if oldpath, ok := names[plug.Metadata.Name]; ok { + return fmt.Errorf( + "two plugins claim the name %q at %q and %q", + plug.Metadata.Name, + oldpath, + plug.Dir, + ) + } + names[plug.Metadata.Name] = plug.Dir + } + + return nil } // LoadDir loads a plugin from the given directory. func LoadDir(dirname string) (*Plugin, error) { - data, err := ioutil.ReadFile(filepath.Join(dirname, pluginFileName)) + pluginfile := filepath.Join(dirname, PluginFileName) + data, err := ioutil.ReadFile(pluginfile) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "failed to read plugin at %q", pluginfile) } plug := &Plugin{Dir: dirname} - if err := yaml.Unmarshal(data, &plug.Metadata); err != nil { - return nil, err + if err := yaml.UnmarshalStrict(data, &plug.Metadata); err != nil { + return nil, errors.Wrapf(err, "failed to load plugin at %q", pluginfile) } - return plug, nil + return plug, validatePluginData(plug, pluginfile) } // LoadAll loads all plugins found beneath the base directory. @@ -131,10 +234,10 @@ func LoadDir(dirname string) (*Plugin, error) { func LoadAll(basedir string) ([]*Plugin, error) { plugins := []*Plugin{} // We want basedir/*/plugin.yaml - scanpath := filepath.Join(basedir, "*", pluginFileName) + scanpath := filepath.Join(basedir, "*", PluginFileName) matches, err := filepath.Glob(scanpath) if err != nil { - return plugins, err + return plugins, errors.Wrapf(err, "failed to find plugins in %q", scanpath) } if matches == nil { @@ -149,7 +252,7 @@ func LoadAll(basedir string) ([]*Plugin, error) { } plugins = append(plugins, p) } - return plugins, nil + return plugins, detectDuplicates(plugins) } // FindPlugins returns a list of YAML files that describe plugins. @@ -169,32 +272,11 @@ func FindPlugins(plugdirs string) ([]*Plugin, error) { // SetupPluginEnv prepares os.Env for plugins. It operates on os.Env because // the plugin subsystem itself needs access to the environment variables // created here. -func SetupPluginEnv(settings helm_env.EnvSettings, - shortName, base string) { - for key, val := range map[string]string{ - "HELM_PLUGIN_NAME": shortName, - "HELM_PLUGIN_DIR": base, - "HELM_BIN": os.Args[0], - - // Set vars that may not have been set, and save client the - // trouble of re-parsing. - "HELM_PLUGIN": settings.PluginDirs(), - "HELM_HOME": settings.Home.String(), - - // Set vars that convey common information. - "HELM_PATH_REPOSITORY": settings.Home.Repository(), - "HELM_PATH_REPOSITORY_FILE": settings.Home.RepositoryFile(), - "HELM_PATH_CACHE": settings.Home.Cache(), - "HELM_PATH_LOCAL_REPOSITORY": settings.Home.LocalRepository(), - "HELM_PATH_STARTER": settings.Home.Starters(), - - "TILLER_HOST": settings.TillerHost, - "TILLER_NAMESPACE": settings.TillerNamespace, - } { +func SetupPluginEnv(settings *cli.EnvSettings, name, base string) { + env := settings.EnvVars() + env["HELM_PLUGIN_NAME"] = name + env["HELM_PLUGIN_DIR"] = base + for key, val := range env { os.Setenv(key, val) } - - if settings.Debug { - os.Setenv("HELM_DEBUG", "1") - } } diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go index 5ddbf15f3e9..2bbdff21d52 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/plugin_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -13,33 +13,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -package plugin // import "k8s.io/helm/pkg/plugin" +package plugin // import "helm.sh/helm/v3/pkg/plugin" import ( + "fmt" + "os" + "path/filepath" "reflect" + "runtime" "testing" + + "helm.sh/helm/v3/pkg/cli" ) -func TestPrepareCommand(t *testing.T) { - p := &Plugin{ - Dir: "/tmp", // Unused - Metadata: &Metadata{ - Name: "test", - Command: "echo -n foo", - }, +func checkCommand(p *Plugin, extraArgs []string, osStrCmp string, t *testing.T) { + cmd, args, err := p.PrepareCommand(extraArgs) + if err != nil { + t.Fatal(err) } - argv := []string{"--debug", "--foo", "bar"} - - cmd, args := p.PrepareCommand(argv) if cmd != "echo" { - t.Errorf("Expected echo, got %q", cmd) + t.Fatalf("Expected echo, got %q", cmd) } if l := len(args); l != 5 { - t.Errorf("expected 5 args, got %d", l) + t.Fatalf("expected 5 args, got %d", l) } - expect := []string{"-n", "foo", "--debug", "--foo", "bar"} + expect := []string{"-n", osStrCmp, "--debug", "--foo", "bar"} for i := 0; i < len(args); i++ { if expect[i] != args[i] { t.Errorf("Expected arg=%q, got %q", expect[i], args[i]) @@ -48,14 +48,17 @@ func TestPrepareCommand(t *testing.T) { // Test with IgnoreFlags. This should omit --debug, --foo, bar p.Metadata.IgnoreFlags = true - cmd, args = p.PrepareCommand(argv) + cmd, args, err = p.PrepareCommand(extraArgs) + if err != nil { + t.Fatal(err) + } if cmd != "echo" { - t.Errorf("Expected echo, got %q", cmd) + t.Fatalf("Expected echo, got %q", cmd) } if l := len(args); l != 2 { - t.Errorf("expected 2 args, got %d", l) + t.Fatalf("expected 2 args, got %d", l) } - expect = []string{"-n", "foo"} + expect = []string{"-n", osStrCmp} for i := 0; i < len(args); i++ { if expect[i] != args[i] { t.Errorf("Expected arg=%q, got %q", expect[i], args[i]) @@ -63,15 +66,126 @@ func TestPrepareCommand(t *testing.T) { } } +func TestPrepareCommand(t *testing.T) { + p := &Plugin{ + Dir: "/tmp", // Unused + Metadata: &Metadata{ + Name: "test", + Command: "echo -n foo", + }, + } + argv := []string{"--debug", "--foo", "bar"} + + checkCommand(p, argv, "foo", t) +} + +func TestPlatformPrepareCommand(t *testing.T) { + p := &Plugin{ + Dir: "/tmp", // Unused + Metadata: &Metadata{ + Name: "test", + Command: "echo -n os-arch", + PlatformCommand: []PlatformCommand{ + {OperatingSystem: "linux", Architecture: "i386", Command: "echo -n linux-i386"}, + {OperatingSystem: "linux", Architecture: "amd64", Command: "echo -n linux-amd64"}, + {OperatingSystem: "linux", Architecture: "arm64", Command: "echo -n linux-arm64"}, + {OperatingSystem: "linux", Architecture: "ppc64le", Command: "echo -n linux-ppc64le"}, + {OperatingSystem: "linux", Architecture: "s390x", Command: "echo -n linux-s390x"}, + {OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"}, + }, + }, + } + var osStrCmp string + os := runtime.GOOS + arch := runtime.GOARCH + if os == "linux" && arch == "i386" { + osStrCmp = "linux-i386" + } else if os == "linux" && arch == "amd64" { + osStrCmp = "linux-amd64" + } else if os == "linux" && arch == "arm64" { + osStrCmp = "linux-arm64" + } else if os == "linux" && arch == "ppc64le" { + osStrCmp = "linux-ppc64le" + } else if os == "linux" && arch == "s390x" { + osStrCmp = "linux-s390x" + } else if os == "windows" && arch == "amd64" { + osStrCmp = "win-64" + } else { + osStrCmp = "os-arch" + } + + argv := []string{"--debug", "--foo", "bar"} + checkCommand(p, argv, osStrCmp, t) +} + +func TestPartialPlatformPrepareCommand(t *testing.T) { + p := &Plugin{ + Dir: "/tmp", // Unused + Metadata: &Metadata{ + Name: "test", + Command: "echo -n os-arch", + PlatformCommand: []PlatformCommand{ + {OperatingSystem: "linux", Architecture: "i386", Command: "echo -n linux-i386"}, + {OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"}, + }, + }, + } + var osStrCmp string + os := runtime.GOOS + arch := runtime.GOARCH + if os == "linux" { + osStrCmp = "linux-i386" + } else if os == "windows" && arch == "amd64" { + osStrCmp = "win-64" + } else { + osStrCmp = "os-arch" + } + + argv := []string{"--debug", "--foo", "bar"} + checkCommand(p, argv, osStrCmp, t) +} + +func TestNoPrepareCommand(t *testing.T) { + p := &Plugin{ + Dir: "/tmp", // Unused + Metadata: &Metadata{ + Name: "test", + }, + } + argv := []string{"--debug", "--foo", "bar"} + + _, _, err := p.PrepareCommand(argv) + if err == nil { + t.Fatalf("Expected error to be returned") + } +} + +func TestNoMatchPrepareCommand(t *testing.T) { + p := &Plugin{ + Dir: "/tmp", // Unused + Metadata: &Metadata{ + Name: "test", + PlatformCommand: []PlatformCommand{ + {OperatingSystem: "no-os", Architecture: "amd64", Command: "echo -n linux-i386"}, + }, + }, + } + argv := []string{"--debug", "--foo", "bar"} + + if _, _, err := p.PrepareCommand(argv); err == nil { + t.Fatalf("Expected error to be returned") + } +} + func TestLoadDir(t *testing.T) { - dirname := "testdata/plugdir/hello" + dirname := "testdata/plugdir/good/hello" plug, err := LoadDir(dirname) if err != nil { t.Fatalf("error loading Hello plugin: %s", err) } if plug.Dir != dirname { - t.Errorf("Expected dir %q, got %q", dirname, plug.Dir) + t.Fatalf("Expected dir %q, got %q", dirname, plug.Dir) } expect := &Metadata{ @@ -80,7 +194,6 @@ func TestLoadDir(t *testing.T) { Usage: "usage", Description: "description", Command: "$HELM_PLUGIN_SELF/hello.sh", - UseTunnel: true, IgnoreFlags: true, Hooks: map[string]string{ Install: "echo installing...", @@ -88,19 +201,26 @@ func TestLoadDir(t *testing.T) { } if !reflect.DeepEqual(expect, plug.Metadata) { - t.Errorf("Expected plugin metadata %v, got %v", expect, plug.Metadata) + t.Fatalf("Expected plugin metadata %v, got %v", expect, plug.Metadata) + } +} + +func TestLoadDirDuplicateEntries(t *testing.T) { + dirname := "testdata/plugdir/bad/duplicate-entries" + if _, err := LoadDir(dirname); err == nil { + t.Errorf("successfully loaded plugin with duplicate entries when it should've failed") } } func TestDownloader(t *testing.T) { - dirname := "testdata/plugdir/downloader" + dirname := "testdata/plugdir/good/downloader" plug, err := LoadDir(dirname) if err != nil { t.Fatalf("error loading Hello plugin: %s", err) } if plug.Dir != dirname { - t.Errorf("Expected dir %q, got %q", dirname, plug.Dir) + t.Fatalf("Expected dir %q, got %q", dirname, plug.Dir) } expect := &Metadata{ @@ -118,7 +238,7 @@ func TestDownloader(t *testing.T) { } if !reflect.DeepEqual(expect, plug.Metadata) { - t.Errorf("Expected metadata %v, got %v", expect, plug.Metadata) + t.Fatalf("Expected metadata %v, got %v", expect, plug.Metadata) } } @@ -131,7 +251,7 @@ func TestLoadAll(t *testing.T) { t.Fatalf("expected empty dir to have 0 plugins") } - basedir := "testdata/plugdir" + basedir := "testdata/plugdir/good" plugs, err := LoadAll(basedir) if err != nil { t.Fatalf("Could not load %q: %s", basedir, err) @@ -151,3 +271,108 @@ func TestLoadAll(t *testing.T) { t.Errorf("Expected second plugin to be hello, got %q", plugs[1].Metadata.Name) } } + +func TestFindPlugins(t *testing.T) { + cases := []struct { + name string + plugdirs string + expected int + }{ + { + name: "plugdirs is empty", + plugdirs: "", + expected: 0, + }, + { + name: "plugdirs isn't dir", + plugdirs: "./plugin_test.go", + expected: 0, + }, + { + name: "plugdirs doesn't have plugin", + plugdirs: ".", + expected: 0, + }, + { + name: "normal", + plugdirs: "./testdata/plugdir/good", + expected: 3, + }, + } + for _, c := range cases { + t.Run(t.Name(), func(t *testing.T) { + plugin, _ := FindPlugins(c.plugdirs) + if len(plugin) != c.expected { + t.Errorf("expected: %v, got: %v", c.expected, len(plugin)) + } + }) + } +} + +func TestSetupEnv(t *testing.T) { + name := "pequod" + base := filepath.Join("testdata/helmhome/helm/plugins", name) + + s := cli.New() + s.PluginsDirectory = "testdata/helmhome/helm/plugins" + + SetupPluginEnv(s, name, base) + for _, tt := range []struct { + name, expect string + }{ + {"HELM_PLUGIN_NAME", name}, + {"HELM_PLUGIN_DIR", base}, + } { + if got := os.Getenv(tt.name); got != tt.expect { + t.Errorf("Expected $%s=%q, got %q", tt.name, tt.expect, got) + } + } +} + +func TestValidatePluginData(t *testing.T) { + for i, item := range []struct { + pass bool + plug *Plugin + }{ + {true, mockPlugin("abcdefghijklmnopqrstuvwxyz0123456789_-ABC")}, + {true, mockPlugin("foo-bar-FOO-BAR_1234")}, + {false, mockPlugin("foo -bar")}, + {false, mockPlugin("$foo -bar")}, // Test leading chars + {false, mockPlugin("foo -bar ")}, // Test trailing chars + {false, mockPlugin("foo\nbar")}, // Test newline + } { + err := validatePluginData(item.plug, fmt.Sprintf("test-%d", i)) + if item.pass && err != nil { + t.Errorf("failed to validate case %d: %s", i, err) + } else if !item.pass && err == nil { + t.Errorf("expected case %d to fail", i) + } + } +} + +func TestDetectDuplicates(t *testing.T) { + plugs := []*Plugin{ + mockPlugin("foo"), + mockPlugin("bar"), + } + if err := detectDuplicates(plugs); err != nil { + t.Error("no duplicates in the first set") + } + plugs = append(plugs, mockPlugin("foo")) + if err := detectDuplicates(plugs); err == nil { + t.Error("duplicates in the second set") + } +} + +func mockPlugin(name string) *Plugin { + return &Plugin{ + Metadata: &Metadata{ + Name: name, + Version: "v0.1.2", + Usage: "Mock plugin", + Description: "Mock plugin for testing", + Command: "echo mock plugin", + }, + Dir: "no-such-dir", + } +} diff --git a/pkg/plugin/testdata/plugdir/bad/duplicate-entries/plugin.yaml b/pkg/plugin/testdata/plugdir/bad/duplicate-entries/plugin.yaml new file mode 100644 index 00000000000..66498be960a --- /dev/null +++ b/pkg/plugin/testdata/plugdir/bad/duplicate-entries/plugin.yaml @@ -0,0 +1,11 @@ +name: "duplicate-entries" +version: "0.1.0" +usage: "usage" +description: |- + description +command: "echo hello" +ignoreFlags: true +hooks: + install: "echo installing..." +hooks: + install: "echo installing something different" diff --git a/pkg/plugin/testdata/plugdir/downloader/plugin.yaml b/pkg/plugin/testdata/plugdir/good/downloader/plugin.yaml similarity index 100% rename from pkg/plugin/testdata/plugdir/downloader/plugin.yaml rename to pkg/plugin/testdata/plugdir/good/downloader/plugin.yaml diff --git a/pkg/plugin/testdata/plugdir/echo/plugin.yaml b/pkg/plugin/testdata/plugdir/good/echo/plugin.yaml similarity index 100% rename from pkg/plugin/testdata/plugdir/echo/plugin.yaml rename to pkg/plugin/testdata/plugdir/good/echo/plugin.yaml diff --git a/pkg/plugin/testdata/plugdir/good/hello/hello.sh b/pkg/plugin/testdata/plugdir/good/hello/hello.sh new file mode 100755 index 00000000000..dcfd5887659 --- /dev/null +++ b/pkg/plugin/testdata/plugdir/good/hello/hello.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +echo "Hello from a Helm plugin" + +echo "PARAMS" +echo $* + +$HELM_BIN ls --all + diff --git a/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml b/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml new file mode 100644 index 00000000000..2b972da59a7 --- /dev/null +++ b/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml @@ -0,0 +1,9 @@ +name: "hello" +version: "0.1.0" +usage: "usage" +description: |- + description +command: "$HELM_PLUGIN_SELF/hello.sh" +ignoreFlags: true +hooks: + install: "echo installing..." diff --git a/pkg/plugin/testdata/plugdir/hello/hello.sh b/pkg/plugin/testdata/plugdir/hello/hello.sh deleted file mode 100755 index db7c0f54daf..00000000000 --- a/pkg/plugin/testdata/plugdir/hello/hello.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -echo "Hello from a Helm plugin" - -echo "PARAMS" -echo $* - -echo "ENVIRONMENT" -echo $TILLER_HOST -echo $HELM_HOME - -$HELM_BIN --host $TILLER_HOST ls --all - diff --git a/pkg/plugin/testdata/plugdir/hello/plugin.yaml b/pkg/plugin/testdata/plugdir/hello/plugin.yaml deleted file mode 100644 index cdb27b29122..00000000000 --- a/pkg/plugin/testdata/plugdir/hello/plugin.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: "hello" -version: "0.1.0" -usage: "usage" -description: |- - description -command: "$HELM_PLUGIN_SELF/hello.sh" -useTunnel: true -ignoreFlags: true -install: "echo installing..." -hooks: - install: "echo installing..." diff --git a/pkg/postrender/exec.go b/pkg/postrender/exec.go new file mode 100644 index 00000000000..1de70b02488 --- /dev/null +++ b/pkg/postrender/exec.go @@ -0,0 +1,108 @@ +/* +Copyright The Helm Authors. + +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 postrender + +import ( + "bytes" + "io" + "os/exec" + "path/filepath" + + "github.com/pkg/errors" +) + +type execRender struct { + binaryPath string +} + +// NewExec returns a PostRenderer implementation that calls the provided binary. +// It returns an error if the binary cannot be found. If the path does not +// contain any separators, it will search in $PATH, otherwise it will resolve +// any relative paths to a fully qualified path +func NewExec(binaryPath string) (PostRenderer, error) { + fullPath, err := getFullPath(binaryPath) + if err != nil { + return nil, err + } + return &execRender{fullPath}, nil +} + +// Run the configured binary for the post render +func (p *execRender) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) { + cmd := exec.Command(p.binaryPath) + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + + var postRendered = &bytes.Buffer{} + var stderr = &bytes.Buffer{} + cmd.Stdout = postRendered + cmd.Stderr = stderr + + go func() { + defer stdin.Close() + io.Copy(stdin, renderedManifests) + }() + err = cmd.Run() + if err != nil { + return nil, errors.Wrapf(err, "error while running command %s. error output:\n%s", p.binaryPath, stderr.String()) + } + + return postRendered, nil +} + +// getFullPath returns the full filepath to the binary to execute. If the path +// does not contain any separators, it will search in $PATH, otherwise it will +// resolve any relative paths to a fully qualified path +func getFullPath(binaryPath string) (string, error) { + // NOTE(thomastaylor312): I am leaving this code commented out here. During + // the implementation of post-render, it was brought up that if we are + // relying on plugins, we should actually use the plugin system so it can + // properly handle multiple OSs. This will be a feature add in the future, + // so I left this code for reference. It can be deleted or reused once the + // feature is implemented + + // Manually check the plugin dir first + // if !strings.Contains(binaryPath, string(filepath.Separator)) { + // // First check the plugin dir + // pluginDir := helmpath.DataPath("plugins") // Default location + // // If location for plugins is explicitly set, check there + // if v, ok := os.LookupEnv("HELM_PLUGINS"); ok { + // pluginDir = v + // } + // // The plugins variable can actually contain multiple paths, so loop through those + // for _, p := range filepath.SplitList(pluginDir) { + // _, err := os.Stat(filepath.Join(p, binaryPath)) + // if err != nil && !os.IsNotExist(err) { + // return "", err + // } else if err == nil { + // binaryPath = filepath.Join(p, binaryPath) + // break + // } + // } + // } + + // Now check for the binary using the given path or check if it exists in + // the path and is executable + checkedPath, err := exec.LookPath(binaryPath) + if err != nil { + return "", errors.Wrapf(err, "unable to find binary at %s", binaryPath) + } + + return filepath.Abs(checkedPath) +} diff --git a/pkg/postrender/exec_test.go b/pkg/postrender/exec_test.go new file mode 100644 index 00000000000..ef095694999 --- /dev/null +++ b/pkg/postrender/exec_test.go @@ -0,0 +1,155 @@ +/* +Copyright The Helm Authors. + +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 postrender + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "helm.sh/helm/v3/internal/test/ensure" +) + +const testingScript = `#!/bin/sh +sed s/FOOTEST/BARTEST/g <&0 +` + +func TestGetFullPath(t *testing.T) { + is := assert.New(t) + t.Run("full path resolves correctly", func(t *testing.T) { + testpath, cleanup := setupTestingScript(t) + defer cleanup() + + fullPath, err := getFullPath(testpath) + is.NoError(err) + is.Equal(testpath, fullPath) + }) + + t.Run("relative path resolves correctly", func(t *testing.T) { + testpath, cleanup := setupTestingScript(t) + defer cleanup() + + currentDir, err := os.Getwd() + require.NoError(t, err) + relative, err := filepath.Rel(currentDir, testpath) + require.NoError(t, err) + fullPath, err := getFullPath(relative) + is.NoError(err) + is.Equal(testpath, fullPath) + }) + + t.Run("binary in PATH resolves correctly", func(t *testing.T) { + testpath, cleanup := setupTestingScript(t) + defer cleanup() + + realPath := os.Getenv("PATH") + os.Setenv("PATH", filepath.Dir(testpath)) + defer func() { + os.Setenv("PATH", realPath) + }() + + fullPath, err := getFullPath(filepath.Base(testpath)) + is.NoError(err) + is.Equal(testpath, fullPath) + }) + + // NOTE(thomastaylor312): See note in getFullPath for more details why this + // is here + + // t.Run("binary in plugin path resolves correctly", func(t *testing.T) { + // testpath, cleanup := setupTestingScript(t) + // defer cleanup() + + // realPath := os.Getenv("HELM_PLUGINS") + // os.Setenv("HELM_PLUGINS", filepath.Dir(testpath)) + // defer func() { + // os.Setenv("HELM_PLUGINS", realPath) + // }() + + // fullPath, err := getFullPath(filepath.Base(testpath)) + // is.NoError(err) + // is.Equal(testpath, fullPath) + // }) + + // t.Run("binary in multiple plugin paths resolves correctly", func(t *testing.T) { + // testpath, cleanup := setupTestingScript(t) + // defer cleanup() + + // realPath := os.Getenv("HELM_PLUGINS") + // os.Setenv("HELM_PLUGINS", filepath.Dir(testpath)+string(os.PathListSeparator)+"/another/dir") + // defer func() { + // os.Setenv("HELM_PLUGINS", realPath) + // }() + + // fullPath, err := getFullPath(filepath.Base(testpath)) + // is.NoError(err) + // is.Equal(testpath, fullPath) + // }) +} + +func TestExecRun(t *testing.T) { + if runtime.GOOS == "windows" { + // the actual Run test uses a basic sed example, so skip this test on windows + t.Skip("skipping on windows") + } + is := assert.New(t) + testpath, cleanup := setupTestingScript(t) + defer cleanup() + + renderer, err := NewExec(testpath) + require.NoError(t, err) + + output, err := renderer.Run(bytes.NewBufferString("FOOTEST")) + is.NoError(err) + is.Contains(output.String(), "BARTEST") +} + +func setupTestingScript(t *testing.T) (filepath string, cleanup func()) { + t.Helper() + + tempdir := ensure.TempDir(t) + + f, err := ioutil.TempFile(tempdir, "post-render-test.sh") + if err != nil { + t.Fatalf("unable to create tempfile for testing: %s", err) + } + + _, err = f.WriteString(testingScript) + if err != nil { + t.Fatalf("unable to write tempfile for testing: %s", err) + } + + err = f.Chmod(0755) + if err != nil { + t.Fatalf("unable to make tempfile executable for testing: %s", err) + } + + err = f.Close() + if err != nil { + t.Fatalf("unable to close tempfile after writing: %s", err) + } + + return f.Name(), func() { + os.RemoveAll(tempdir) + } +} diff --git a/pkg/postrender/postrender.go b/pkg/postrender/postrender.go new file mode 100644 index 00000000000..3af38429077 --- /dev/null +++ b/pkg/postrender/postrender.go @@ -0,0 +1,29 @@ +/* +Copyright The Helm Authors. + +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 postrender contains an interface that can be implemented for custom +// post-renderers and an exec implementation that can be used for arbitrary +// binaries and scripts +package postrender + +import "bytes" + +type PostRenderer interface { + // Run expects a single buffer filled with Helm rendered manifests. It + // expects the modified results to be returned on a separate buffer or an + // error if there was an issue or failure while running the post render step + Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) +} diff --git a/pkg/proto/hapi/chart/chart.pb.go b/pkg/proto/hapi/chart/chart.pb.go deleted file mode 100644 index a884ed552ee..00000000000 --- a/pkg/proto/hapi/chart/chart.pb.go +++ /dev/null @@ -1,119 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/chart/chart.proto - -/* -Package chart is a generated protocol buffer package. - -It is generated from these files: - hapi/chart/chart.proto - hapi/chart/config.proto - hapi/chart/metadata.proto - hapi/chart/template.proto - -It has these top-level messages: - Chart - Config - Value - Maintainer - Metadata - Template -*/ -package chart - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" -import google_protobuf "github.com/golang/protobuf/ptypes/any" - -// 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. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -// Chart is a helm package that contains metadata, a default config, zero or more -// optionally parameterizable templates, and zero or more charts (dependencies). -type Chart struct { - // Contents of the Chartfile. - Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata" json:"metadata,omitempty"` - // Templates for this chart. - Templates []*Template `protobuf:"bytes,2,rep,name=templates" json:"templates,omitempty"` - // Charts that this chart depends on. - Dependencies []*Chart `protobuf:"bytes,3,rep,name=dependencies" json:"dependencies,omitempty"` - // Default config for this template. - Values *Config `protobuf:"bytes,4,opt,name=values" json:"values,omitempty"` - // Miscellaneous files in a chart archive, - // e.g. README, LICENSE, etc. - Files []*google_protobuf.Any `protobuf:"bytes,5,rep,name=files" json:"files,omitempty"` -} - -func (m *Chart) Reset() { *m = Chart{} } -func (m *Chart) String() string { return proto.CompactTextString(m) } -func (*Chart) ProtoMessage() {} -func (*Chart) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } - -func (m *Chart) GetMetadata() *Metadata { - if m != nil { - return m.Metadata - } - return nil -} - -func (m *Chart) GetTemplates() []*Template { - if m != nil { - return m.Templates - } - return nil -} - -func (m *Chart) GetDependencies() []*Chart { - if m != nil { - return m.Dependencies - } - return nil -} - -func (m *Chart) GetValues() *Config { - if m != nil { - return m.Values - } - return nil -} - -func (m *Chart) GetFiles() []*google_protobuf.Any { - if m != nil { - return m.Files - } - return nil -} - -func init() { - proto.RegisterType((*Chart)(nil), "hapi.chart.Chart") -} - -func init() { proto.RegisterFile("hapi/chart/chart.proto", fileDescriptor0) } - -var fileDescriptor0 = []byte{ - // 242 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0xb1, 0x4e, 0xc3, 0x30, - 0x10, 0x86, 0x15, 0x4a, 0x0a, 0x1c, 0x2c, 0x58, 0x08, 0x4c, 0xa7, 0x8a, 0x09, 0x75, 0x70, 0x50, - 0x11, 0x0f, 0x00, 0xcc, 0x2c, 0x16, 0x13, 0xdb, 0xb5, 0xb9, 0xa4, 0x91, 0x52, 0x3b, 0xaa, 0x5d, - 0xa4, 0xbe, 0x3b, 0x03, 0xea, 0xd9, 0xa6, 0x09, 0xea, 0x12, 0x29, 0xf7, 0x7d, 0xff, 0xe5, 0xbf, - 0xc0, 0xed, 0x0a, 0xbb, 0xa6, 0x58, 0xae, 0x70, 0xe3, 0xc3, 0x53, 0x75, 0x1b, 0xeb, 0xad, 0x80, - 0xfd, 0x5c, 0xf1, 0x64, 0x72, 0xd7, 0x77, 0xac, 0xa9, 0x9a, 0x3a, 0x48, 0x93, 0xfb, 0x1e, 0x58, - 0x93, 0xc7, 0x12, 0x3d, 0x1e, 0x41, 0x9e, 0xd6, 0x5d, 0x8b, 0x9e, 0x12, 0xaa, 0xad, 0xad, 0x5b, - 0x2a, 0xf8, 0x6d, 0xb1, 0xad, 0x0a, 0x34, 0xbb, 0x80, 0x1e, 0x7e, 0x32, 0xc8, 0xdf, 0xf7, 0x19, - 0xf1, 0x04, 0xe7, 0x69, 0xa3, 0xcc, 0xa6, 0xd9, 0xe3, 0xe5, 0xfc, 0x46, 0x1d, 0x2a, 0xa9, 0x8f, - 0xc8, 0xf4, 0x9f, 0x25, 0xe6, 0x70, 0x91, 0x3e, 0xe4, 0xe4, 0xc9, 0x74, 0xf4, 0x3f, 0xf2, 0x19, - 0xa1, 0x3e, 0x68, 0xe2, 0x05, 0xae, 0x4a, 0xea, 0xc8, 0x94, 0x64, 0x96, 0x0d, 0x39, 0x39, 0xe2, - 0xd8, 0x75, 0x3f, 0xc6, 0x75, 0xf4, 0x40, 0x13, 0x33, 0x18, 0x7f, 0x63, 0xbb, 0x25, 0x27, 0x4f, - 0xb9, 0x9a, 0x18, 0x04, 0xf8, 0x0f, 0xe9, 0x68, 0x88, 0x19, 0xe4, 0x55, 0xd3, 0x92, 0x93, 0x79, - 0xac, 0x14, 0xae, 0x57, 0xe9, 0x7a, 0xf5, 0x6a, 0x76, 0x3a, 0x28, 0x6f, 0x67, 0x5f, 0x39, 0xef, - 0x58, 0x8c, 0x99, 0x3e, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x70, 0x34, 0x75, 0x9e, 0x01, - 0x00, 0x00, -} diff --git a/pkg/proto/hapi/chart/config.pb.go b/pkg/proto/hapi/chart/config.pb.go deleted file mode 100644 index 30c652700a1..00000000000 --- a/pkg/proto/hapi/chart/config.pb.go +++ /dev/null @@ -1,78 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/chart/config.proto - -package chart - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// Config supplies values to the parametrizable templates of a chart. -type Config struct { - Raw string `protobuf:"bytes,1,opt,name=raw" json:"raw,omitempty"` - Values map[string]*Value `protobuf:"bytes,2,rep,name=values" json:"values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` -} - -func (m *Config) Reset() { *m = Config{} } -func (m *Config) String() string { return proto.CompactTextString(m) } -func (*Config) ProtoMessage() {} -func (*Config) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} } - -func (m *Config) GetRaw() string { - if m != nil { - return m.Raw - } - return "" -} - -func (m *Config) GetValues() map[string]*Value { - if m != nil { - return m.Values - } - return nil -} - -// Value describes a configuration value as a string. -type Value struct { - Value string `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"` -} - -func (m *Value) Reset() { *m = Value{} } -func (m *Value) String() string { return proto.CompactTextString(m) } -func (*Value) ProtoMessage() {} -func (*Value) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{1} } - -func (m *Value) GetValue() string { - if m != nil { - return m.Value - } - return "" -} - -func init() { - proto.RegisterType((*Config)(nil), "hapi.chart.Config") - proto.RegisterType((*Value)(nil), "hapi.chart.Value") -} - -func init() { proto.RegisterFile("hapi/chart/config.proto", fileDescriptor1) } - -var fileDescriptor1 = []byte{ - // 182 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, - 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x4f, 0xce, 0xcf, 0x4b, 0xcb, 0x4c, 0xd7, 0x2b, 0x28, - 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe8, 0x81, 0x25, 0x94, 0x16, 0x30, 0x72, 0xb1, 0x39, - 0x83, 0x25, 0x85, 0x04, 0xb8, 0x98, 0x8b, 0x12, 0xcb, 0x25, 0x18, 0x15, 0x18, 0x35, 0x38, 0x83, - 0x40, 0x4c, 0x21, 0x33, 0x2e, 0xb6, 0xb2, 0xc4, 0x9c, 0xd2, 0xd4, 0x62, 0x09, 0x26, 0x05, 0x66, - 0x0d, 0x6e, 0x23, 0x39, 0x3d, 0x84, 0x4e, 0x3d, 0x88, 0x2e, 0xbd, 0x30, 0xb0, 0x02, 0xd7, 0xbc, - 0x92, 0xa2, 0xca, 0x20, 0xa8, 0x6a, 0x29, 0x1f, 0x2e, 0x6e, 0x24, 0x61, 0x90, 0xc1, 0xd9, 0xa9, - 0x95, 0x30, 0x83, 0xb3, 0x53, 0x2b, 0x85, 0xd4, 0xb9, 0x58, 0xc1, 0x4a, 0x25, 0x98, 0x14, 0x18, - 0x35, 0xb8, 0x8d, 0x04, 0x91, 0xcd, 0x05, 0xeb, 0x0c, 0x82, 0xc8, 0x5b, 0x31, 0x59, 0x30, 0x2a, - 0xc9, 0x72, 0xb1, 0x82, 0xc5, 0x84, 0x44, 0x60, 0xba, 0x20, 0x26, 0x41, 0x38, 0x4e, 0xec, 0x51, - 0xac, 0x60, 0x8d, 0x49, 0x6c, 0x60, 0xdf, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xe1, 0x12, - 0x60, 0xda, 0xf8, 0x00, 0x00, 0x00, -} diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go deleted file mode 100644 index 9daeaa9e565..00000000000 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ /dev/null @@ -1,276 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/chart/metadata.proto - -package chart - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -type Metadata_Engine int32 - -const ( - Metadata_UNKNOWN Metadata_Engine = 0 - Metadata_GOTPL Metadata_Engine = 1 -) - -var Metadata_Engine_name = map[int32]string{ - 0: "UNKNOWN", - 1: "GOTPL", -} -var Metadata_Engine_value = map[string]int32{ - "UNKNOWN": 0, - "GOTPL": 1, -} - -func (x Metadata_Engine) String() string { - return proto.EnumName(Metadata_Engine_name, int32(x)) -} -func (Metadata_Engine) EnumDescriptor() ([]byte, []int) { return fileDescriptor2, []int{1, 0} } - -// Maintainer describes a Chart maintainer. -type Maintainer struct { - // Name is a user name or organization name - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // Email is an optional email address to contact the named maintainer - Email string `protobuf:"bytes,2,opt,name=email" json:"email,omitempty"` - // Url is an optional URL to an address for the named maintainer - Url string `protobuf:"bytes,3,opt,name=url" json:"url,omitempty"` -} - -func (m *Maintainer) Reset() { *m = Maintainer{} } -func (m *Maintainer) String() string { return proto.CompactTextString(m) } -func (*Maintainer) ProtoMessage() {} -func (*Maintainer) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} } - -func (m *Maintainer) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *Maintainer) GetEmail() string { - if m != nil { - return m.Email - } - return "" -} - -func (m *Maintainer) GetUrl() string { - if m != nil { - return m.Url - } - return "" -} - -// Metadata for a Chart file. This models the structure of a Chart.yaml file. -// -// Spec: https://k8s.io/helm/blob/master/docs/design/chart_format.md#the-chart-file -type Metadata struct { - // The name of the chart - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // The URL to a relevant project page, git repo, or contact person - Home string `protobuf:"bytes,2,opt,name=home" json:"home,omitempty"` - // Source is the URL to the source code of this chart - Sources []string `protobuf:"bytes,3,rep,name=sources" json:"sources,omitempty"` - // A SemVer 2 conformant version string of the chart - Version string `protobuf:"bytes,4,opt,name=version" json:"version,omitempty"` - // A one-sentence description of the chart - Description string `protobuf:"bytes,5,opt,name=description" json:"description,omitempty"` - // A list of string keywords - Keywords []string `protobuf:"bytes,6,rep,name=keywords" json:"keywords,omitempty"` - // A list of name and URL/email address combinations for the maintainer(s) - Maintainers []*Maintainer `protobuf:"bytes,7,rep,name=maintainers" json:"maintainers,omitempty"` - // The name of the template engine to use. Defaults to 'gotpl'. - Engine string `protobuf:"bytes,8,opt,name=engine" json:"engine,omitempty"` - // The URL to an icon file. - Icon string `protobuf:"bytes,9,opt,name=icon" json:"icon,omitempty"` - // The API Version of this chart. - ApiVersion string `protobuf:"bytes,10,opt,name=apiVersion" json:"apiVersion,omitempty"` - // The condition to check to enable chart - Condition string `protobuf:"bytes,11,opt,name=condition" json:"condition,omitempty"` - // The tags to check to enable chart - Tags string `protobuf:"bytes,12,opt,name=tags" json:"tags,omitempty"` - // The version of the application enclosed inside of this chart. - AppVersion string `protobuf:"bytes,13,opt,name=appVersion" json:"appVersion,omitempty"` - // Whether or not this chart is deprecated - Deprecated bool `protobuf:"varint,14,opt,name=deprecated" json:"deprecated,omitempty"` - // TillerVersion is a SemVer constraints on what version of Tiller is required. - // See SemVer ranges here: https://github.com/Masterminds/semver#basic-comparisons - TillerVersion string `protobuf:"bytes,15,opt,name=tillerVersion" json:"tillerVersion,omitempty"` - // Annotations are additional mappings uninterpreted by Tiller, - // made available for inspection by other applications. - Annotations map[string]string `protobuf:"bytes,16,rep,name=annotations" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - // KubeVersion is a SemVer constraint specifying the version of Kubernetes required. - KubeVersion string `protobuf:"bytes,17,opt,name=kubeVersion" json:"kubeVersion,omitempty"` -} - -func (m *Metadata) Reset() { *m = Metadata{} } -func (m *Metadata) String() string { return proto.CompactTextString(m) } -func (*Metadata) ProtoMessage() {} -func (*Metadata) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} } - -func (m *Metadata) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *Metadata) GetHome() string { - if m != nil { - return m.Home - } - return "" -} - -func (m *Metadata) GetSources() []string { - if m != nil { - return m.Sources - } - return nil -} - -func (m *Metadata) GetVersion() string { - if m != nil { - return m.Version - } - return "" -} - -func (m *Metadata) GetDescription() string { - if m != nil { - return m.Description - } - return "" -} - -func (m *Metadata) GetKeywords() []string { - if m != nil { - return m.Keywords - } - return nil -} - -func (m *Metadata) GetMaintainers() []*Maintainer { - if m != nil { - return m.Maintainers - } - return nil -} - -func (m *Metadata) GetEngine() string { - if m != nil { - return m.Engine - } - return "" -} - -func (m *Metadata) GetIcon() string { - if m != nil { - return m.Icon - } - return "" -} - -func (m *Metadata) GetApiVersion() string { - if m != nil { - return m.ApiVersion - } - return "" -} - -func (m *Metadata) GetCondition() string { - if m != nil { - return m.Condition - } - return "" -} - -func (m *Metadata) GetTags() string { - if m != nil { - return m.Tags - } - return "" -} - -func (m *Metadata) GetAppVersion() string { - if m != nil { - return m.AppVersion - } - return "" -} - -func (m *Metadata) GetDeprecated() bool { - if m != nil { - return m.Deprecated - } - return false -} - -func (m *Metadata) GetTillerVersion() string { - if m != nil { - return m.TillerVersion - } - return "" -} - -func (m *Metadata) GetAnnotations() map[string]string { - if m != nil { - return m.Annotations - } - return nil -} - -func (m *Metadata) GetKubeVersion() string { - if m != nil { - return m.KubeVersion - } - return "" -} - -func init() { - proto.RegisterType((*Maintainer)(nil), "hapi.chart.Maintainer") - proto.RegisterType((*Metadata)(nil), "hapi.chart.Metadata") - proto.RegisterEnum("hapi.chart.Metadata_Engine", Metadata_Engine_name, Metadata_Engine_value) -} - -func init() { proto.RegisterFile("hapi/chart/metadata.proto", fileDescriptor2) } - -var fileDescriptor2 = []byte{ - // 435 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0x5d, 0x6b, 0xd4, 0x40, - 0x14, 0x35, 0xcd, 0x66, 0x77, 0x73, 0x63, 0x35, 0x0e, 0x52, 0xc6, 0x22, 0x12, 0x16, 0x85, 0x7d, - 0xda, 0x82, 0xbe, 0x14, 0x1f, 0x04, 0x85, 0x52, 0x41, 0xbb, 0x95, 0xe0, 0x07, 0xf8, 0x36, 0x4d, - 0x2e, 0xdd, 0x61, 0x93, 0x99, 0x30, 0x99, 0xad, 0xec, 0xaf, 0xf0, 0x2f, 0xcb, 0xdc, 0x64, 0x9a, - 0xac, 0xf4, 0xed, 0x9e, 0x73, 0x66, 0xce, 0xcc, 0xbd, 0xf7, 0xc0, 0x8b, 0x8d, 0x68, 0xe4, 0x59, - 0xb1, 0x11, 0xc6, 0x9e, 0xd5, 0x68, 0x45, 0x29, 0xac, 0x58, 0x35, 0x46, 0x5b, 0xcd, 0xc0, 0x49, - 0x2b, 0x92, 0x16, 0x9f, 0x01, 0xae, 0x84, 0x54, 0x56, 0x48, 0x85, 0x86, 0x31, 0x98, 0x28, 0x51, - 0x23, 0x0f, 0xb2, 0x60, 0x19, 0xe7, 0x54, 0xb3, 0xe7, 0x10, 0x61, 0x2d, 0x64, 0xc5, 0x8f, 0x88, - 0xec, 0x00, 0x4b, 0x21, 0xdc, 0x99, 0x8a, 0x87, 0xc4, 0xb9, 0x72, 0xf1, 0x37, 0x82, 0xf9, 0x55, - 0xff, 0xd0, 0x83, 0x46, 0x0c, 0x26, 0x1b, 0x5d, 0x63, 0xef, 0x43, 0x35, 0xe3, 0x30, 0x6b, 0xf5, - 0xce, 0x14, 0xd8, 0xf2, 0x30, 0x0b, 0x97, 0x71, 0xee, 0xa1, 0x53, 0xee, 0xd0, 0xb4, 0x52, 0x2b, - 0x3e, 0xa1, 0x0b, 0x1e, 0xb2, 0x0c, 0x92, 0x12, 0xdb, 0xc2, 0xc8, 0xc6, 0x3a, 0x35, 0x22, 0x75, - 0x4c, 0xb1, 0x53, 0x98, 0x6f, 0x71, 0xff, 0x47, 0x9b, 0xb2, 0xe5, 0x53, 0xb2, 0xbd, 0xc7, 0xec, - 0x1c, 0x92, 0xfa, 0xbe, 0xe1, 0x96, 0xcf, 0xb2, 0x70, 0x99, 0xbc, 0x3d, 0x59, 0x0d, 0x23, 0x59, - 0x0d, 0xf3, 0xc8, 0xc7, 0x47, 0xd9, 0x09, 0x4c, 0x51, 0xdd, 0x4a, 0x85, 0x7c, 0x4e, 0x4f, 0xf6, - 0xc8, 0xf5, 0x25, 0x0b, 0xad, 0x78, 0xdc, 0xf5, 0xe5, 0x6a, 0xf6, 0x0a, 0x40, 0x34, 0xf2, 0x67, - 0xdf, 0x00, 0x90, 0x32, 0x62, 0xd8, 0x4b, 0x88, 0x0b, 0xad, 0x4a, 0x49, 0x1d, 0x24, 0x24, 0x0f, - 0x84, 0x73, 0xb4, 0xe2, 0xb6, 0xe5, 0x8f, 0x3b, 0x47, 0x57, 0x77, 0x8e, 0x8d, 0x77, 0x3c, 0xf6, - 0x8e, 0x9e, 0x71, 0x7a, 0x89, 0x8d, 0xc1, 0x42, 0x58, 0x2c, 0xf9, 0x93, 0x2c, 0x58, 0xce, 0xf3, - 0x11, 0xc3, 0x5e, 0xc3, 0xb1, 0x95, 0x55, 0x85, 0xc6, 0x5b, 0x3c, 0x25, 0x8b, 0x43, 0x92, 0x5d, - 0x42, 0x22, 0x94, 0xd2, 0x56, 0xb8, 0x7f, 0xb4, 0x3c, 0xa5, 0xe9, 0xbc, 0x39, 0x98, 0x8e, 0xcf, - 0xd2, 0xc7, 0xe1, 0xdc, 0x85, 0xb2, 0x66, 0x9f, 0x8f, 0x6f, 0xba, 0x25, 0x6d, 0x77, 0x37, 0xe8, - 0x1f, 0x7b, 0xd6, 0x2d, 0x69, 0x44, 0x9d, 0x7e, 0x80, 0xf4, 0x7f, 0x0b, 0x97, 0xaa, 0x2d, 0xee, - 0xfb, 0xd4, 0xb8, 0xd2, 0xa5, 0xef, 0x4e, 0x54, 0x3b, 0x9f, 0x9a, 0x0e, 0xbc, 0x3f, 0x3a, 0x0f, - 0x16, 0x19, 0x4c, 0x2f, 0xba, 0x05, 0x24, 0x30, 0xfb, 0xb1, 0xfe, 0xb2, 0xbe, 0xfe, 0xb5, 0x4e, - 0x1f, 0xb1, 0x18, 0xa2, 0xcb, 0xeb, 0xef, 0xdf, 0xbe, 0xa6, 0xc1, 0xa7, 0xd9, 0xef, 0x88, 0xfe, - 0x7c, 0x33, 0xa5, 0xdc, 0xbf, 0xfb, 0x17, 0x00, 0x00, 0xff, 0xff, 0x36, 0xf9, 0x0d, 0xa6, 0x14, - 0x03, 0x00, 0x00, -} diff --git a/pkg/proto/hapi/chart/template.pb.go b/pkg/proto/hapi/chart/template.pb.go deleted file mode 100644 index 439aec5a8fd..00000000000 --- a/pkg/proto/hapi/chart/template.pb.go +++ /dev/null @@ -1,60 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/chart/template.proto - -package chart - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// Template represents a template as a name/value pair. -// -// By convention, name is a relative path within the scope of the chart's -// base directory. -type Template struct { - // Name is the path-like name of the template. - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // Data is the template as byte data. - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` -} - -func (m *Template) Reset() { *m = Template{} } -func (m *Template) String() string { return proto.CompactTextString(m) } -func (*Template) ProtoMessage() {} -func (*Template) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} } - -func (m *Template) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *Template) GetData() []byte { - if m != nil { - return m.Data - } - return nil -} - -func init() { - proto.RegisterType((*Template)(nil), "hapi.chart.Template") -} - -func init() { proto.RegisterFile("hapi/chart/template.proto", fileDescriptor3) } - -var fileDescriptor3 = []byte{ - // 107 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0xcc, 0x48, 0x2c, 0xc8, - 0xd4, 0x4f, 0xce, 0x48, 0x2c, 0x2a, 0xd1, 0x2f, 0x49, 0xcd, 0x2d, 0xc8, 0x49, 0x2c, 0x49, 0xd5, - 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x02, 0x49, 0xe9, 0x81, 0xa5, 0x94, 0x8c, 0xb8, 0x38, - 0x42, 0xa0, 0xb2, 0x42, 0x42, 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, - 0x9c, 0x41, 0x60, 0x36, 0x48, 0x2c, 0x25, 0xb1, 0x24, 0x51, 0x82, 0x49, 0x81, 0x51, 0x83, 0x27, - 0x08, 0xcc, 0x76, 0x62, 0x8f, 0x62, 0x05, 0x6b, 0x4e, 0x62, 0x03, 0x9b, 0x67, 0x0c, 0x08, 0x00, - 0x00, 0xff, 0xff, 0x53, 0xee, 0x0e, 0x67, 0x6c, 0x00, 0x00, 0x00, -} diff --git a/pkg/proto/hapi/release/hook.pb.go b/pkg/proto/hapi/release/hook.pb.go deleted file mode 100644 index 00fa5c18857..00000000000 --- a/pkg/proto/hapi/release/hook.pb.go +++ /dev/null @@ -1,231 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/release/hook.proto - -/* -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 - -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. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -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 - Hook_PRE_ROLLBACK Hook_Event = 7 - Hook_POST_ROLLBACK Hook_Event = 8 - Hook_RELEASE_TEST_SUCCESS Hook_Event = 9 - Hook_RELEASE_TEST_FAILURE Hook_Event = 10 -) - -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", - 7: "PRE_ROLLBACK", - 8: "POST_ROLLBACK", - 9: "RELEASE_TEST_SUCCESS", - 10: "RELEASE_TEST_FAILURE", -} -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, - "PRE_ROLLBACK": 7, - "POST_ROLLBACK": 8, - "RELEASE_TEST_SUCCESS": 9, - "RELEASE_TEST_FAILURE": 10, -} - -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} } - -type Hook_DeletePolicy int32 - -const ( - Hook_SUCCEEDED Hook_DeletePolicy = 0 - Hook_FAILED Hook_DeletePolicy = 1 - Hook_BEFORE_HOOK_CREATION Hook_DeletePolicy = 2 -) - -var Hook_DeletePolicy_name = map[int32]string{ - 0: "SUCCEEDED", - 1: "FAILED", - 2: "BEFORE_HOOK_CREATION", -} -var Hook_DeletePolicy_value = map[string]int32{ - "SUCCEEDED": 0, - "FAILED": 1, - "BEFORE_HOOK_CREATION": 2, -} - -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"` - // 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,packed,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"` - // 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{} } -func (m *Hook) String() string { return proto.CompactTextString(m) } -func (*Hook) ProtoMessage() {} -func (*Hook) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } - -func (m *Hook) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *Hook) GetKind() string { - if m != nil { - return m.Kind - } - return "" -} - -func (m *Hook) GetPath() string { - if m != nil { - return m.Path - } - return "" -} - -func (m *Hook) GetManifest() string { - if m != nil { - return m.Manifest - } - return "" -} - -func (m *Hook) GetEvents() []Hook_Event { - if m != nil { - return m.Events - } - return nil -} - -func (m *Hook) GetLastRun() *google_protobuf.Timestamp { - if m != nil { - return m.LastRun - } - return nil -} - -func (m *Hook) GetWeight() int32 { - if m != nil { - return m.Weight - } - 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{ - // 445 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x91, 0x51, 0x8f, 0x9a, 0x40, - 0x10, 0x80, 0x8f, 0x13, 0x41, 0x47, 0xcf, 0xdb, 0x6e, 0x9a, 0x76, 0xe3, 0xcb, 0x19, 0x9f, 0x7c, - 0xc2, 0xe6, 0x9a, 0xfe, 0x00, 0x84, 0xb9, 0x6a, 0x24, 0x60, 0x16, 0x4c, 0x93, 0xbe, 0x10, 0xae, - 0xee, 0x29, 0x11, 0x81, 0x08, 0xb6, 0xe9, 0x0f, 0xec, 0x3f, 0xe8, 0x0f, 0x6a, 0x76, 0x45, 0x7b, - 0x49, 0xfb, 0x36, 0xf3, 0xcd, 0x37, 0xc3, 0x0c, 0x0b, 0xef, 0x77, 0x49, 0x99, 0x4e, 0x8f, 0x22, - 0x13, 0x49, 0x25, 0xa6, 0xbb, 0xa2, 0xd8, 0x5b, 0xe5, 0xb1, 0xa8, 0x0b, 0xda, 0x97, 0x05, 0xab, - 0x29, 0x0c, 0x1f, 0xb6, 0x45, 0xb1, 0xcd, 0xc4, 0x54, 0xd5, 0x9e, 0x4f, 0x2f, 0xd3, 0x3a, 0x3d, - 0x88, 0xaa, 0x4e, 0x0e, 0xe5, 0x59, 0x1f, 0xff, 0xd2, 0x41, 0x9f, 0x17, 0xc5, 0x9e, 0x52, 0xd0, - 0xf3, 0xe4, 0x20, 0x98, 0x36, 0xd2, 0x26, 0x5d, 0xae, 0x62, 0xc9, 0xf6, 0x69, 0xbe, 0x61, 0xb7, - 0x67, 0x26, 0x63, 0xc9, 0xca, 0xa4, 0xde, 0xb1, 0xd6, 0x99, 0xc9, 0x98, 0x0e, 0xa1, 0x73, 0x48, - 0xf2, 0xf4, 0x45, 0x54, 0x35, 0xd3, 0x15, 0xbf, 0xe6, 0xf4, 0x03, 0x18, 0xe2, 0xbb, 0xc8, 0xeb, - 0x8a, 0xb5, 0x47, 0xad, 0xc9, 0xe0, 0x91, 0x59, 0xaf, 0x17, 0xb4, 0xe4, 0xb7, 0x2d, 0x94, 0x02, - 0x6f, 0x3c, 0xfa, 0x09, 0x3a, 0x59, 0x52, 0xd5, 0xf1, 0xf1, 0x94, 0x33, 0x63, 0xa4, 0x4d, 0x7a, - 0x8f, 0x43, 0xeb, 0x7c, 0x86, 0x75, 0x39, 0xc3, 0x8a, 0x2e, 0x67, 0x70, 0x53, 0xba, 0xfc, 0x94, - 0xd3, 0x77, 0x60, 0xfc, 0x10, 0xe9, 0x76, 0x57, 0x33, 0x73, 0xa4, 0x4d, 0xda, 0xbc, 0xc9, 0xe8, - 0x1c, 0xee, 0x37, 0x22, 0x13, 0xb5, 0x88, 0xcb, 0x22, 0x4b, 0xbf, 0xa5, 0xa2, 0x62, 0x1d, 0xb5, - 0xc9, 0xc3, 0x7f, 0x36, 0x71, 0x95, 0xb9, 0x92, 0xe2, 0x4f, 0x3e, 0xd8, 0xfc, 0xcd, 0x52, 0x51, - 0x8d, 0x7f, 0x6b, 0xd0, 0x56, 0xab, 0xd2, 0x1e, 0x98, 0x6b, 0x7f, 0xe9, 0x07, 0x5f, 0x7c, 0x72, - 0x43, 0xef, 0xa1, 0xb7, 0xe2, 0x18, 0x2f, 0xfc, 0x30, 0xb2, 0x3d, 0x8f, 0x68, 0x94, 0x40, 0x7f, - 0x15, 0x84, 0xd1, 0x95, 0xdc, 0xd2, 0x01, 0x80, 0x54, 0x5c, 0xf4, 0x30, 0x42, 0xd2, 0x52, 0x2d, - 0xd2, 0x68, 0x80, 0x7e, 0x99, 0xb1, 0x5e, 0x7d, 0xe6, 0xb6, 0x8b, 0xa4, 0x7d, 0x9d, 0x71, 0x21, - 0x86, 0x22, 0x1c, 0x63, 0x1e, 0x78, 0xde, 0xcc, 0x76, 0x96, 0xc4, 0xa4, 0x6f, 0xe0, 0x4e, 0x39, - 0x57, 0xd4, 0xa1, 0x0c, 0xde, 0x72, 0xf4, 0xd0, 0x0e, 0x31, 0x8e, 0x30, 0x8c, 0xe2, 0x70, 0xed, - 0x38, 0x18, 0x86, 0xa4, 0xfb, 0x4f, 0xe5, 0xc9, 0x5e, 0x78, 0x6b, 0x8e, 0x04, 0xc6, 0x0e, 0xf4, - 0x5f, 0x9f, 0x4d, 0xef, 0xa0, 0xab, 0xda, 0xd0, 0x45, 0x97, 0xdc, 0x50, 0x00, 0x43, 0xba, 0xe8, - 0x12, 0x4d, 0x0e, 0x99, 0xe1, 0x53, 0xc0, 0x31, 0x9e, 0x07, 0xc1, 0x32, 0x76, 0x38, 0xda, 0xd1, - 0x22, 0xf0, 0xc9, 0xed, 0xac, 0xfb, 0xd5, 0x6c, 0x7e, 0xe4, 0xb3, 0xa1, 0x5e, 0xe9, 0xe3, 0x9f, - 0x00, 0x00, 0x00, 0xff, 0xff, 0x13, 0x64, 0x75, 0x6c, 0xa3, 0x02, 0x00, 0x00, -} diff --git a/pkg/proto/hapi/release/info.pb.go b/pkg/proto/hapi/release/info.pb.go deleted file mode 100644 index 7a7ccdd7467..00000000000 --- a/pkg/proto/hapi/release/info.pb.go +++ /dev/null @@ -1,90 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/release/info.proto - -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 - -// Info describes release information. -type Info struct { - Status *Status `protobuf:"bytes,1,opt,name=status" json:"status,omitempty"` - FirstDeployed *google_protobuf.Timestamp `protobuf:"bytes,2,opt,name=first_deployed,json=firstDeployed" json:"first_deployed,omitempty"` - LastDeployed *google_protobuf.Timestamp `protobuf:"bytes,3,opt,name=last_deployed,json=lastDeployed" json:"last_deployed,omitempty"` - // Deleted tracks when this object was deleted. - Deleted *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=deleted" json:"deleted,omitempty"` - // Description is human-friendly "log entry" about this release. - Description string `protobuf:"bytes,5,opt,name=Description" json:"Description,omitempty"` -} - -func (m *Info) Reset() { *m = Info{} } -func (m *Info) String() string { return proto.CompactTextString(m) } -func (*Info) ProtoMessage() {} -func (*Info) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} } - -func (m *Info) GetStatus() *Status { - if m != nil { - return m.Status - } - return nil -} - -func (m *Info) GetFirstDeployed() *google_protobuf.Timestamp { - if m != nil { - return m.FirstDeployed - } - return nil -} - -func (m *Info) GetLastDeployed() *google_protobuf.Timestamp { - if m != nil { - return m.LastDeployed - } - return nil -} - -func (m *Info) GetDeleted() *google_protobuf.Timestamp { - if m != nil { - return m.Deleted - } - return nil -} - -func (m *Info) GetDescription() string { - if m != nil { - return m.Description - } - return "" -} - -func init() { - proto.RegisterType((*Info)(nil), "hapi.release.Info") -} - -func init() { proto.RegisterFile("hapi/release/info.proto", fileDescriptor1) } - -var fileDescriptor1 = []byte{ - // 235 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x8f, 0x31, 0x4f, 0xc3, 0x30, - 0x10, 0x85, 0x95, 0x52, 0x5a, 0xd5, 0x6d, 0x19, 0x2c, 0x24, 0x42, 0x16, 0x22, 0xa6, 0x0e, 0xc8, - 0x91, 0x80, 0x1d, 0x81, 0xba, 0xb0, 0x06, 0x26, 0x16, 0xe4, 0xe2, 0x73, 0xb1, 0xe4, 0xe6, 0x2c, - 0xfb, 0x3a, 0xf0, 0x2f, 0xf8, 0xc9, 0xa8, 0xb6, 0x83, 0xd2, 0xa9, 0xab, 0xbf, 0xf7, 0x3e, 0xbf, - 0x63, 0x57, 0xdf, 0xd2, 0x99, 0xc6, 0x83, 0x05, 0x19, 0xa0, 0x31, 0x9d, 0x46, 0xe1, 0x3c, 0x12, - 0xf2, 0xc5, 0x01, 0x88, 0x0c, 0xaa, 0x9b, 0x2d, 0xe2, 0xd6, 0x42, 0x13, 0xd9, 0x66, 0xaf, 0x1b, - 0x32, 0x3b, 0x08, 0x24, 0x77, 0x2e, 0xc5, 0xab, 0xeb, 0x23, 0x4f, 0x20, 0x49, 0xfb, 0x90, 0xd0, - 0xed, 0xef, 0x88, 0x8d, 0x5f, 0x3b, 0x8d, 0xfc, 0x8e, 0x4d, 0x12, 0x28, 0x8b, 0xba, 0x58, 0xcd, - 0xef, 0x2f, 0xc5, 0xf0, 0x0f, 0xf1, 0x16, 0x59, 0x9b, 0x33, 0xfc, 0x99, 0x5d, 0x68, 0xe3, 0x03, - 0x7d, 0x2a, 0x70, 0x16, 0x7f, 0x40, 0x95, 0xa3, 0xd8, 0xaa, 0x44, 0xda, 0x22, 0xfa, 0x2d, 0xe2, - 0xbd, 0xdf, 0xd2, 0x2e, 0x63, 0x63, 0x9d, 0x0b, 0xfc, 0x89, 0x2d, 0xad, 0x1c, 0x1a, 0xce, 0x4e, - 0x1a, 0x16, 0x87, 0xc2, 0xbf, 0xe0, 0x91, 0x4d, 0x15, 0x58, 0x20, 0x50, 0xe5, 0xf8, 0x64, 0xb5, - 0x8f, 0xf2, 0x9a, 0xcd, 0xd7, 0x10, 0xbe, 0xbc, 0x71, 0x64, 0xb0, 0x2b, 0xcf, 0xeb, 0x62, 0x35, - 0x6b, 0x87, 0x4f, 0x2f, 0xb3, 0x8f, 0x69, 0xbe, 0x7a, 0x33, 0x89, 0xa6, 0x87, 0xbf, 0x00, 0x00, - 0x00, 0xff, 0xff, 0x1a, 0x52, 0x8f, 0x9c, 0x89, 0x01, 0x00, 0x00, -} diff --git a/pkg/proto/hapi/release/release.pb.go b/pkg/proto/hapi/release/release.pb.go deleted file mode 100644 index 511b543d790..00000000000 --- a/pkg/proto/hapi/release/release.pb.go +++ /dev/null @@ -1,124 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/release/release.proto - -package release - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" -import hapi_chart "k8s.io/helm/pkg/proto/hapi/chart" -import hapi_chart3 "k8s.io/helm/pkg/proto/hapi/chart" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// Release describes a deployment of a chart, together with the chart -// and the variables used to deploy that chart. -type Release struct { - // Name is the name of the release - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // Info provides information about a release - Info *Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` - // Chart is the chart that was released. - Chart *hapi_chart3.Chart `protobuf:"bytes,3,opt,name=chart" json:"chart,omitempty"` - // Config is the set of extra Values added to the chart. - // These values override the default values inside of the chart. - Config *hapi_chart.Config `protobuf:"bytes,4,opt,name=config" json:"config,omitempty"` - // Manifest is the string representation of the rendered template. - Manifest string `protobuf:"bytes,5,opt,name=manifest" json:"manifest,omitempty"` - // Hooks are all of the hooks declared for this release. - Hooks []*Hook `protobuf:"bytes,6,rep,name=hooks" json:"hooks,omitempty"` - // Version is an int32 which represents the version of the release. - Version int32 `protobuf:"varint,7,opt,name=version" json:"version,omitempty"` - // Namespace is the kubernetes namespace of the release. - Namespace string `protobuf:"bytes,8,opt,name=namespace" json:"namespace,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 fileDescriptor2, []int{0} } - -func (m *Release) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *Release) GetInfo() *Info { - if m != nil { - return m.Info - } - return nil -} - -func (m *Release) GetChart() *hapi_chart3.Chart { - if m != nil { - return m.Chart - } - return nil -} - -func (m *Release) GetConfig() *hapi_chart.Config { - if m != nil { - return m.Config - } - return nil -} - -func (m *Release) GetManifest() string { - if m != nil { - return m.Manifest - } - return "" -} - -func (m *Release) GetHooks() []*Hook { - if m != nil { - return m.Hooks - } - return nil -} - -func (m *Release) GetVersion() int32 { - if m != nil { - return m.Version - } - return 0 -} - -func (m *Release) GetNamespace() string { - if m != nil { - return m.Namespace - } - return "" -} - -func init() { - proto.RegisterType((*Release)(nil), "hapi.release.Release") -} - -func init() { proto.RegisterFile("hapi/release/release.proto", fileDescriptor2) } - -var fileDescriptor2 = []byte{ - // 256 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0xbf, 0x4e, 0xc3, 0x40, - 0x0c, 0xc6, 0x95, 0x36, 0x7f, 0x1a, 0xc3, 0x82, 0x07, 0xb0, 0x22, 0x86, 0x88, 0x01, 0x22, 0x86, - 0x54, 0x82, 0x37, 0x80, 0x05, 0xd6, 0x1b, 0xd9, 0x8e, 0xe8, 0x42, 0x4e, 0xa5, 0xe7, 0x28, 0x17, - 0xf1, 0x2c, 0x3c, 0x2e, 0xba, 0x3f, 0x85, 0x94, 0x2e, 0x4e, 0xec, 0xdf, 0xa7, 0xcf, 0xdf, 0x19, - 0xaa, 0x41, 0x8e, 0x7a, 0x3b, 0xa9, 0x4f, 0x25, 0xad, 0x3a, 0x7c, 0xdb, 0x71, 0xe2, 0x99, 0xf1, - 0xdc, 0xb1, 0x36, 0xce, 0xaa, 0xab, 0x23, 0xe5, 0xc0, 0xbc, 0x0b, 0xb2, 0x7f, 0x40, 0x9b, 0x9e, - 0x8f, 0x40, 0x37, 0xc8, 0x69, 0xde, 0x76, 0x6c, 0x7a, 0xfd, 0x11, 0xc1, 0xe5, 0x12, 0xb8, 0x1a, - 0xe6, 0x37, 0xdf, 0x2b, 0x28, 0x44, 0xf0, 0x41, 0x84, 0xd4, 0xc8, 0xbd, 0xa2, 0xa4, 0x4e, 0x9a, - 0x52, 0xf8, 0x7f, 0xbc, 0x85, 0xd4, 0xd9, 0xd3, 0xaa, 0x4e, 0x9a, 0xb3, 0x07, 0x6c, 0x97, 0xf9, - 0xda, 0x57, 0xd3, 0xb3, 0xf0, 0x1c, 0xef, 0x20, 0xf3, 0xb6, 0xb4, 0xf6, 0xc2, 0x8b, 0x20, 0x0c, - 0x9b, 0x9e, 0x5d, 0x15, 0x81, 0xe3, 0x3d, 0xe4, 0x21, 0x18, 0xa5, 0x4b, 0xcb, 0xa8, 0xf4, 0x44, - 0x44, 0x05, 0x56, 0xb0, 0xd9, 0x4b, 0xa3, 0x7b, 0x65, 0x67, 0xca, 0x7c, 0xa8, 0xdf, 0x1e, 0x1b, - 0xc8, 0xdc, 0x41, 0x2c, 0xe5, 0xf5, 0xfa, 0x34, 0xd9, 0x0b, 0xf3, 0x4e, 0x04, 0x01, 0x12, 0x14, - 0x5f, 0x6a, 0xb2, 0x9a, 0x0d, 0x15, 0x75, 0xd2, 0x64, 0xe2, 0xd0, 0xe2, 0x35, 0x94, 0xee, 0x91, - 0x76, 0x94, 0x9d, 0xa2, 0x8d, 0x5f, 0xf0, 0x37, 0x78, 0x2a, 0xdf, 0x8a, 0x68, 0xf7, 0x9e, 0xfb, - 0x63, 0x3d, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x8f, 0xec, 0x97, 0xbb, 0x01, 0x00, 0x00, -} diff --git a/pkg/proto/hapi/release/status.pb.go b/pkg/proto/hapi/release/status.pb.go deleted file mode 100644 index 284892642f9..00000000000 --- a/pkg/proto/hapi/release/status.pb.go +++ /dev/null @@ -1,141 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/release/status.proto - -package release - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" -import _ "github.com/golang/protobuf/ptypes/any" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -type Status_Code int32 - -const ( - // Status_UNKNOWN indicates that a release is in an uncertain state. - Status_UNKNOWN Status_Code = 0 - // Status_DEPLOYED indicates that the release has been pushed to Kubernetes. - Status_DEPLOYED Status_Code = 1 - // Status_DELETED indicates that a release has been deleted from Kubermetes. - Status_DELETED Status_Code = 2 - // Status_SUPERSEDED indicates that this release object is outdated and a newer one exists. - Status_SUPERSEDED Status_Code = 3 - // Status_FAILED indicates that the release was not successfully deployed. - Status_FAILED Status_Code = 4 - // Status_DELETING indicates that a delete operation is underway. - Status_DELETING Status_Code = 5 - // Status_PENDING_INSTALL indicates that an install operation is underway. - Status_PENDING_INSTALL Status_Code = 6 - // Status_PENDING_UPGRADE indicates that an upgrade operation is underway. - Status_PENDING_UPGRADE Status_Code = 7 - // Status_PENDING_ROLLBACK indicates that an rollback operation is underway. - Status_PENDING_ROLLBACK Status_Code = 8 -) - -var Status_Code_name = map[int32]string{ - 0: "UNKNOWN", - 1: "DEPLOYED", - 2: "DELETED", - 3: "SUPERSEDED", - 4: "FAILED", - 5: "DELETING", - 6: "PENDING_INSTALL", - 7: "PENDING_UPGRADE", - 8: "PENDING_ROLLBACK", -} -var Status_Code_value = map[string]int32{ - "UNKNOWN": 0, - "DEPLOYED": 1, - "DELETED": 2, - "SUPERSEDED": 3, - "FAILED": 4, - "DELETING": 5, - "PENDING_INSTALL": 6, - "PENDING_UPGRADE": 7, - "PENDING_ROLLBACK": 8, -} - -func (x Status_Code) String() string { - return proto.EnumName(Status_Code_name, int32(x)) -} -func (Status_Code) EnumDescriptor() ([]byte, []int) { return fileDescriptor3, []int{0, 0} } - -// Status defines the status of a release. -type Status struct { - Code Status_Code `protobuf:"varint,1,opt,name=code,enum=hapi.release.Status_Code" json:"code,omitempty"` - // Cluster resources as kubectl would print them. - Resources string `protobuf:"bytes,3,opt,name=resources" json:"resources,omitempty"` - // Contains the rendered templates/NOTES.txt if available - Notes string `protobuf:"bytes,4,opt,name=notes" json:"notes,omitempty"` - // LastTestSuiteRun provides results on the last test run on a release - LastTestSuiteRun *TestSuite `protobuf:"bytes,5,opt,name=last_test_suite_run,json=lastTestSuiteRun" json:"last_test_suite_run,omitempty"` -} - -func (m *Status) Reset() { *m = Status{} } -func (m *Status) String() string { return proto.CompactTextString(m) } -func (*Status) ProtoMessage() {} -func (*Status) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} } - -func (m *Status) GetCode() Status_Code { - if m != nil { - return m.Code - } - return Status_UNKNOWN -} - -func (m *Status) GetResources() string { - if m != nil { - return m.Resources - } - return "" -} - -func (m *Status) GetNotes() string { - if m != nil { - return m.Notes - } - return "" -} - -func (m *Status) GetLastTestSuiteRun() *TestSuite { - if m != nil { - return m.LastTestSuiteRun - } - return nil -} - -func init() { - proto.RegisterType((*Status)(nil), "hapi.release.Status") - proto.RegisterEnum("hapi.release.Status_Code", Status_Code_name, Status_Code_value) -} - -func init() { proto.RegisterFile("hapi/release/status.proto", fileDescriptor3) } - -var fileDescriptor3 = []byte{ - // 333 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x90, 0xd1, 0x6e, 0xa2, 0x40, - 0x14, 0x86, 0x17, 0x45, 0xd4, 0xa3, 0x71, 0x27, 0xa3, 0xc9, 0xa2, 0xd9, 0x4d, 0x8c, 0x57, 0xde, - 0x2c, 0x24, 0xf6, 0x09, 0xd0, 0x19, 0x0d, 0x71, 0x82, 0x04, 0x30, 0x4d, 0x7b, 0x43, 0x50, 0xa7, - 0xd6, 0xc4, 0x30, 0x86, 0x19, 0x2e, 0xfa, 0x26, 0x7d, 0xaa, 0x3e, 0x53, 0x03, 0xd8, 0xa8, 0x97, - 0xff, 0xff, 0x7d, 0x87, 0x73, 0x18, 0x18, 0xbe, 0x27, 0x97, 0x93, 0x9d, 0xf1, 0x33, 0x4f, 0x24, - 0xb7, 0xa5, 0x4a, 0x54, 0x2e, 0xad, 0x4b, 0x26, 0x94, 0xc0, 0xdd, 0x02, 0x59, 0x57, 0x34, 0xfa, - 0xf7, 0x20, 0x2a, 0x2e, 0x55, 0x2c, 0xf3, 0x93, 0xe2, 0x95, 0x3c, 0x1a, 0x1e, 0x85, 0x38, 0x9e, - 0xb9, 0x5d, 0xa6, 0x5d, 0xfe, 0x66, 0x27, 0xe9, 0x47, 0x85, 0x26, 0x5f, 0x35, 0x30, 0xc2, 0xf2, - 0xc3, 0xf8, 0x3f, 0xe8, 0x7b, 0x71, 0xe0, 0xa6, 0x36, 0xd6, 0xa6, 0xbd, 0xd9, 0xd0, 0xba, 0xdf, - 0x60, 0x55, 0x8e, 0xb5, 0x10, 0x07, 0x1e, 0x94, 0x1a, 0xfe, 0x0b, 0xed, 0x8c, 0x4b, 0x91, 0x67, - 0x7b, 0x2e, 0xcd, 0xfa, 0x58, 0x9b, 0xb6, 0x83, 0x5b, 0x81, 0x07, 0xd0, 0x48, 0x85, 0xe2, 0xd2, - 0xd4, 0x4b, 0x52, 0x05, 0xbc, 0x84, 0xfe, 0x39, 0x91, 0x2a, 0xbe, 0x5d, 0x18, 0x67, 0x79, 0x6a, - 0x36, 0xc6, 0xda, 0xb4, 0x33, 0xfb, 0xf3, 0xb8, 0x31, 0xe2, 0x52, 0x85, 0x85, 0x12, 0xa0, 0x62, - 0xe6, 0x16, 0xf3, 0x74, 0xf2, 0xa9, 0x81, 0x5e, 0x9c, 0x82, 0x3b, 0xd0, 0xdc, 0x7a, 0x6b, 0x6f, - 0xf3, 0xec, 0xa1, 0x5f, 0xb8, 0x0b, 0x2d, 0x42, 0x7d, 0xb6, 0x79, 0xa1, 0x04, 0x69, 0x05, 0x22, - 0x94, 0xd1, 0x88, 0x12, 0x54, 0xc3, 0x3d, 0x80, 0x70, 0xeb, 0xd3, 0x20, 0xa4, 0x84, 0x12, 0x54, - 0xc7, 0x00, 0xc6, 0xd2, 0x71, 0x19, 0x25, 0x48, 0xaf, 0xc6, 0x18, 0x8d, 0x5c, 0x6f, 0x85, 0x1a, - 0xb8, 0x0f, 0xbf, 0x7d, 0xea, 0x11, 0xd7, 0x5b, 0xc5, 0xae, 0x17, 0x46, 0x0e, 0x63, 0xc8, 0xb8, - 0x2f, 0xb7, 0xfe, 0x2a, 0x70, 0x08, 0x45, 0x4d, 0x3c, 0x00, 0xf4, 0x53, 0x06, 0x1b, 0xc6, 0xe6, - 0xce, 0x62, 0x8d, 0x5a, 0xf3, 0xf6, 0x6b, 0xf3, 0xfa, 0x07, 0x3b, 0xa3, 0x7c, 0xe2, 0xa7, 0xef, - 0x00, 0x00, 0x00, 0xff, 0xff, 0x09, 0x48, 0x18, 0xba, 0xc7, 0x01, 0x00, 0x00, -} diff --git a/pkg/proto/hapi/release/test_run.pb.go b/pkg/proto/hapi/release/test_run.pb.go deleted file mode 100644 index 4d39d17c2b8..00000000000 --- a/pkg/proto/hapi/release/test_run.pb.go +++ /dev/null @@ -1,118 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/release/test_run.proto - -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 - -type TestRun_Status int32 - -const ( - TestRun_UNKNOWN TestRun_Status = 0 - TestRun_SUCCESS TestRun_Status = 1 - TestRun_FAILURE TestRun_Status = 2 - TestRun_RUNNING TestRun_Status = 3 -) - -var TestRun_Status_name = map[int32]string{ - 0: "UNKNOWN", - 1: "SUCCESS", - 2: "FAILURE", - 3: "RUNNING", -} -var TestRun_Status_value = map[string]int32{ - "UNKNOWN": 0, - "SUCCESS": 1, - "FAILURE": 2, - "RUNNING": 3, -} - -func (x TestRun_Status) String() string { - return proto.EnumName(TestRun_Status_name, int32(x)) -} -func (TestRun_Status) EnumDescriptor() ([]byte, []int) { return fileDescriptor4, []int{0, 0} } - -type TestRun struct { - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - Status TestRun_Status `protobuf:"varint,2,opt,name=status,enum=hapi.release.TestRun_Status" json:"status,omitempty"` - Info string `protobuf:"bytes,3,opt,name=info" json:"info,omitempty"` - StartedAt *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=started_at,json=startedAt" json:"started_at,omitempty"` - CompletedAt *google_protobuf.Timestamp `protobuf:"bytes,5,opt,name=completed_at,json=completedAt" json:"completed_at,omitempty"` -} - -func (m *TestRun) Reset() { *m = TestRun{} } -func (m *TestRun) String() string { return proto.CompactTextString(m) } -func (*TestRun) ProtoMessage() {} -func (*TestRun) Descriptor() ([]byte, []int) { return fileDescriptor4, []int{0} } - -func (m *TestRun) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *TestRun) GetStatus() TestRun_Status { - if m != nil { - return m.Status - } - return TestRun_UNKNOWN -} - -func (m *TestRun) GetInfo() string { - if m != nil { - return m.Info - } - return "" -} - -func (m *TestRun) GetStartedAt() *google_protobuf.Timestamp { - if m != nil { - return m.StartedAt - } - return nil -} - -func (m *TestRun) GetCompletedAt() *google_protobuf.Timestamp { - if m != nil { - return m.CompletedAt - } - return nil -} - -func init() { - proto.RegisterType((*TestRun)(nil), "hapi.release.TestRun") - proto.RegisterEnum("hapi.release.TestRun_Status", TestRun_Status_name, TestRun_Status_value) -} - -func init() { proto.RegisterFile("hapi/release/test_run.proto", fileDescriptor4) } - -var fileDescriptor4 = []byte{ - // 274 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x8f, 0xc1, 0x4b, 0xfb, 0x30, - 0x1c, 0xc5, 0x7f, 0xe9, 0xf6, 0x6b, 0x69, 0x3a, 0xa4, 0xe4, 0x54, 0xa6, 0x60, 0xd9, 0xa9, 0xa7, - 0x14, 0xa6, 0x17, 0x41, 0x0f, 0x75, 0x4c, 0x19, 0x4a, 0x84, 0x74, 0x45, 0xf0, 0x32, 0x32, 0xcd, - 0x66, 0xa1, 0x6d, 0x4a, 0xf3, 0xed, 0xdf, 0xe3, 0xbf, 0x2a, 0x69, 0x33, 0xf1, 0xe6, 0xed, 0xfb, - 0x78, 0x9f, 0xf7, 0xf2, 0x82, 0xcf, 0x3f, 0x45, 0x5b, 0xa6, 0x9d, 0xac, 0xa4, 0xd0, 0x32, 0x05, - 0xa9, 0x61, 0xd7, 0xf5, 0x0d, 0x6d, 0x3b, 0x05, 0x8a, 0xcc, 0x8c, 0x49, 0xad, 0x39, 0xbf, 0x3c, - 0x2a, 0x75, 0xac, 0x64, 0x3a, 0x78, 0xfb, 0xfe, 0x90, 0x42, 0x59, 0x4b, 0x0d, 0xa2, 0x6e, 0x47, - 0x7c, 0xf1, 0xe5, 0x60, 0x6f, 0x2b, 0x35, 0xf0, 0xbe, 0x21, 0x04, 0x4f, 0x1b, 0x51, 0xcb, 0x08, - 0xc5, 0x28, 0xf1, 0xf9, 0x70, 0x93, 0x6b, 0xec, 0x6a, 0x10, 0xd0, 0xeb, 0xc8, 0x89, 0x51, 0x72, - 0xb6, 0xbc, 0xa0, 0xbf, 0xfb, 0xa9, 0x8d, 0xd2, 0x7c, 0x60, 0xb8, 0x65, 0x4d, 0x53, 0xd9, 0x1c, - 0x54, 0x34, 0x19, 0x9b, 0xcc, 0x4d, 0x6e, 0x30, 0xd6, 0x20, 0x3a, 0x90, 0x1f, 0x3b, 0x01, 0xd1, - 0x34, 0x46, 0x49, 0xb0, 0x9c, 0xd3, 0x71, 0x1f, 0x3d, 0xed, 0xa3, 0xdb, 0xd3, 0x3e, 0xee, 0x5b, - 0x3a, 0x03, 0x72, 0x87, 0x67, 0xef, 0xaa, 0x6e, 0x2b, 0x69, 0xc3, 0xff, 0xff, 0x0c, 0x07, 0x3f, - 0x7c, 0x06, 0x8b, 0x5b, 0xec, 0x8e, 0xfb, 0x48, 0x80, 0xbd, 0x82, 0x3d, 0xb1, 0x97, 0x57, 0x16, - 0xfe, 0x33, 0x22, 0x2f, 0x56, 0xab, 0x75, 0x9e, 0x87, 0xc8, 0x88, 0x87, 0x6c, 0xf3, 0x5c, 0xf0, - 0x75, 0xe8, 0x18, 0xc1, 0x0b, 0xc6, 0x36, 0xec, 0x31, 0x9c, 0xdc, 0xfb, 0x6f, 0x9e, 0xfd, 0xed, - 0xde, 0x1d, 0x5e, 0xba, 0xfa, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x31, 0x86, 0x46, 0xdb, 0x81, 0x01, - 0x00, 0x00, -} diff --git a/pkg/proto/hapi/release/test_suite.pb.go b/pkg/proto/hapi/release/test_suite.pb.go deleted file mode 100644 index b7fa261476d..00000000000 --- a/pkg/proto/hapi/release/test_suite.pb.go +++ /dev/null @@ -1,73 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/release/test_suite.proto - -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 - -// TestSuite comprises of the last run of the pre-defined test suite of a release version -type TestSuite struct { - // StartedAt indicates the date/time this test suite was kicked off - StartedAt *google_protobuf.Timestamp `protobuf:"bytes,1,opt,name=started_at,json=startedAt" json:"started_at,omitempty"` - // CompletedAt indicates the date/time this test suite was completed - CompletedAt *google_protobuf.Timestamp `protobuf:"bytes,2,opt,name=completed_at,json=completedAt" json:"completed_at,omitempty"` - // Results are the results of each segment of the test - Results []*TestRun `protobuf:"bytes,3,rep,name=results" json:"results,omitempty"` -} - -func (m *TestSuite) Reset() { *m = TestSuite{} } -func (m *TestSuite) String() string { return proto.CompactTextString(m) } -func (*TestSuite) ProtoMessage() {} -func (*TestSuite) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{0} } - -func (m *TestSuite) GetStartedAt() *google_protobuf.Timestamp { - if m != nil { - return m.StartedAt - } - return nil -} - -func (m *TestSuite) GetCompletedAt() *google_protobuf.Timestamp { - if m != nil { - return m.CompletedAt - } - return nil -} - -func (m *TestSuite) GetResults() []*TestRun { - if m != nil { - return m.Results - } - return nil -} - -func init() { - proto.RegisterType((*TestSuite)(nil), "hapi.release.TestSuite") -} - -func init() { proto.RegisterFile("hapi/release/test_suite.proto", fileDescriptor5) } - -var fileDescriptor5 = []byte{ - // 207 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x8f, 0xc1, 0x4a, 0x86, 0x40, - 0x14, 0x85, 0x31, 0x21, 0x71, 0x74, 0x35, 0x10, 0x88, 0x11, 0x49, 0x2b, 0x57, 0x33, 0x60, 0xab, - 0x16, 0x2d, 0xec, 0x11, 0xcc, 0x55, 0x1b, 0x19, 0xeb, 0x66, 0xc2, 0xe8, 0x0c, 0x73, 0xef, 0xbc, - 0x5a, 0xcf, 0x17, 0xea, 0x18, 0x41, 0x8b, 0x7f, 0xfd, 0x7d, 0xe7, 0x9c, 0x7b, 0xd9, 0xdd, 0x97, - 0xb2, 0xb3, 0x74, 0xa0, 0x41, 0x21, 0x48, 0x02, 0xa4, 0x01, 0xfd, 0x4c, 0x20, 0xac, 0x33, 0x64, - 0x78, 0xbe, 0x61, 0x11, 0x70, 0x79, 0x3f, 0x19, 0x33, 0x69, 0x90, 0x3b, 0x1b, 0xfd, 0xa7, 0xa4, - 0x79, 0x01, 0x24, 0xb5, 0xd8, 0x43, 0x2f, 0x6f, 0xff, 0xb7, 0x39, 0xbf, 0x1e, 0xf0, 0xe1, 0x3b, - 0x62, 0x69, 0x0f, 0x48, 0xaf, 0x5b, 0x3f, 0x7f, 0x62, 0x0c, 0x49, 0x39, 0x82, 0x8f, 0x41, 0x51, - 0x11, 0x55, 0x51, 0x9d, 0x35, 0xa5, 0x38, 0x06, 0xc4, 0x39, 0x20, 0xfa, 0x73, 0xa0, 0x4b, 0x83, - 0xdd, 0x12, 0x7f, 0x66, 0xf9, 0xbb, 0x59, 0xac, 0x86, 0x10, 0xbe, 0xba, 0x18, 0xce, 0x7e, 0xfd, - 0x96, 0xb8, 0x64, 0x89, 0x03, 0xf4, 0x9a, 0xb0, 0x88, 0xab, 0xb8, 0xce, 0x9a, 0x1b, 0xf1, 0xf7, - 0x4b, 0xb1, 0xdd, 0xd8, 0xf9, 0xb5, 0x3b, 0xad, 0x97, 0xf4, 0x2d, 0x09, 0x6c, 0xbc, 0xde, 0xcb, - 0x1f, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x8c, 0x59, 0x65, 0x4f, 0x37, 0x01, 0x00, 0x00, -} diff --git a/pkg/proto/hapi/rudder/rudder.pb.go b/pkg/proto/hapi/rudder/rudder.pb.go deleted file mode 100644 index 6e26d71eb0f..00000000000 --- a/pkg/proto/hapi/rudder/rudder.pb.go +++ /dev/null @@ -1,722 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/rudder/rudder.proto - -/* -Package rudder is a generated protocol buffer package. - -It is generated from these files: - hapi/rudder/rudder.proto - -It has these top-level messages: - Result - VersionReleaseRequest - VersionReleaseResponse - InstallReleaseRequest - InstallReleaseResponse - DeleteReleaseRequest - DeleteReleaseResponse - UpgradeReleaseRequest - UpgradeReleaseResponse - RollbackReleaseRequest - RollbackReleaseResponse - ReleaseStatusRequest - ReleaseStatusResponse -*/ -package rudder - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" -import hapi_release3 "k8s.io/helm/pkg/proto/hapi/release" -import hapi_release5 "k8s.io/helm/pkg/proto/hapi/release" - -import ( - context "golang.org/x/net/context" - grpc "google.golang.org/grpc" -) - -// 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. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -type Result_Status int32 - -const ( - // No status set - Result_UNKNOWN Result_Status = 0 - // Operation was successful - Result_SUCCESS Result_Status = 1 - // Operation had no results (e.g. upgrade identical, rollback to same, delete non-existent) - Result_UNCHANGED Result_Status = 2 - // Operation failed - Result_ERROR Result_Status = 3 -) - -var Result_Status_name = map[int32]string{ - 0: "UNKNOWN", - 1: "SUCCESS", - 2: "UNCHANGED", - 3: "ERROR", -} -var Result_Status_value = map[string]int32{ - "UNKNOWN": 0, - "SUCCESS": 1, - "UNCHANGED": 2, - "ERROR": 3, -} - -func (x Result_Status) String() string { - return proto.EnumName(Result_Status_name, int32(x)) -} -func (Result_Status) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0, 0} } - -type Result struct { - Info string `protobuf:"bytes,1,opt,name=info" json:"info,omitempty"` - Log []string `protobuf:"bytes,2,rep,name=log" json:"log,omitempty"` -} - -func (m *Result) Reset() { *m = Result{} } -func (m *Result) String() string { return proto.CompactTextString(m) } -func (*Result) ProtoMessage() {} -func (*Result) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } - -func (m *Result) GetInfo() string { - if m != nil { - return m.Info - } - return "" -} - -func (m *Result) GetLog() []string { - if m != nil { - return m.Log - } - return nil -} - -type VersionReleaseRequest struct { -} - -func (m *VersionReleaseRequest) Reset() { *m = VersionReleaseRequest{} } -func (m *VersionReleaseRequest) String() string { return proto.CompactTextString(m) } -func (*VersionReleaseRequest) ProtoMessage() {} -func (*VersionReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } - -type VersionReleaseResponse struct { - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - Version string `protobuf:"bytes,2,opt,name=version" json:"version,omitempty"` -} - -func (m *VersionReleaseResponse) Reset() { *m = VersionReleaseResponse{} } -func (m *VersionReleaseResponse) String() string { return proto.CompactTextString(m) } -func (*VersionReleaseResponse) ProtoMessage() {} -func (*VersionReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } - -func (m *VersionReleaseResponse) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *VersionReleaseResponse) GetVersion() string { - if m != nil { - return m.Version - } - return "" -} - -type InstallReleaseRequest struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` -} - -func (m *InstallReleaseRequest) Reset() { *m = InstallReleaseRequest{} } -func (m *InstallReleaseRequest) String() string { return proto.CompactTextString(m) } -func (*InstallReleaseRequest) ProtoMessage() {} -func (*InstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } - -func (m *InstallReleaseRequest) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -type InstallReleaseResponse struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` - Result *Result `protobuf:"bytes,2,opt,name=result" json:"result,omitempty"` -} - -func (m *InstallReleaseResponse) Reset() { *m = InstallReleaseResponse{} } -func (m *InstallReleaseResponse) String() string { return proto.CompactTextString(m) } -func (*InstallReleaseResponse) ProtoMessage() {} -func (*InstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } - -func (m *InstallReleaseResponse) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -func (m *InstallReleaseResponse) GetResult() *Result { - if m != nil { - return m.Result - } - return nil -} - -type DeleteReleaseRequest struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` -} - -func (m *DeleteReleaseRequest) Reset() { *m = DeleteReleaseRequest{} } -func (m *DeleteReleaseRequest) String() string { return proto.CompactTextString(m) } -func (*DeleteReleaseRequest) ProtoMessage() {} -func (*DeleteReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } - -func (m *DeleteReleaseRequest) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -type DeleteReleaseResponse struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` - Result *Result `protobuf:"bytes,2,opt,name=result" json:"result,omitempty"` -} - -func (m *DeleteReleaseResponse) Reset() { *m = DeleteReleaseResponse{} } -func (m *DeleteReleaseResponse) String() string { return proto.CompactTextString(m) } -func (*DeleteReleaseResponse) ProtoMessage() {} -func (*DeleteReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } - -func (m *DeleteReleaseResponse) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -func (m *DeleteReleaseResponse) GetResult() *Result { - if m != nil { - return m.Result - } - return nil -} - -type UpgradeReleaseRequest struct { - Current *hapi_release5.Release `protobuf:"bytes,1,opt,name=current" json:"current,omitempty"` - Target *hapi_release5.Release `protobuf:"bytes,2,opt,name=target" json:"target,omitempty"` - Timeout int64 `protobuf:"varint,3,opt,name=Timeout" json:"Timeout,omitempty"` - Wait bool `protobuf:"varint,4,opt,name=Wait" json:"Wait,omitempty"` - Recreate bool `protobuf:"varint,5,opt,name=Recreate" json:"Recreate,omitempty"` - Force bool `protobuf:"varint,6,opt,name=Force" json:"Force,omitempty"` -} - -func (m *UpgradeReleaseRequest) Reset() { *m = UpgradeReleaseRequest{} } -func (m *UpgradeReleaseRequest) String() string { return proto.CompactTextString(m) } -func (*UpgradeReleaseRequest) ProtoMessage() {} -func (*UpgradeReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } - -func (m *UpgradeReleaseRequest) GetCurrent() *hapi_release5.Release { - if m != nil { - return m.Current - } - return nil -} - -func (m *UpgradeReleaseRequest) GetTarget() *hapi_release5.Release { - if m != nil { - return m.Target - } - return nil -} - -func (m *UpgradeReleaseRequest) GetTimeout() int64 { - if m != nil { - return m.Timeout - } - return 0 -} - -func (m *UpgradeReleaseRequest) GetWait() bool { - if m != nil { - return m.Wait - } - return false -} - -func (m *UpgradeReleaseRequest) GetRecreate() bool { - if m != nil { - return m.Recreate - } - return false -} - -func (m *UpgradeReleaseRequest) GetForce() bool { - if m != nil { - return m.Force - } - return false -} - -type UpgradeReleaseResponse struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` - Result *Result `protobuf:"bytes,2,opt,name=result" json:"result,omitempty"` -} - -func (m *UpgradeReleaseResponse) Reset() { *m = UpgradeReleaseResponse{} } -func (m *UpgradeReleaseResponse) String() string { return proto.CompactTextString(m) } -func (*UpgradeReleaseResponse) ProtoMessage() {} -func (*UpgradeReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} } - -func (m *UpgradeReleaseResponse) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -func (m *UpgradeReleaseResponse) GetResult() *Result { - if m != nil { - return m.Result - } - return nil -} - -type RollbackReleaseRequest struct { - Current *hapi_release5.Release `protobuf:"bytes,1,opt,name=current" json:"current,omitempty"` - Target *hapi_release5.Release `protobuf:"bytes,2,opt,name=target" json:"target,omitempty"` - Timeout int64 `protobuf:"varint,3,opt,name=Timeout" json:"Timeout,omitempty"` - Wait bool `protobuf:"varint,4,opt,name=Wait" json:"Wait,omitempty"` - Recreate bool `protobuf:"varint,5,opt,name=Recreate" json:"Recreate,omitempty"` - Force bool `protobuf:"varint,6,opt,name=Force" json:"Force,omitempty"` -} - -func (m *RollbackReleaseRequest) Reset() { *m = RollbackReleaseRequest{} } -func (m *RollbackReleaseRequest) String() string { return proto.CompactTextString(m) } -func (*RollbackReleaseRequest) ProtoMessage() {} -func (*RollbackReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} } - -func (m *RollbackReleaseRequest) GetCurrent() *hapi_release5.Release { - if m != nil { - return m.Current - } - return nil -} - -func (m *RollbackReleaseRequest) GetTarget() *hapi_release5.Release { - if m != nil { - return m.Target - } - return nil -} - -func (m *RollbackReleaseRequest) GetTimeout() int64 { - if m != nil { - return m.Timeout - } - return 0 -} - -func (m *RollbackReleaseRequest) GetWait() bool { - if m != nil { - return m.Wait - } - return false -} - -func (m *RollbackReleaseRequest) GetRecreate() bool { - if m != nil { - return m.Recreate - } - return false -} - -func (m *RollbackReleaseRequest) GetForce() bool { - if m != nil { - return m.Force - } - return false -} - -type RollbackReleaseResponse struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` - Result *Result `protobuf:"bytes,2,opt,name=result" json:"result,omitempty"` -} - -func (m *RollbackReleaseResponse) Reset() { *m = RollbackReleaseResponse{} } -func (m *RollbackReleaseResponse) String() string { return proto.CompactTextString(m) } -func (*RollbackReleaseResponse) ProtoMessage() {} -func (*RollbackReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} } - -func (m *RollbackReleaseResponse) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -func (m *RollbackReleaseResponse) GetResult() *Result { - if m != nil { - return m.Result - } - return nil -} - -type ReleaseStatusRequest struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` -} - -func (m *ReleaseStatusRequest) Reset() { *m = ReleaseStatusRequest{} } -func (m *ReleaseStatusRequest) String() string { return proto.CompactTextString(m) } -func (*ReleaseStatusRequest) ProtoMessage() {} -func (*ReleaseStatusRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} } - -func (m *ReleaseStatusRequest) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -type ReleaseStatusResponse struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` - Info *hapi_release3.Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` -} - -func (m *ReleaseStatusResponse) Reset() { *m = ReleaseStatusResponse{} } -func (m *ReleaseStatusResponse) String() string { return proto.CompactTextString(m) } -func (*ReleaseStatusResponse) ProtoMessage() {} -func (*ReleaseStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} } - -func (m *ReleaseStatusResponse) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -func (m *ReleaseStatusResponse) GetInfo() *hapi_release3.Info { - if m != nil { - return m.Info - } - return nil -} - -func init() { - proto.RegisterType((*Result)(nil), "hapi.services.rudder.Result") - proto.RegisterType((*VersionReleaseRequest)(nil), "hapi.services.rudder.VersionReleaseRequest") - proto.RegisterType((*VersionReleaseResponse)(nil), "hapi.services.rudder.VersionReleaseResponse") - proto.RegisterType((*InstallReleaseRequest)(nil), "hapi.services.rudder.InstallReleaseRequest") - proto.RegisterType((*InstallReleaseResponse)(nil), "hapi.services.rudder.InstallReleaseResponse") - proto.RegisterType((*DeleteReleaseRequest)(nil), "hapi.services.rudder.DeleteReleaseRequest") - proto.RegisterType((*DeleteReleaseResponse)(nil), "hapi.services.rudder.DeleteReleaseResponse") - proto.RegisterType((*UpgradeReleaseRequest)(nil), "hapi.services.rudder.UpgradeReleaseRequest") - proto.RegisterType((*UpgradeReleaseResponse)(nil), "hapi.services.rudder.UpgradeReleaseResponse") - proto.RegisterType((*RollbackReleaseRequest)(nil), "hapi.services.rudder.RollbackReleaseRequest") - proto.RegisterType((*RollbackReleaseResponse)(nil), "hapi.services.rudder.RollbackReleaseResponse") - proto.RegisterType((*ReleaseStatusRequest)(nil), "hapi.services.rudder.ReleaseStatusRequest") - proto.RegisterType((*ReleaseStatusResponse)(nil), "hapi.services.rudder.ReleaseStatusResponse") - proto.RegisterEnum("hapi.services.rudder.Result_Status", Result_Status_name, Result_Status_value) -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// Client API for ReleaseModuleService service - -type ReleaseModuleServiceClient interface { - Version(ctx context.Context, in *VersionReleaseRequest, opts ...grpc.CallOption) (*VersionReleaseResponse, error) - // InstallRelease requests installation of a chart as a new release. - InstallRelease(ctx context.Context, in *InstallReleaseRequest, opts ...grpc.CallOption) (*InstallReleaseResponse, error) - // DeleteRelease requests deletion of a named release. - DeleteRelease(ctx context.Context, in *DeleteReleaseRequest, opts ...grpc.CallOption) (*DeleteReleaseResponse, error) - // RollbackRelease rolls back a release to a previous version. - RollbackRelease(ctx context.Context, in *RollbackReleaseRequest, opts ...grpc.CallOption) (*RollbackReleaseResponse, error) - // UpgradeRelease updates release content. - UpgradeRelease(ctx context.Context, in *UpgradeReleaseRequest, opts ...grpc.CallOption) (*UpgradeReleaseResponse, error) - // ReleaseStatus retrieves release status. - ReleaseStatus(ctx context.Context, in *ReleaseStatusRequest, opts ...grpc.CallOption) (*ReleaseStatusResponse, error) -} - -type releaseModuleServiceClient struct { - cc *grpc.ClientConn -} - -func NewReleaseModuleServiceClient(cc *grpc.ClientConn) ReleaseModuleServiceClient { - return &releaseModuleServiceClient{cc} -} - -func (c *releaseModuleServiceClient) Version(ctx context.Context, in *VersionReleaseRequest, opts ...grpc.CallOption) (*VersionReleaseResponse, error) { - out := new(VersionReleaseResponse) - err := grpc.Invoke(ctx, "/hapi.services.rudder.ReleaseModuleService/Version", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseModuleServiceClient) InstallRelease(ctx context.Context, in *InstallReleaseRequest, opts ...grpc.CallOption) (*InstallReleaseResponse, error) { - out := new(InstallReleaseResponse) - err := grpc.Invoke(ctx, "/hapi.services.rudder.ReleaseModuleService/InstallRelease", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseModuleServiceClient) DeleteRelease(ctx context.Context, in *DeleteReleaseRequest, opts ...grpc.CallOption) (*DeleteReleaseResponse, error) { - out := new(DeleteReleaseResponse) - err := grpc.Invoke(ctx, "/hapi.services.rudder.ReleaseModuleService/DeleteRelease", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseModuleServiceClient) RollbackRelease(ctx context.Context, in *RollbackReleaseRequest, opts ...grpc.CallOption) (*RollbackReleaseResponse, error) { - out := new(RollbackReleaseResponse) - err := grpc.Invoke(ctx, "/hapi.services.rudder.ReleaseModuleService/RollbackRelease", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseModuleServiceClient) UpgradeRelease(ctx context.Context, in *UpgradeReleaseRequest, opts ...grpc.CallOption) (*UpgradeReleaseResponse, error) { - out := new(UpgradeReleaseResponse) - err := grpc.Invoke(ctx, "/hapi.services.rudder.ReleaseModuleService/UpgradeRelease", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseModuleServiceClient) ReleaseStatus(ctx context.Context, in *ReleaseStatusRequest, opts ...grpc.CallOption) (*ReleaseStatusResponse, error) { - out := new(ReleaseStatusResponse) - err := grpc.Invoke(ctx, "/hapi.services.rudder.ReleaseModuleService/ReleaseStatus", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// Server API for ReleaseModuleService service - -type ReleaseModuleServiceServer interface { - Version(context.Context, *VersionReleaseRequest) (*VersionReleaseResponse, error) - // InstallRelease requests installation of a chart as a new release. - InstallRelease(context.Context, *InstallReleaseRequest) (*InstallReleaseResponse, error) - // DeleteRelease requests deletion of a named release. - DeleteRelease(context.Context, *DeleteReleaseRequest) (*DeleteReleaseResponse, error) - // RollbackRelease rolls back a release to a previous version. - RollbackRelease(context.Context, *RollbackReleaseRequest) (*RollbackReleaseResponse, error) - // UpgradeRelease updates release content. - UpgradeRelease(context.Context, *UpgradeReleaseRequest) (*UpgradeReleaseResponse, error) - // ReleaseStatus retrieves release status. - ReleaseStatus(context.Context, *ReleaseStatusRequest) (*ReleaseStatusResponse, error) -} - -func RegisterReleaseModuleServiceServer(s *grpc.Server, srv ReleaseModuleServiceServer) { - s.RegisterService(&_ReleaseModuleService_serviceDesc, srv) -} - -func _ReleaseModuleService_Version_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(VersionReleaseRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseModuleServiceServer).Version(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.rudder.ReleaseModuleService/Version", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseModuleServiceServer).Version(ctx, req.(*VersionReleaseRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseModuleService_InstallRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(InstallReleaseRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseModuleServiceServer).InstallRelease(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.rudder.ReleaseModuleService/InstallRelease", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseModuleServiceServer).InstallRelease(ctx, req.(*InstallReleaseRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseModuleService_DeleteRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(DeleteReleaseRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseModuleServiceServer).DeleteRelease(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.rudder.ReleaseModuleService/DeleteRelease", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseModuleServiceServer).DeleteRelease(ctx, req.(*DeleteReleaseRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseModuleService_RollbackRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RollbackReleaseRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseModuleServiceServer).RollbackRelease(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.rudder.ReleaseModuleService/RollbackRelease", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseModuleServiceServer).RollbackRelease(ctx, req.(*RollbackReleaseRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseModuleService_UpgradeRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UpgradeReleaseRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseModuleServiceServer).UpgradeRelease(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.rudder.ReleaseModuleService/UpgradeRelease", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseModuleServiceServer).UpgradeRelease(ctx, req.(*UpgradeReleaseRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseModuleService_ReleaseStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(ReleaseStatusRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseModuleServiceServer).ReleaseStatus(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.rudder.ReleaseModuleService/ReleaseStatus", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseModuleServiceServer).ReleaseStatus(ctx, req.(*ReleaseStatusRequest)) - } - return interceptor(ctx, in, info, handler) -} - -var _ReleaseModuleService_serviceDesc = grpc.ServiceDesc{ - ServiceName: "hapi.services.rudder.ReleaseModuleService", - HandlerType: (*ReleaseModuleServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Version", - Handler: _ReleaseModuleService_Version_Handler, - }, - { - MethodName: "InstallRelease", - Handler: _ReleaseModuleService_InstallRelease_Handler, - }, - { - MethodName: "DeleteRelease", - Handler: _ReleaseModuleService_DeleteRelease_Handler, - }, - { - MethodName: "RollbackRelease", - Handler: _ReleaseModuleService_RollbackRelease_Handler, - }, - { - MethodName: "UpgradeRelease", - Handler: _ReleaseModuleService_UpgradeRelease_Handler, - }, - { - MethodName: "ReleaseStatus", - Handler: _ReleaseModuleService_ReleaseStatus_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "hapi/rudder/rudder.proto", -} - -func init() { proto.RegisterFile("hapi/rudder/rudder.proto", fileDescriptor0) } - -var fileDescriptor0 = []byte{ - // 597 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x56, 0x5f, 0x8f, 0xd2, 0x4e, - 0x14, 0xa5, 0xb0, 0x14, 0xb8, 0x64, 0x7f, 0x3f, 0x32, 0xa1, 0xd0, 0x34, 0x3e, 0x90, 0x3e, 0x18, - 0xe2, 0xba, 0x25, 0x41, 0x1f, 0x7d, 0x51, 0x96, 0xfd, 0x13, 0x23, 0x9b, 0x0c, 0xe2, 0x26, 0xbe, - 0x75, 0xe1, 0x82, 0xd5, 0xd2, 0xd6, 0xe9, 0x74, 0x1f, 0xd5, 0x4f, 0xe3, 0x57, 0xd2, 0x8f, 0x63, - 0xda, 0x69, 0x89, 0xad, 0xd3, 0x88, 0x6b, 0xc2, 0x83, 0x4f, 0x9d, 0xe9, 0x3d, 0xdc, 0x39, 0xe7, - 0xf4, 0xce, 0x09, 0xa0, 0xbf, 0xb3, 0x03, 0x67, 0xc4, 0xa2, 0xd5, 0x0a, 0x59, 0xfa, 0xb0, 0x02, - 0xe6, 0x73, 0x9f, 0x74, 0xe3, 0x8a, 0x15, 0x22, 0xbb, 0x73, 0x96, 0x18, 0x5a, 0xa2, 0x66, 0xf4, - 0x05, 0x1e, 0x5d, 0xb4, 0x43, 0x1c, 0x39, 0xde, 0xda, 0x17, 0x70, 0xc3, 0xc8, 0x15, 0xd2, 0xa7, - 0xa8, 0x99, 0x2e, 0xa8, 0x14, 0xc3, 0xc8, 0xe5, 0x84, 0xc0, 0x51, 0xfc, 0x1b, 0x5d, 0x19, 0x28, - 0xc3, 0x16, 0x4d, 0xd6, 0xa4, 0x03, 0x35, 0xd7, 0xdf, 0xe8, 0xd5, 0x41, 0x6d, 0xd8, 0xa2, 0xf1, - 0xd2, 0x7c, 0x06, 0xea, 0x9c, 0xdb, 0x3c, 0x0a, 0x49, 0x1b, 0x1a, 0x8b, 0xd9, 0xcb, 0xd9, 0xf5, - 0xcd, 0xac, 0x53, 0x89, 0x37, 0xf3, 0xc5, 0x64, 0x32, 0x9d, 0xcf, 0x3b, 0x0a, 0x39, 0x86, 0xd6, - 0x62, 0x36, 0xb9, 0x7c, 0x3e, 0xbb, 0x98, 0x9e, 0x75, 0xaa, 0xa4, 0x05, 0xf5, 0x29, 0xa5, 0xd7, - 0xb4, 0x53, 0x33, 0xfb, 0xa0, 0xbd, 0x41, 0x16, 0x3a, 0xbe, 0x47, 0x05, 0x0b, 0x8a, 0x1f, 0x23, - 0x0c, 0xb9, 0x79, 0x0e, 0xbd, 0x62, 0x21, 0x0c, 0x7c, 0x2f, 0xc4, 0x98, 0x96, 0x67, 0x6f, 0x31, - 0xa3, 0x15, 0xaf, 0x89, 0x0e, 0x8d, 0x3b, 0x81, 0xd6, 0xab, 0xc9, 0xeb, 0x6c, 0x6b, 0x5e, 0x82, - 0x76, 0xe5, 0x85, 0xdc, 0x76, 0xdd, 0xfc, 0x01, 0x64, 0x04, 0x8d, 0x54, 0x78, 0xd2, 0xa9, 0x3d, - 0xd6, 0xac, 0xc4, 0xc4, 0xcc, 0x8d, 0x0c, 0x9e, 0xa1, 0xcc, 0xcf, 0xd0, 0x2b, 0x76, 0x4a, 0x19, - 0xfd, 0x69, 0x2b, 0xf2, 0x14, 0x54, 0x96, 0x78, 0x9c, 0xb0, 0x6d, 0x8f, 0x1f, 0x58, 0xb2, 0xef, - 0x67, 0x89, 0xef, 0x40, 0x53, 0xac, 0x79, 0x01, 0xdd, 0x33, 0x74, 0x91, 0xe3, 0xdf, 0x2a, 0xf9, - 0x04, 0x5a, 0xa1, 0xd1, 0x61, 0x85, 0x7c, 0x53, 0x40, 0x5b, 0x04, 0x1b, 0x66, 0xaf, 0x24, 0x52, - 0x96, 0x11, 0x63, 0xe8, 0xf1, 0xdf, 0x10, 0x48, 0x51, 0xe4, 0x14, 0x54, 0x6e, 0xb3, 0x0d, 0x66, - 0x04, 0x4a, 0xf0, 0x29, 0x28, 0x9e, 0x93, 0xd7, 0xce, 0x16, 0xfd, 0x88, 0xeb, 0xb5, 0x81, 0x32, - 0xac, 0xd1, 0x6c, 0x1b, 0x4f, 0xd5, 0x8d, 0xed, 0x70, 0xfd, 0x68, 0xa0, 0x0c, 0x9b, 0x34, 0x59, - 0x13, 0x03, 0x9a, 0x14, 0x97, 0x0c, 0x6d, 0x8e, 0x7a, 0x3d, 0x79, 0xbf, 0xdb, 0x93, 0x2e, 0xd4, - 0xcf, 0x7d, 0xb6, 0x44, 0x5d, 0x4d, 0x0a, 0x62, 0x13, 0xcf, 0x48, 0x51, 0xd8, 0x61, 0xad, 0xfd, - 0xae, 0x40, 0x8f, 0xfa, 0xae, 0x7b, 0x6b, 0x2f, 0x3f, 0xfc, 0x63, 0xde, 0x7e, 0x51, 0xa0, 0xff, - 0x8b, 0xb4, 0x83, 0xdf, 0xc0, 0xb4, 0x93, 0x88, 0xbc, 0x7b, 0xdf, 0xc0, 0x00, 0xb4, 0x42, 0xa3, - 0xfb, 0x0a, 0x79, 0x98, 0x86, 0xb4, 0x90, 0x41, 0xf2, 0xe8, 0x2b, 0x6f, 0xed, 0x8b, 0xe0, 0x1e, - 0x7f, 0xad, 0xef, 0xb8, 0xbf, 0xf2, 0x57, 0x91, 0x8b, 0x73, 0x21, 0x95, 0xac, 0xa1, 0x91, 0x06, - 0x2d, 0x39, 0x91, 0x9b, 0x20, 0x0d, 0x68, 0xe3, 0xf1, 0x7e, 0x60, 0xa1, 0xcb, 0xac, 0x90, 0x2d, - 0xfc, 0x97, 0x8f, 0xcf, 0xb2, 0xe3, 0xa4, 0x71, 0x5d, 0x76, 0x9c, 0x3c, 0x91, 0xcd, 0x0a, 0x79, - 0x0f, 0xc7, 0xb9, 0x8c, 0x23, 0x8f, 0xe4, 0x0d, 0x64, 0x89, 0x6a, 0x9c, 0xec, 0x85, 0xdd, 0x9d, - 0x15, 0xc0, 0xff, 0x85, 0xc1, 0x24, 0x25, 0x74, 0xe5, 0x57, 0xd3, 0x38, 0xdd, 0x13, 0xfd, 0xb3, - 0x99, 0xf9, 0x9c, 0x29, 0x33, 0x53, 0x1a, 0xb3, 0x65, 0x66, 0xca, 0xa3, 0x4b, 0x98, 0x99, 0x1b, - 0xd7, 0x32, 0x33, 0x65, 0x97, 0xa3, 0xcc, 0x4c, 0xe9, 0xfc, 0x9b, 0x95, 0x17, 0xcd, 0xb7, 0xaa, - 0x40, 0xdc, 0xaa, 0xc9, 0x1f, 0x92, 0x27, 0x3f, 0x02, 0x00, 0x00, 0xff, 0xff, 0xb6, 0xa5, 0x37, - 0x75, 0xf7, 0x08, 0x00, 0x00, -} diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go deleted file mode 100644 index 37535aac719..00000000000 --- a/pkg/proto/hapi/services/tiller.pb.go +++ /dev/null @@ -1,1449 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/services/tiller.proto - -/* -Package services is a generated protocol buffer package. - -It is generated from these files: - hapi/services/tiller.proto - -It has these top-level messages: - ListReleasesRequest - ListSort - ListReleasesResponse - GetReleaseStatusRequest - GetReleaseStatusResponse - GetReleaseContentRequest - GetReleaseContentResponse - UpdateReleaseRequest - UpdateReleaseResponse - RollbackReleaseRequest - RollbackReleaseResponse - InstallReleaseRequest - InstallReleaseResponse - UninstallReleaseRequest - UninstallReleaseResponse - GetVersionRequest - GetVersionResponse - GetHistoryRequest - GetHistoryResponse - TestReleaseRequest - TestReleaseResponse -*/ -package services - -import proto "github.com/golang/protobuf/proto" -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_release5 "k8s.io/helm/pkg/proto/hapi/release" -import hapi_release4 "k8s.io/helm/pkg/proto/hapi/release" -import hapi_release1 "k8s.io/helm/pkg/proto/hapi/release" -import hapi_release3 "k8s.io/helm/pkg/proto/hapi/release" -import hapi_version "k8s.io/helm/pkg/proto/hapi/version" - -import ( - context "golang.org/x/net/context" - grpc "google.golang.org/grpc" -) - -// 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. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -// SortBy defines sort operations. -type ListSort_SortBy int32 - -const ( - ListSort_UNKNOWN ListSort_SortBy = 0 - ListSort_NAME ListSort_SortBy = 1 - ListSort_LAST_RELEASED ListSort_SortBy = 2 -) - -var ListSort_SortBy_name = map[int32]string{ - 0: "UNKNOWN", - 1: "NAME", - 2: "LAST_RELEASED", -} -var ListSort_SortBy_value = map[string]int32{ - "UNKNOWN": 0, - "NAME": 1, - "LAST_RELEASED": 2, -} - -func (x ListSort_SortBy) String() string { - return proto.EnumName(ListSort_SortBy_name, int32(x)) -} -func (ListSort_SortBy) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{1, 0} } - -// SortOrder defines sort orders to augment sorting operations. -type ListSort_SortOrder int32 - -const ( - ListSort_ASC ListSort_SortOrder = 0 - ListSort_DESC ListSort_SortOrder = 1 -) - -var ListSort_SortOrder_name = map[int32]string{ - 0: "ASC", - 1: "DESC", -} -var ListSort_SortOrder_value = map[string]int32{ - "ASC": 0, - "DESC": 1, -} - -func (x ListSort_SortOrder) String() string { - return proto.EnumName(ListSort_SortOrder_name, int32(x)) -} -func (ListSort_SortOrder) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{1, 1} } - -// ListReleasesRequest requests a list of releases. -// -// Releases can be retrieved in chunks by setting limit and offset. -// -// Releases can be sorted according to a few pre-determined sort stategies. -type ListReleasesRequest struct { - // Limit is the maximum number of releases to be returned. - Limit int64 `protobuf:"varint,1,opt,name=limit" json:"limit,omitempty"` - // Offset is the last release name that was seen. The next listing - // operation will start with the name after this one. - // Example: If list one returns albert, bernie, carl, and sets 'next: dennis'. - // dennis is the offset. Supplying 'dennis' for the next request should - // cause the next batch to return a set of results starting with 'dennis'. - Offset string `protobuf:"bytes,2,opt,name=offset" json:"offset,omitempty"` - // SortBy is the sort field that the ListReleases server should sort data before returning. - SortBy ListSort_SortBy `protobuf:"varint,3,opt,name=sort_by,json=sortBy,enum=hapi.services.tiller.ListSort_SortBy" json:"sort_by,omitempty"` - // Filter is a regular expression used to filter which releases should be listed. - // - // Anything that matches the regexp will be included in the results. - Filter string `protobuf:"bytes,4,opt,name=filter" json:"filter,omitempty"` - // SortOrder is the ordering directive used for sorting. - SortOrder ListSort_SortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,enum=hapi.services.tiller.ListSort_SortOrder" json:"sort_order,omitempty"` - StatusCodes []hapi_release3.Status_Code `protobuf:"varint,6,rep,packed,name=status_codes,json=statusCodes,enum=hapi.release.Status_Code" json:"status_codes,omitempty"` - // Namespace is the filter to select releases only from a specific namespace. - Namespace string `protobuf:"bytes,7,opt,name=namespace" json:"namespace,omitempty"` -} - -func (m *ListReleasesRequest) Reset() { *m = ListReleasesRequest{} } -func (m *ListReleasesRequest) String() string { return proto.CompactTextString(m) } -func (*ListReleasesRequest) ProtoMessage() {} -func (*ListReleasesRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } - -func (m *ListReleasesRequest) GetLimit() int64 { - if m != nil { - return m.Limit - } - return 0 -} - -func (m *ListReleasesRequest) GetOffset() string { - if m != nil { - return m.Offset - } - return "" -} - -func (m *ListReleasesRequest) GetSortBy() ListSort_SortBy { - if m != nil { - return m.SortBy - } - return ListSort_UNKNOWN -} - -func (m *ListReleasesRequest) GetFilter() string { - if m != nil { - return m.Filter - } - return "" -} - -func (m *ListReleasesRequest) GetSortOrder() ListSort_SortOrder { - if m != nil { - return m.SortOrder - } - return ListSort_ASC -} - -func (m *ListReleasesRequest) GetStatusCodes() []hapi_release3.Status_Code { - if m != nil { - return m.StatusCodes - } - return nil -} - -func (m *ListReleasesRequest) GetNamespace() string { - if m != nil { - return m.Namespace - } - return "" -} - -// ListSort defines sorting fields on a release list. -type ListSort struct { -} - -func (m *ListSort) Reset() { *m = ListSort{} } -func (m *ListSort) String() string { return proto.CompactTextString(m) } -func (*ListSort) ProtoMessage() {} -func (*ListSort) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } - -// ListReleasesResponse is a list of releases. -type ListReleasesResponse struct { - // Count is the expected total number of releases to be returned. - Count int64 `protobuf:"varint,1,opt,name=count" json:"count,omitempty"` - // Next is the name of the next release. If this is other than an empty - // string, it means there are more results. - Next string `protobuf:"bytes,2,opt,name=next" json:"next,omitempty"` - // 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_release5.Release `protobuf:"bytes,4,rep,name=releases" json:"releases,omitempty"` -} - -func (m *ListReleasesResponse) Reset() { *m = ListReleasesResponse{} } -func (m *ListReleasesResponse) String() string { return proto.CompactTextString(m) } -func (*ListReleasesResponse) ProtoMessage() {} -func (*ListReleasesResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } - -func (m *ListReleasesResponse) GetCount() int64 { - if m != nil { - return m.Count - } - return 0 -} - -func (m *ListReleasesResponse) GetNext() string { - if m != nil { - return m.Next - } - return "" -} - -func (m *ListReleasesResponse) GetTotal() int64 { - if m != nil { - return m.Total - } - return 0 -} - -func (m *ListReleasesResponse) GetReleases() []*hapi_release5.Release { - if m != nil { - return m.Releases - } - return nil -} - -// GetReleaseStatusRequest is a request to get the status of a release. -type GetReleaseStatusRequest struct { - // Name is the name of the release - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // Version is the version of the release - Version int32 `protobuf:"varint,2,opt,name=version" json:"version,omitempty"` -} - -func (m *GetReleaseStatusRequest) Reset() { *m = GetReleaseStatusRequest{} } -func (m *GetReleaseStatusRequest) String() string { return proto.CompactTextString(m) } -func (*GetReleaseStatusRequest) ProtoMessage() {} -func (*GetReleaseStatusRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } - -func (m *GetReleaseStatusRequest) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *GetReleaseStatusRequest) GetVersion() int32 { - if m != nil { - return m.Version - } - return 0 -} - -// GetReleaseStatusResponse is the response indicating the status of the named release. -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_release4.Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` - // Namespace the release was released into - Namespace string `protobuf:"bytes,3,opt,name=namespace" json:"namespace,omitempty"` -} - -func (m *GetReleaseStatusResponse) Reset() { *m = GetReleaseStatusResponse{} } -func (m *GetReleaseStatusResponse) String() string { return proto.CompactTextString(m) } -func (*GetReleaseStatusResponse) ProtoMessage() {} -func (*GetReleaseStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } - -func (m *GetReleaseStatusResponse) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *GetReleaseStatusResponse) GetInfo() *hapi_release4.Info { - if m != nil { - return m.Info - } - return nil -} - -func (m *GetReleaseStatusResponse) GetNamespace() string { - if m != nil { - return m.Namespace - } - return "" -} - -// GetReleaseContentRequest is a request to get the contents of a release. -type GetReleaseContentRequest struct { - // The name of the release - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // Version is the version of the release - Version int32 `protobuf:"varint,2,opt,name=version" json:"version,omitempty"` -} - -func (m *GetReleaseContentRequest) Reset() { *m = GetReleaseContentRequest{} } -func (m *GetReleaseContentRequest) String() string { return proto.CompactTextString(m) } -func (*GetReleaseContentRequest) ProtoMessage() {} -func (*GetReleaseContentRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } - -func (m *GetReleaseContentRequest) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *GetReleaseContentRequest) GetVersion() int32 { - if m != nil { - return m.Version - } - return 0 -} - -// GetReleaseContentResponse is a response containing the contents of a release. -type GetReleaseContentResponse struct { - // The release content - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` -} - -func (m *GetReleaseContentResponse) Reset() { *m = GetReleaseContentResponse{} } -func (m *GetReleaseContentResponse) String() string { return proto.CompactTextString(m) } -func (*GetReleaseContentResponse) ProtoMessage() {} -func (*GetReleaseContentResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } - -func (m *GetReleaseContentResponse) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -// UpdateReleaseRequest updates a release. -type UpdateReleaseRequest struct { - // The name of the release - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // Chart is the protobuf representation of a chart. - Chart *hapi_chart3.Chart `protobuf:"bytes,2,opt,name=chart" json:"chart,omitempty"` - // Values is a string containing (unparsed) YAML values. - Values *hapi_chart.Config `protobuf:"bytes,3,opt,name=values" json:"values,omitempty"` - // dry_run, if true, will run through the release logic, but neither create - DryRun bool `protobuf:"varint,4,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"` - // DisableHooks causes the server to skip running any hooks for the upgrade. - DisableHooks bool `protobuf:"varint,5,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"` - // Performs pods restart for resources if applicable - Recreate bool `protobuf:"varint,6,opt,name=recreate" json:"recreate,omitempty"` - // timeout specifies the max amount of time any kubernetes client command can run. - Timeout int64 `protobuf:"varint,7,opt,name=timeout" json:"timeout,omitempty"` - // ResetValues will cause Tiller to ignore stored values, resetting to default values. - ResetValues bool `protobuf:"varint,8,opt,name=reset_values,json=resetValues" json:"reset_values,omitempty"` - // wait, if true, will wait until all Pods, PVCs, and Services are in a ready state - // before marking the release as successful. It will wait for as long as timeout - Wait bool `protobuf:"varint,9,opt,name=wait" json:"wait,omitempty"` - // ReuseValues will cause Tiller to reuse the values from the last release. - // This is ignored if reset_values is set. - ReuseValues bool `protobuf:"varint,10,opt,name=reuse_values,json=reuseValues" json:"reuse_values,omitempty"` - // Force resource update through delete/recreate if needed. - Force bool `protobuf:"varint,11,opt,name=force" json:"force,omitempty"` -} - -func (m *UpdateReleaseRequest) Reset() { *m = UpdateReleaseRequest{} } -func (m *UpdateReleaseRequest) String() string { return proto.CompactTextString(m) } -func (*UpdateReleaseRequest) ProtoMessage() {} -func (*UpdateReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } - -func (m *UpdateReleaseRequest) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *UpdateReleaseRequest) GetChart() *hapi_chart3.Chart { - if m != nil { - return m.Chart - } - return nil -} - -func (m *UpdateReleaseRequest) GetValues() *hapi_chart.Config { - if m != nil { - return m.Values - } - return nil -} - -func (m *UpdateReleaseRequest) GetDryRun() bool { - if m != nil { - return m.DryRun - } - return false -} - -func (m *UpdateReleaseRequest) GetDisableHooks() bool { - if m != nil { - return m.DisableHooks - } - return false -} - -func (m *UpdateReleaseRequest) GetRecreate() bool { - if m != nil { - return m.Recreate - } - return false -} - -func (m *UpdateReleaseRequest) GetTimeout() int64 { - if m != nil { - return m.Timeout - } - return 0 -} - -func (m *UpdateReleaseRequest) GetResetValues() bool { - if m != nil { - return m.ResetValues - } - return false -} - -func (m *UpdateReleaseRequest) GetWait() bool { - if m != nil { - return m.Wait - } - return false -} - -func (m *UpdateReleaseRequest) GetReuseValues() bool { - if m != nil { - return m.ReuseValues - } - return false -} - -func (m *UpdateReleaseRequest) GetForce() bool { - if m != nil { - return m.Force - } - return false -} - -// UpdateReleaseResponse is the response to an update request. -type UpdateReleaseResponse struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` -} - -func (m *UpdateReleaseResponse) Reset() { *m = UpdateReleaseResponse{} } -func (m *UpdateReleaseResponse) String() string { return proto.CompactTextString(m) } -func (*UpdateReleaseResponse) ProtoMessage() {} -func (*UpdateReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} } - -func (m *UpdateReleaseResponse) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -type RollbackReleaseRequest struct { - // The name of the release - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // dry_run, if true, will run through the release logic but no create - DryRun bool `protobuf:"varint,2,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"` - // DisableHooks causes the server to skip running any hooks for the rollback - DisableHooks bool `protobuf:"varint,3,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"` - // Version is the version of the release to deploy. - Version int32 `protobuf:"varint,4,opt,name=version" json:"version,omitempty"` - // Performs pods restart for resources if applicable - Recreate bool `protobuf:"varint,5,opt,name=recreate" json:"recreate,omitempty"` - // timeout specifies the max amount of time any kubernetes client command can run. - Timeout int64 `protobuf:"varint,6,opt,name=timeout" json:"timeout,omitempty"` - // wait, if true, will wait until all Pods, PVCs, and Services are in a ready state - // before marking the release as successful. It will wait for as long as timeout - Wait bool `protobuf:"varint,7,opt,name=wait" json:"wait,omitempty"` - // Force resource update through delete/recreate if needed. - Force bool `protobuf:"varint,8,opt,name=force" json:"force,omitempty"` -} - -func (m *RollbackReleaseRequest) Reset() { *m = RollbackReleaseRequest{} } -func (m *RollbackReleaseRequest) String() string { return proto.CompactTextString(m) } -func (*RollbackReleaseRequest) ProtoMessage() {} -func (*RollbackReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} } - -func (m *RollbackReleaseRequest) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *RollbackReleaseRequest) GetDryRun() bool { - if m != nil { - return m.DryRun - } - return false -} - -func (m *RollbackReleaseRequest) GetDisableHooks() bool { - if m != nil { - return m.DisableHooks - } - return false -} - -func (m *RollbackReleaseRequest) GetVersion() int32 { - if m != nil { - return m.Version - } - return 0 -} - -func (m *RollbackReleaseRequest) GetRecreate() bool { - if m != nil { - return m.Recreate - } - return false -} - -func (m *RollbackReleaseRequest) GetTimeout() int64 { - if m != nil { - return m.Timeout - } - return 0 -} - -func (m *RollbackReleaseRequest) GetWait() bool { - if m != nil { - return m.Wait - } - return false -} - -func (m *RollbackReleaseRequest) GetForce() bool { - if m != nil { - return m.Force - } - return false -} - -// RollbackReleaseResponse is the response to an update request. -type RollbackReleaseResponse struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` -} - -func (m *RollbackReleaseResponse) Reset() { *m = RollbackReleaseResponse{} } -func (m *RollbackReleaseResponse) String() string { return proto.CompactTextString(m) } -func (*RollbackReleaseResponse) ProtoMessage() {} -func (*RollbackReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} } - -func (m *RollbackReleaseResponse) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -// InstallReleaseRequest is the request for an installation of a chart. -type InstallReleaseRequest struct { - // Chart is the protobuf representation of a chart. - Chart *hapi_chart3.Chart `protobuf:"bytes,1,opt,name=chart" json:"chart,omitempty"` - // Values is a string containing (unparsed) YAML values. - Values *hapi_chart.Config `protobuf:"bytes,2,opt,name=values" json:"values,omitempty"` - // DryRun, if true, will run through the release logic, but neither create - // a release object nor deploy to Kubernetes. The release object returned - // in the response will be fake. - DryRun bool `protobuf:"varint,3,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"` - // Name is the candidate release name. This must be unique to the - // namespace, otherwise the server will return an error. If it is not - // supplied, the server will autogenerate one. - Name string `protobuf:"bytes,4,opt,name=name" json:"name,omitempty"` - // DisableHooks causes the server to skip running any hooks for the install. - DisableHooks bool `protobuf:"varint,5,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"` - // Namepace is the kubernetes namespace of the release. - Namespace string `protobuf:"bytes,6,opt,name=namespace" json:"namespace,omitempty"` - // ReuseName requests that Tiller re-uses a name, instead of erroring out. - ReuseName bool `protobuf:"varint,7,opt,name=reuse_name,json=reuseName" json:"reuse_name,omitempty"` - // timeout specifies the max amount of time any kubernetes client command can run. - Timeout int64 `protobuf:"varint,8,opt,name=timeout" json:"timeout,omitempty"` - // wait, if true, will wait until all Pods, PVCs, and Services are in a ready state - // before marking the release as successful. It will wait for as long as timeout - Wait bool `protobuf:"varint,9,opt,name=wait" json:"wait,omitempty"` -} - -func (m *InstallReleaseRequest) Reset() { *m = InstallReleaseRequest{} } -func (m *InstallReleaseRequest) String() string { return proto.CompactTextString(m) } -func (*InstallReleaseRequest) ProtoMessage() {} -func (*InstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} } - -func (m *InstallReleaseRequest) GetChart() *hapi_chart3.Chart { - if m != nil { - return m.Chart - } - return nil -} - -func (m *InstallReleaseRequest) GetValues() *hapi_chart.Config { - if m != nil { - return m.Values - } - return nil -} - -func (m *InstallReleaseRequest) GetDryRun() bool { - if m != nil { - return m.DryRun - } - return false -} - -func (m *InstallReleaseRequest) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *InstallReleaseRequest) GetDisableHooks() bool { - if m != nil { - return m.DisableHooks - } - return false -} - -func (m *InstallReleaseRequest) GetNamespace() string { - if m != nil { - return m.Namespace - } - return "" -} - -func (m *InstallReleaseRequest) GetReuseName() bool { - if m != nil { - return m.ReuseName - } - return false -} - -func (m *InstallReleaseRequest) GetTimeout() int64 { - if m != nil { - return m.Timeout - } - return 0 -} - -func (m *InstallReleaseRequest) GetWait() bool { - if m != nil { - return m.Wait - } - return false -} - -// InstallReleaseResponse is the response from a release installation. -type InstallReleaseResponse struct { - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` -} - -func (m *InstallReleaseResponse) Reset() { *m = InstallReleaseResponse{} } -func (m *InstallReleaseResponse) String() string { return proto.CompactTextString(m) } -func (*InstallReleaseResponse) ProtoMessage() {} -func (*InstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} } - -func (m *InstallReleaseResponse) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -// UninstallReleaseRequest represents a request to uninstall a named release. -type UninstallReleaseRequest struct { - // Name is the name of the release to delete. - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // DisableHooks causes the server to skip running any hooks for the uninstall. - DisableHooks bool `protobuf:"varint,2,opt,name=disable_hooks,json=disableHooks" json:"disable_hooks,omitempty"` - // Purge removes the release from the store and make its name free for later use. - Purge bool `protobuf:"varint,3,opt,name=purge" json:"purge,omitempty"` - // timeout specifies the max amount of time any kubernetes client command can run. - Timeout int64 `protobuf:"varint,4,opt,name=timeout" json:"timeout,omitempty"` -} - -func (m *UninstallReleaseRequest) Reset() { *m = UninstallReleaseRequest{} } -func (m *UninstallReleaseRequest) String() string { return proto.CompactTextString(m) } -func (*UninstallReleaseRequest) ProtoMessage() {} -func (*UninstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{13} } - -func (m *UninstallReleaseRequest) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *UninstallReleaseRequest) GetDisableHooks() bool { - if m != nil { - return m.DisableHooks - } - return false -} - -func (m *UninstallReleaseRequest) GetPurge() bool { - if m != nil { - return m.Purge - } - return false -} - -func (m *UninstallReleaseRequest) GetTimeout() int64 { - if m != nil { - return m.Timeout - } - return 0 -} - -// UninstallReleaseResponse represents a successful response to an uninstall request. -type UninstallReleaseResponse struct { - // Release is the release that was marked deleted. - Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` - // Info is an uninstall message - Info string `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` -} - -func (m *UninstallReleaseResponse) Reset() { *m = UninstallReleaseResponse{} } -func (m *UninstallReleaseResponse) String() string { return proto.CompactTextString(m) } -func (*UninstallReleaseResponse) ProtoMessage() {} -func (*UninstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} } - -func (m *UninstallReleaseResponse) GetRelease() *hapi_release5.Release { - if m != nil { - return m.Release - } - return nil -} - -func (m *UninstallReleaseResponse) GetInfo() string { - if m != nil { - return m.Info - } - return "" -} - -// GetVersionRequest requests for version information. -type GetVersionRequest struct { -} - -func (m *GetVersionRequest) Reset() { *m = GetVersionRequest{} } -func (m *GetVersionRequest) String() string { return proto.CompactTextString(m) } -func (*GetVersionRequest) ProtoMessage() {} -func (*GetVersionRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{15} } - -type GetVersionResponse struct { - Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version" json:"Version,omitempty"` -} - -func (m *GetVersionResponse) Reset() { *m = GetVersionResponse{} } -func (m *GetVersionResponse) String() string { return proto.CompactTextString(m) } -func (*GetVersionResponse) ProtoMessage() {} -func (*GetVersionResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{16} } - -func (m *GetVersionResponse) GetVersion() *hapi_version.Version { - if m != nil { - return m.Version - } - return nil -} - -// GetHistoryRequest requests a release's history. -type GetHistoryRequest struct { - // The name of the release. - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // The maximum number of releases to include. - Max int32 `protobuf:"varint,2,opt,name=max" json:"max,omitempty"` -} - -func (m *GetHistoryRequest) Reset() { *m = GetHistoryRequest{} } -func (m *GetHistoryRequest) String() string { return proto.CompactTextString(m) } -func (*GetHistoryRequest) ProtoMessage() {} -func (*GetHistoryRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{17} } - -func (m *GetHistoryRequest) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *GetHistoryRequest) GetMax() int32 { - if m != nil { - return m.Max - } - return 0 -} - -// GetHistoryResponse is received in response to a GetHistory rpc. -type GetHistoryResponse struct { - Releases []*hapi_release5.Release `protobuf:"bytes,1,rep,name=releases" json:"releases,omitempty"` -} - -func (m *GetHistoryResponse) Reset() { *m = GetHistoryResponse{} } -func (m *GetHistoryResponse) String() string { return proto.CompactTextString(m) } -func (*GetHistoryResponse) ProtoMessage() {} -func (*GetHistoryResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{18} } - -func (m *GetHistoryResponse) GetReleases() []*hapi_release5.Release { - if m != nil { - return m.Releases - } - return nil -} - -// TestReleaseRequest is a request to get the status of a release. -type TestReleaseRequest struct { - // Name is the name of the release - Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` - // timeout specifies the max amount of time any kubernetes client command can run. - Timeout int64 `protobuf:"varint,2,opt,name=timeout" json:"timeout,omitempty"` - // cleanup specifies whether or not to attempt pod deletion after test completes - Cleanup bool `protobuf:"varint,3,opt,name=cleanup" json:"cleanup,omitempty"` -} - -func (m *TestReleaseRequest) Reset() { *m = TestReleaseRequest{} } -func (m *TestReleaseRequest) String() string { return proto.CompactTextString(m) } -func (*TestReleaseRequest) ProtoMessage() {} -func (*TestReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{19} } - -func (m *TestReleaseRequest) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *TestReleaseRequest) GetTimeout() int64 { - if m != nil { - return m.Timeout - } - return 0 -} - -func (m *TestReleaseRequest) GetCleanup() bool { - if m != nil { - return m.Cleanup - } - return false -} - -// TestReleaseResponse represents a message from executing a test -type TestReleaseResponse struct { - Msg string `protobuf:"bytes,1,opt,name=msg" json:"msg,omitempty"` - Status hapi_release1.TestRun_Status `protobuf:"varint,2,opt,name=status,enum=hapi.release.TestRun_Status" json:"status,omitempty"` -} - -func (m *TestReleaseResponse) Reset() { *m = TestReleaseResponse{} } -func (m *TestReleaseResponse) String() string { return proto.CompactTextString(m) } -func (*TestReleaseResponse) ProtoMessage() {} -func (*TestReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{20} } - -func (m *TestReleaseResponse) GetMsg() string { - if m != nil { - return m.Msg - } - return "" -} - -func (m *TestReleaseResponse) GetStatus() hapi_release1.TestRun_Status { - if m != nil { - return m.Status - } - return hapi_release1.TestRun_UNKNOWN -} - -func init() { - proto.RegisterType((*ListReleasesRequest)(nil), "hapi.services.tiller.ListReleasesRequest") - proto.RegisterType((*ListSort)(nil), "hapi.services.tiller.ListSort") - proto.RegisterType((*ListReleasesResponse)(nil), "hapi.services.tiller.ListReleasesResponse") - proto.RegisterType((*GetReleaseStatusRequest)(nil), "hapi.services.tiller.GetReleaseStatusRequest") - proto.RegisterType((*GetReleaseStatusResponse)(nil), "hapi.services.tiller.GetReleaseStatusResponse") - proto.RegisterType((*GetReleaseContentRequest)(nil), "hapi.services.tiller.GetReleaseContentRequest") - proto.RegisterType((*GetReleaseContentResponse)(nil), "hapi.services.tiller.GetReleaseContentResponse") - proto.RegisterType((*UpdateReleaseRequest)(nil), "hapi.services.tiller.UpdateReleaseRequest") - proto.RegisterType((*UpdateReleaseResponse)(nil), "hapi.services.tiller.UpdateReleaseResponse") - proto.RegisterType((*RollbackReleaseRequest)(nil), "hapi.services.tiller.RollbackReleaseRequest") - proto.RegisterType((*RollbackReleaseResponse)(nil), "hapi.services.tiller.RollbackReleaseResponse") - proto.RegisterType((*InstallReleaseRequest)(nil), "hapi.services.tiller.InstallReleaseRequest") - proto.RegisterType((*InstallReleaseResponse)(nil), "hapi.services.tiller.InstallReleaseResponse") - proto.RegisterType((*UninstallReleaseRequest)(nil), "hapi.services.tiller.UninstallReleaseRequest") - proto.RegisterType((*UninstallReleaseResponse)(nil), "hapi.services.tiller.UninstallReleaseResponse") - proto.RegisterType((*GetVersionRequest)(nil), "hapi.services.tiller.GetVersionRequest") - proto.RegisterType((*GetVersionResponse)(nil), "hapi.services.tiller.GetVersionResponse") - proto.RegisterType((*GetHistoryRequest)(nil), "hapi.services.tiller.GetHistoryRequest") - proto.RegisterType((*GetHistoryResponse)(nil), "hapi.services.tiller.GetHistoryResponse") - proto.RegisterType((*TestReleaseRequest)(nil), "hapi.services.tiller.TestReleaseRequest") - proto.RegisterType((*TestReleaseResponse)(nil), "hapi.services.tiller.TestReleaseResponse") - proto.RegisterEnum("hapi.services.tiller.ListSort_SortBy", ListSort_SortBy_name, ListSort_SortBy_value) - proto.RegisterEnum("hapi.services.tiller.ListSort_SortOrder", ListSort_SortOrder_name, ListSort_SortOrder_value) -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// Client API for ReleaseService service - -type ReleaseServiceClient interface { - // ListReleases retrieves release history. - // TODO: Allow filtering the set of releases by - // release status. By default, ListAllReleases returns the releases who - // current status is "Active". - ListReleases(ctx context.Context, in *ListReleasesRequest, opts ...grpc.CallOption) (ReleaseService_ListReleasesClient, error) - // GetReleasesStatus retrieves status information for the specified release. - GetReleaseStatus(ctx context.Context, in *GetReleaseStatusRequest, opts ...grpc.CallOption) (*GetReleaseStatusResponse, error) - // GetReleaseContent retrieves the release content (chart + value) for the specified release. - GetReleaseContent(ctx context.Context, in *GetReleaseContentRequest, opts ...grpc.CallOption) (*GetReleaseContentResponse, error) - // UpdateRelease updates release content. - UpdateRelease(ctx context.Context, in *UpdateReleaseRequest, opts ...grpc.CallOption) (*UpdateReleaseResponse, error) - // InstallRelease requests installation of a chart as a new release. - InstallRelease(ctx context.Context, in *InstallReleaseRequest, opts ...grpc.CallOption) (*InstallReleaseResponse, error) - // UninstallRelease requests deletion of a named release. - UninstallRelease(ctx context.Context, in *UninstallReleaseRequest, opts ...grpc.CallOption) (*UninstallReleaseResponse, error) - // GetVersion returns the current version of the server. - GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) - // RollbackRelease rolls back a release to a previous version. - RollbackRelease(ctx context.Context, in *RollbackReleaseRequest, opts ...grpc.CallOption) (*RollbackReleaseResponse, error) - // ReleaseHistory retrieves a releasse's history. - GetHistory(ctx context.Context, in *GetHistoryRequest, opts ...grpc.CallOption) (*GetHistoryResponse, error) - // RunReleaseTest executes the tests defined of a named release - RunReleaseTest(ctx context.Context, in *TestReleaseRequest, opts ...grpc.CallOption) (ReleaseService_RunReleaseTestClient, error) -} - -type releaseServiceClient struct { - cc *grpc.ClientConn -} - -func NewReleaseServiceClient(cc *grpc.ClientConn) ReleaseServiceClient { - return &releaseServiceClient{cc} -} - -func (c *releaseServiceClient) ListReleases(ctx context.Context, in *ListReleasesRequest, opts ...grpc.CallOption) (ReleaseService_ListReleasesClient, error) { - stream, err := grpc.NewClientStream(ctx, &_ReleaseService_serviceDesc.Streams[0], c.cc, "/hapi.services.tiller.ReleaseService/ListReleases", opts...) - if err != nil { - return nil, err - } - x := &releaseServiceListReleasesClient{stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -type ReleaseService_ListReleasesClient interface { - Recv() (*ListReleasesResponse, error) - grpc.ClientStream -} - -type releaseServiceListReleasesClient struct { - grpc.ClientStream -} - -func (x *releaseServiceListReleasesClient) Recv() (*ListReleasesResponse, error) { - m := new(ListReleasesResponse) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -func (c *releaseServiceClient) GetReleaseStatus(ctx context.Context, in *GetReleaseStatusRequest, opts ...grpc.CallOption) (*GetReleaseStatusResponse, error) { - out := new(GetReleaseStatusResponse) - err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/GetReleaseStatus", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseServiceClient) GetReleaseContent(ctx context.Context, in *GetReleaseContentRequest, opts ...grpc.CallOption) (*GetReleaseContentResponse, error) { - out := new(GetReleaseContentResponse) - err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/GetReleaseContent", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseServiceClient) UpdateRelease(ctx context.Context, in *UpdateReleaseRequest, opts ...grpc.CallOption) (*UpdateReleaseResponse, error) { - out := new(UpdateReleaseResponse) - err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/UpdateRelease", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseServiceClient) InstallRelease(ctx context.Context, in *InstallReleaseRequest, opts ...grpc.CallOption) (*InstallReleaseResponse, error) { - out := new(InstallReleaseResponse) - err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/InstallRelease", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseServiceClient) UninstallRelease(ctx context.Context, in *UninstallReleaseRequest, opts ...grpc.CallOption) (*UninstallReleaseResponse, error) { - out := new(UninstallReleaseResponse) - err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/UninstallRelease", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseServiceClient) GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) { - out := new(GetVersionResponse) - err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/GetVersion", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseServiceClient) RollbackRelease(ctx context.Context, in *RollbackReleaseRequest, opts ...grpc.CallOption) (*RollbackReleaseResponse, error) { - out := new(RollbackReleaseResponse) - err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/RollbackRelease", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseServiceClient) GetHistory(ctx context.Context, in *GetHistoryRequest, opts ...grpc.CallOption) (*GetHistoryResponse, error) { - out := new(GetHistoryResponse) - err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/GetHistory", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *releaseServiceClient) RunReleaseTest(ctx context.Context, in *TestReleaseRequest, opts ...grpc.CallOption) (ReleaseService_RunReleaseTestClient, error) { - stream, err := grpc.NewClientStream(ctx, &_ReleaseService_serviceDesc.Streams[1], c.cc, "/hapi.services.tiller.ReleaseService/RunReleaseTest", opts...) - if err != nil { - return nil, err - } - x := &releaseServiceRunReleaseTestClient{stream} - if err := x.ClientStream.SendMsg(in); err != nil { - return nil, err - } - if err := x.ClientStream.CloseSend(); err != nil { - return nil, err - } - return x, nil -} - -type ReleaseService_RunReleaseTestClient interface { - Recv() (*TestReleaseResponse, error) - grpc.ClientStream -} - -type releaseServiceRunReleaseTestClient struct { - grpc.ClientStream -} - -func (x *releaseServiceRunReleaseTestClient) Recv() (*TestReleaseResponse, error) { - m := new(TestReleaseResponse) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -// Server API for ReleaseService service - -type ReleaseServiceServer interface { - // ListReleases retrieves release history. - // TODO: Allow filtering the set of releases by - // release status. By default, ListAllReleases returns the releases who - // current status is "Active". - ListReleases(*ListReleasesRequest, ReleaseService_ListReleasesServer) error - // GetReleasesStatus retrieves status information for the specified release. - GetReleaseStatus(context.Context, *GetReleaseStatusRequest) (*GetReleaseStatusResponse, error) - // GetReleaseContent retrieves the release content (chart + value) for the specified release. - GetReleaseContent(context.Context, *GetReleaseContentRequest) (*GetReleaseContentResponse, error) - // UpdateRelease updates release content. - UpdateRelease(context.Context, *UpdateReleaseRequest) (*UpdateReleaseResponse, error) - // InstallRelease requests installation of a chart as a new release. - InstallRelease(context.Context, *InstallReleaseRequest) (*InstallReleaseResponse, error) - // UninstallRelease requests deletion of a named release. - UninstallRelease(context.Context, *UninstallReleaseRequest) (*UninstallReleaseResponse, error) - // GetVersion returns the current version of the server. - GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) - // RollbackRelease rolls back a release to a previous version. - RollbackRelease(context.Context, *RollbackReleaseRequest) (*RollbackReleaseResponse, error) - // ReleaseHistory retrieves a releasse's history. - GetHistory(context.Context, *GetHistoryRequest) (*GetHistoryResponse, error) - // RunReleaseTest executes the tests defined of a named release - RunReleaseTest(*TestReleaseRequest, ReleaseService_RunReleaseTestServer) error -} - -func RegisterReleaseServiceServer(s *grpc.Server, srv ReleaseServiceServer) { - s.RegisterService(&_ReleaseService_serviceDesc, srv) -} - -func _ReleaseService_ListReleases_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(ListReleasesRequest) - if err := stream.RecvMsg(m); err != nil { - return err - } - return srv.(ReleaseServiceServer).ListReleases(m, &releaseServiceListReleasesServer{stream}) -} - -type ReleaseService_ListReleasesServer interface { - Send(*ListReleasesResponse) error - grpc.ServerStream -} - -type releaseServiceListReleasesServer struct { - grpc.ServerStream -} - -func (x *releaseServiceListReleasesServer) Send(m *ListReleasesResponse) error { - return x.ServerStream.SendMsg(m) -} - -func _ReleaseService_GetReleaseStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetReleaseStatusRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseServiceServer).GetReleaseStatus(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.tiller.ReleaseService/GetReleaseStatus", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseServiceServer).GetReleaseStatus(ctx, req.(*GetReleaseStatusRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseService_GetReleaseContent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetReleaseContentRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseServiceServer).GetReleaseContent(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.tiller.ReleaseService/GetReleaseContent", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseServiceServer).GetReleaseContent(ctx, req.(*GetReleaseContentRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseService_UpdateRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UpdateReleaseRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseServiceServer).UpdateRelease(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.tiller.ReleaseService/UpdateRelease", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseServiceServer).UpdateRelease(ctx, req.(*UpdateReleaseRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseService_InstallRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(InstallReleaseRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseServiceServer).InstallRelease(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.tiller.ReleaseService/InstallRelease", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseServiceServer).InstallRelease(ctx, req.(*InstallReleaseRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseService_UninstallRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UninstallReleaseRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseServiceServer).UninstallRelease(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.tiller.ReleaseService/UninstallRelease", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseServiceServer).UninstallRelease(ctx, req.(*UninstallReleaseRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseService_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetVersionRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseServiceServer).GetVersion(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.tiller.ReleaseService/GetVersion", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseServiceServer).GetVersion(ctx, req.(*GetVersionRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseService_RollbackRelease_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(RollbackReleaseRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseServiceServer).RollbackRelease(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.tiller.ReleaseService/RollbackRelease", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseServiceServer).RollbackRelease(ctx, req.(*RollbackReleaseRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseService_GetHistory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetHistoryRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(ReleaseServiceServer).GetHistory(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/hapi.services.tiller.ReleaseService/GetHistory", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(ReleaseServiceServer).GetHistory(ctx, req.(*GetHistoryRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _ReleaseService_RunReleaseTest_Handler(srv interface{}, stream grpc.ServerStream) error { - m := new(TestReleaseRequest) - if err := stream.RecvMsg(m); err != nil { - return err - } - return srv.(ReleaseServiceServer).RunReleaseTest(m, &releaseServiceRunReleaseTestServer{stream}) -} - -type ReleaseService_RunReleaseTestServer interface { - Send(*TestReleaseResponse) error - grpc.ServerStream -} - -type releaseServiceRunReleaseTestServer struct { - grpc.ServerStream -} - -func (x *releaseServiceRunReleaseTestServer) Send(m *TestReleaseResponse) error { - return x.ServerStream.SendMsg(m) -} - -var _ReleaseService_serviceDesc = grpc.ServiceDesc{ - ServiceName: "hapi.services.tiller.ReleaseService", - HandlerType: (*ReleaseServiceServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "GetReleaseStatus", - Handler: _ReleaseService_GetReleaseStatus_Handler, - }, - { - MethodName: "GetReleaseContent", - Handler: _ReleaseService_GetReleaseContent_Handler, - }, - { - MethodName: "UpdateRelease", - Handler: _ReleaseService_UpdateRelease_Handler, - }, - { - MethodName: "InstallRelease", - Handler: _ReleaseService_InstallRelease_Handler, - }, - { - MethodName: "UninstallRelease", - Handler: _ReleaseService_UninstallRelease_Handler, - }, - { - MethodName: "GetVersion", - Handler: _ReleaseService_GetVersion_Handler, - }, - { - MethodName: "RollbackRelease", - Handler: _ReleaseService_RollbackRelease_Handler, - }, - { - MethodName: "GetHistory", - Handler: _ReleaseService_GetHistory_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "ListReleases", - Handler: _ReleaseService_ListReleases_Handler, - ServerStreams: true, - }, - { - StreamName: "RunReleaseTest", - Handler: _ReleaseService_RunReleaseTest_Handler, - ServerStreams: true, - }, - }, - Metadata: "hapi/services/tiller.proto", -} - -func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) } - -var fileDescriptor0 = []byte{ - // 1217 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x57, 0xdd, 0x6e, 0xe3, 0xc4, - 0x17, 0xaf, 0xf3, 0x9d, 0x93, 0x36, 0xff, 0x74, 0x9a, 0xb6, 0xae, 0xff, 0x0b, 0x2a, 0x46, 0xb0, - 0xd9, 0x85, 0x4d, 0x21, 0x70, 0x83, 0x84, 0x90, 0xba, 0xdd, 0xa8, 0x2d, 0x94, 0xae, 0xe4, 0x6c, - 0x17, 0x09, 0x01, 0x91, 0x9b, 0x4c, 0x5a, 0xb3, 0x8e, 0x27, 0x78, 0xc6, 0x65, 0x7b, 0xcb, 0x1d, - 0x8f, 0xc2, 0x5b, 0xf0, 0x1e, 0x5c, 0xc2, 0x83, 0x20, 0xcf, 0x87, 0xeb, 0x49, 0xed, 0xd6, 0xf4, - 0x26, 0x9e, 0x99, 0xf3, 0xfd, 0x3b, 0x67, 0xce, 0x9c, 0x80, 0x75, 0xe9, 0x2e, 0xbc, 0x3d, 0x8a, - 0xc3, 0x2b, 0x6f, 0x82, 0xe9, 0x1e, 0xf3, 0x7c, 0x1f, 0x87, 0xfd, 0x45, 0x48, 0x18, 0x41, 0xdd, - 0x98, 0xd6, 0x57, 0xb4, 0xbe, 0xa0, 0x59, 0x5b, 0x5c, 0x62, 0x72, 0xe9, 0x86, 0x4c, 0xfc, 0x0a, - 0x6e, 0x6b, 0x3b, 0x7d, 0x4e, 0x82, 0x99, 0x77, 0x21, 0x09, 0xc2, 0x44, 0x88, 0x7d, 0xec, 0x52, - 0xac, 0xbe, 0x9a, 0x90, 0xa2, 0x79, 0xc1, 0x8c, 0x48, 0xc2, 0xff, 0x35, 0x02, 0xc3, 0x94, 0x8d, - 0xc3, 0x28, 0x90, 0xc4, 0x1d, 0x8d, 0x48, 0x99, 0xcb, 0x22, 0xaa, 0x19, 0xbb, 0xc2, 0x21, 0xf5, - 0x48, 0xa0, 0xbe, 0x82, 0x66, 0xff, 0x59, 0x82, 0x8d, 0x13, 0x8f, 0x32, 0x47, 0x08, 0x52, 0x07, - 0xff, 0x12, 0x61, 0xca, 0x50, 0x17, 0xaa, 0xbe, 0x37, 0xf7, 0x98, 0x69, 0xec, 0x1a, 0xbd, 0xb2, - 0x23, 0x36, 0x68, 0x0b, 0x6a, 0x64, 0x36, 0xa3, 0x98, 0x99, 0xa5, 0x5d, 0xa3, 0xd7, 0x74, 0xe4, - 0x0e, 0x7d, 0x05, 0x75, 0x4a, 0x42, 0x36, 0x3e, 0xbf, 0x36, 0xcb, 0xbb, 0x46, 0xaf, 0x3d, 0xf8, - 0xa0, 0x9f, 0x85, 0x53, 0x3f, 0xb6, 0x34, 0x22, 0x21, 0xeb, 0xc7, 0x3f, 0xcf, 0xaf, 0x9d, 0x1a, - 0xe5, 0xdf, 0x58, 0xef, 0xcc, 0xf3, 0x19, 0x0e, 0xcd, 0x8a, 0xd0, 0x2b, 0x76, 0xe8, 0x10, 0x80, - 0xeb, 0x25, 0xe1, 0x14, 0x87, 0x66, 0x95, 0xab, 0xee, 0x15, 0x50, 0xfd, 0x32, 0xe6, 0x77, 0x9a, - 0x54, 0x2d, 0xd1, 0x97, 0xb0, 0x2a, 0x20, 0x19, 0x4f, 0xc8, 0x14, 0x53, 0xb3, 0xb6, 0x5b, 0xee, - 0xb5, 0x07, 0x3b, 0x42, 0x95, 0x82, 0x7f, 0x24, 0x40, 0x3b, 0x20, 0x53, 0xec, 0xb4, 0x04, 0x7b, - 0xbc, 0xa6, 0xe8, 0x11, 0x34, 0x03, 0x77, 0x8e, 0xe9, 0xc2, 0x9d, 0x60, 0xb3, 0xce, 0x3d, 0xbc, - 0x39, 0xb0, 0x7f, 0x82, 0x86, 0x32, 0x6e, 0x0f, 0xa0, 0x26, 0x42, 0x43, 0x2d, 0xa8, 0x9f, 0x9d, - 0x7e, 0x73, 0xfa, 0xf2, 0xbb, 0xd3, 0xce, 0x0a, 0x6a, 0x40, 0xe5, 0x74, 0xff, 0xdb, 0x61, 0xc7, - 0x40, 0xeb, 0xb0, 0x76, 0xb2, 0x3f, 0x7a, 0x35, 0x76, 0x86, 0x27, 0xc3, 0xfd, 0xd1, 0xf0, 0x45, - 0xa7, 0x64, 0xbf, 0x0b, 0xcd, 0xc4, 0x67, 0x54, 0x87, 0xf2, 0xfe, 0xe8, 0x40, 0x88, 0xbc, 0x18, - 0x8e, 0x0e, 0x3a, 0x86, 0xfd, 0xbb, 0x01, 0x5d, 0x3d, 0x45, 0x74, 0x41, 0x02, 0x8a, 0xe3, 0x1c, - 0x4d, 0x48, 0x14, 0x24, 0x39, 0xe2, 0x1b, 0x84, 0xa0, 0x12, 0xe0, 0xb7, 0x2a, 0x43, 0x7c, 0x1d, - 0x73, 0x32, 0xc2, 0x5c, 0x9f, 0x67, 0xa7, 0xec, 0x88, 0x0d, 0xfa, 0x14, 0x1a, 0x32, 0x74, 0x6a, - 0x56, 0x76, 0xcb, 0xbd, 0xd6, 0x60, 0x53, 0x07, 0x44, 0x5a, 0x74, 0x12, 0x36, 0xfb, 0x10, 0xb6, - 0x0f, 0xb1, 0xf2, 0x44, 0xe0, 0xa5, 0x2a, 0x26, 0xb6, 0xeb, 0xce, 0x31, 0x77, 0x26, 0xb6, 0xeb, - 0xce, 0x31, 0x32, 0xa1, 0x2e, 0xcb, 0x8d, 0xbb, 0x53, 0x75, 0xd4, 0xd6, 0x66, 0x60, 0xde, 0x56, - 0x24, 0xe3, 0xca, 0xd2, 0xf4, 0x21, 0x54, 0xe2, 0x9b, 0xc0, 0xd5, 0xb4, 0x06, 0x48, 0xf7, 0xf3, - 0x38, 0x98, 0x11, 0x87, 0xd3, 0xf5, 0x54, 0x95, 0x97, 0x53, 0x75, 0x94, 0xb6, 0x7a, 0x40, 0x02, - 0x86, 0x03, 0xf6, 0x30, 0xff, 0x4f, 0x60, 0x27, 0x43, 0x93, 0x0c, 0x60, 0x0f, 0xea, 0xd2, 0x35, - 0xae, 0x2d, 0x17, 0x57, 0xc5, 0x65, 0xff, 0x5d, 0x82, 0xee, 0xd9, 0x62, 0xea, 0x32, 0xac, 0x48, - 0x77, 0x38, 0xf5, 0x18, 0xaa, 0xbc, 0xa3, 0x48, 0x2c, 0xd6, 0x85, 0x6e, 0xd1, 0x76, 0x0e, 0xe2, - 0x5f, 0x47, 0xd0, 0xd1, 0x53, 0xa8, 0x5d, 0xb9, 0x7e, 0x84, 0x29, 0x07, 0x22, 0x41, 0x4d, 0x72, - 0xf2, 0x76, 0xe4, 0x48, 0x0e, 0xb4, 0x0d, 0xf5, 0x69, 0x78, 0x1d, 0xf7, 0x13, 0x7e, 0x05, 0x1b, - 0x4e, 0x6d, 0x1a, 0x5e, 0x3b, 0x51, 0x80, 0xde, 0x87, 0xb5, 0xa9, 0x47, 0xdd, 0x73, 0x1f, 0x8f, - 0x2f, 0x09, 0x79, 0x43, 0xf9, 0x2d, 0x6c, 0x38, 0xab, 0xf2, 0xf0, 0x28, 0x3e, 0x43, 0x56, 0x5c, - 0x49, 0x93, 0x10, 0xbb, 0x0c, 0x9b, 0x35, 0x4e, 0x4f, 0xf6, 0x31, 0x86, 0xcc, 0x9b, 0x63, 0x12, - 0x31, 0x7e, 0x75, 0xca, 0x8e, 0xda, 0xa2, 0xf7, 0x60, 0x35, 0xc4, 0x14, 0xb3, 0xb1, 0xf4, 0xb2, - 0xc1, 0x25, 0x5b, 0xfc, 0xec, 0xb5, 0x70, 0x0b, 0x41, 0xe5, 0x57, 0xd7, 0x63, 0x66, 0x93, 0x93, - 0xf8, 0x5a, 0x88, 0x45, 0x14, 0x2b, 0x31, 0x50, 0x62, 0x11, 0xc5, 0x52, 0xac, 0x0b, 0xd5, 0x19, - 0x09, 0x27, 0xd8, 0x6c, 0x71, 0x9a, 0xd8, 0xd8, 0x47, 0xb0, 0xb9, 0x04, 0xf2, 0x43, 0xf3, 0xf5, - 0x8f, 0x01, 0x5b, 0x0e, 0xf1, 0xfd, 0x73, 0x77, 0xf2, 0xa6, 0x40, 0xc6, 0x52, 0xe0, 0x96, 0xee, - 0x06, 0xb7, 0x9c, 0x01, 0x6e, 0xaa, 0x08, 0x2b, 0x5a, 0x11, 0x6a, 0xb0, 0x57, 0xf3, 0x61, 0xaf, - 0xe9, 0xb0, 0x2b, 0x4c, 0xeb, 0x29, 0x4c, 0x13, 0xc0, 0x1a, 0x69, 0xc0, 0xbe, 0x86, 0xed, 0x5b, - 0x51, 0x3e, 0x14, 0xb2, 0x3f, 0x4a, 0xb0, 0x79, 0x1c, 0x50, 0xe6, 0xfa, 0xfe, 0x12, 0x62, 0x49, - 0x3d, 0x1b, 0x85, 0xeb, 0xb9, 0xf4, 0x5f, 0xea, 0xb9, 0xac, 0x41, 0xae, 0xf2, 0x53, 0x49, 0xe5, - 0xa7, 0x50, 0x8d, 0x6b, 0x9d, 0xa5, 0xb6, 0xd4, 0x59, 0xd0, 0x3b, 0x00, 0xa2, 0x28, 0xb9, 0x72, - 0x01, 0x6d, 0x93, 0x9f, 0x9c, 0xca, 0x46, 0xa2, 0xb2, 0xd1, 0xc8, 0xce, 0x46, 0xaa, 0xc2, 0xed, - 0x63, 0xd8, 0x5a, 0x86, 0xea, 0xa1, 0xb0, 0xff, 0x66, 0xc0, 0xf6, 0x59, 0xe0, 0x65, 0x02, 0x9f, - 0x55, 0xaa, 0xb7, 0xa0, 0x28, 0x65, 0x40, 0xd1, 0x85, 0xea, 0x22, 0x0a, 0x2f, 0xb0, 0x84, 0x56, - 0x6c, 0xd2, 0x31, 0x56, 0xb4, 0x18, 0xed, 0x31, 0x98, 0xb7, 0x7d, 0x78, 0x60, 0x44, 0xb1, 0xd7, - 0xc9, 0x4b, 0xd0, 0x14, 0x5d, 0xdf, 0xde, 0x80, 0xf5, 0x43, 0xcc, 0x5e, 0x8b, 0x6b, 0x21, 0xc3, - 0xb3, 0x87, 0x80, 0xd2, 0x87, 0x37, 0xf6, 0xe4, 0x91, 0x6e, 0x4f, 0x8d, 0x45, 0x8a, 0x5f, 0x71, - 0xd9, 0x5f, 0x70, 0xdd, 0x47, 0x1e, 0x65, 0x24, 0xbc, 0xbe, 0x0b, 0xba, 0x0e, 0x94, 0xe7, 0xee, - 0x5b, 0xf9, 0x50, 0xc4, 0x4b, 0xfb, 0x90, 0x7b, 0x90, 0x88, 0x4a, 0x0f, 0xd2, 0xcf, 0xae, 0x51, - 0xec, 0xd9, 0xfd, 0x01, 0xd0, 0x2b, 0x9c, 0x4c, 0x00, 0xf7, 0xbc, 0x58, 0x2a, 0x09, 0x25, 0xbd, - 0xd0, 0x4c, 0xa8, 0x4f, 0x7c, 0xec, 0x06, 0xd1, 0x42, 0xa6, 0x4d, 0x6d, 0xed, 0x1f, 0x61, 0x43, - 0xd3, 0x2e, 0xfd, 0x8c, 0xe3, 0xa1, 0x17, 0x52, 0x7b, 0xbc, 0x44, 0x9f, 0x43, 0x4d, 0x8c, 0x45, - 0x5c, 0x77, 0x7b, 0xf0, 0x48, 0xf7, 0x9b, 0x2b, 0x89, 0x02, 0x39, 0x47, 0x39, 0x92, 0x77, 0xf0, - 0x57, 0x03, 0xda, 0xea, 0xa1, 0x17, 0x43, 0x1b, 0xf2, 0x60, 0x35, 0x3d, 0xd1, 0xa0, 0x27, 0xf9, - 0x33, 0xdd, 0xd2, 0x60, 0x6a, 0x3d, 0x2d, 0xc2, 0x2a, 0x22, 0xb0, 0x57, 0x3e, 0x31, 0x10, 0x85, - 0xce, 0xf2, 0xa0, 0x81, 0x9e, 0x65, 0xeb, 0xc8, 0x99, 0x6c, 0xac, 0x7e, 0x51, 0x76, 0x65, 0x16, - 0x5d, 0xf1, 0x9a, 0xd1, 0xa7, 0x03, 0x74, 0xaf, 0x1a, 0x7d, 0x20, 0xb1, 0xf6, 0x0a, 0xf3, 0x27, - 0x76, 0x7f, 0x86, 0x35, 0xed, 0x85, 0x43, 0x39, 0x68, 0x65, 0xcd, 0x1a, 0xd6, 0x47, 0x85, 0x78, - 0x13, 0x5b, 0x73, 0x68, 0xeb, 0x4d, 0x0a, 0xe5, 0x28, 0xc8, 0xec, 0xfa, 0xd6, 0xc7, 0xc5, 0x98, - 0x13, 0x73, 0x14, 0x3a, 0xcb, 0x3d, 0x24, 0x2f, 0x8f, 0x39, 0xfd, 0x2e, 0x2f, 0x8f, 0x79, 0xad, - 0xc9, 0x5e, 0x41, 0x2e, 0xc0, 0x4d, 0x0b, 0x41, 0x8f, 0x73, 0x13, 0xa2, 0x77, 0x1e, 0xab, 0x77, - 0x3f, 0x63, 0x62, 0x62, 0x01, 0xff, 0x5b, 0x7a, 0x63, 0x51, 0x0e, 0x34, 0xd9, 0x03, 0x87, 0xf5, - 0xac, 0x20, 0xf7, 0x52, 0x50, 0xb2, 0x2b, 0xdd, 0x11, 0x94, 0xde, 0xf2, 0xee, 0x08, 0x6a, 0xa9, - 0xc1, 0xd9, 0x2b, 0xc8, 0x83, 0xb6, 0x13, 0x05, 0xd2, 0x74, 0xdc, 0x16, 0x50, 0x8e, 0xf4, 0xed, - 0xae, 0x66, 0x3d, 0x29, 0xc0, 0x79, 0x73, 0xbf, 0x9f, 0xc3, 0xf7, 0x0d, 0xc5, 0x7a, 0x5e, 0xe3, - 0xff, 0x69, 0x3f, 0xfb, 0x37, 0x00, 0x00, 0xff, 0xff, 0xf3, 0x7c, 0x9c, 0x49, 0xc1, 0x0f, 0x00, - 0x00, -} diff --git a/pkg/proto/hapi/version/version.pb.go b/pkg/proto/hapi/version/version.pb.go deleted file mode 100644 index 13c8568f053..00000000000 --- a/pkg/proto/hapi/version/version.pb.go +++ /dev/null @@ -1,81 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: hapi/version/version.proto - -/* -Package version is a generated protocol buffer package. - -It is generated from these files: - hapi/version/version.proto - -It has these top-level messages: - Version -*/ -package version - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" - -// 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. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -type Version struct { - // Sem ver string for the version - SemVer string `protobuf:"bytes,1,opt,name=sem_ver,json=semVer" json:"sem_ver,omitempty"` - GitCommit string `protobuf:"bytes,2,opt,name=git_commit,json=gitCommit" json:"git_commit,omitempty"` - GitTreeState string `protobuf:"bytes,3,opt,name=git_tree_state,json=gitTreeState" json:"git_tree_state,omitempty"` -} - -func (m *Version) Reset() { *m = Version{} } -func (m *Version) String() string { return proto.CompactTextString(m) } -func (*Version) ProtoMessage() {} -func (*Version) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } - -func (m *Version) GetSemVer() string { - if m != nil { - return m.SemVer - } - return "" -} - -func (m *Version) GetGitCommit() string { - if m != nil { - return m.GitCommit - } - return "" -} - -func (m *Version) GetGitTreeState() string { - if m != nil { - return m.GitTreeState - } - return "" -} - -func init() { - proto.RegisterType((*Version)(nil), "hapi.version.Version") -} - -func init() { proto.RegisterFile("hapi/version/version.proto", fileDescriptor0) } - -var fileDescriptor0 = []byte{ - // 151 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0xca, 0x48, 0x2c, 0xc8, - 0xd4, 0x2f, 0x4b, 0x2d, 0x2a, 0xce, 0xcc, 0xcf, 0x83, 0xd1, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, - 0x42, 0x3c, 0x20, 0x39, 0x3d, 0xa8, 0x98, 0x52, 0x3a, 0x17, 0x7b, 0x18, 0x84, 0x29, 0x24, 0xce, - 0xc5, 0x5e, 0x9c, 0x9a, 0x1b, 0x5f, 0x96, 0x5a, 0x24, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x19, 0xc4, - 0x56, 0x9c, 0x9a, 0x1b, 0x96, 0x5a, 0x24, 0x24, 0xcb, 0xc5, 0x95, 0x9e, 0x59, 0x12, 0x9f, 0x9c, - 0x9f, 0x9b, 0x9b, 0x59, 0x22, 0xc1, 0x04, 0x96, 0xe3, 0x4c, 0xcf, 0x2c, 0x71, 0x06, 0x0b, 0x08, - 0xa9, 0x70, 0xf1, 0x81, 0xa4, 0x4b, 0x8a, 0x52, 0x53, 0xe3, 0x8b, 0x4b, 0x12, 0x4b, 0x52, 0x25, - 0x98, 0xc1, 0x4a, 0x78, 0xd2, 0x33, 0x4b, 0x42, 0x8a, 0x52, 0x53, 0x83, 0x41, 0x62, 0x4e, 0x9c, - 0x51, 0xec, 0x50, 0x3b, 0x93, 0xd8, 0xc0, 0x0e, 0x31, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x20, - 0xcc, 0x0e, 0x1b, 0xa6, 0x00, 0x00, 0x00, -} diff --git a/pkg/provenance/doc.go b/pkg/provenance/doc.go index dacfa9e6920..3d2d0ea971a 100644 --- a/pkg/provenance/doc.go +++ b/pkg/provenance/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -34,4 +34,4 @@ and using `gpg --verify`, `keybase pgp verify`, or similar: gpg: Signature made Mon Jul 25 17:23:44 2016 MDT using RSA key ID 1FC18762 gpg: Good signature from "Helm Testing (This key should only be used for testing. DO NOT TRUST.) " [ultimate] */ -package provenance // import "k8s.io/helm/pkg/provenance" +package provenance // import "helm.sh/helm/v3/pkg/provenance" diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index ecd6612a3f5..5d16779f107 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -19,22 +19,20 @@ import ( "bytes" "crypto" "encoding/hex" - "errors" - "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" - "github.com/ghodss/yaml" - + "github.com/pkg/errors" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/clearsign" "golang.org/x/crypto/openpgp/packet" + "sigs.k8s.io/yaml" - "k8s.io/helm/pkg/chartutil" - hapi "k8s.io/helm/pkg/proto/hapi/chart" + hapi "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" ) var defaultPGPConfig = packet.Config{ @@ -142,7 +140,7 @@ func NewFromKeyring(keyringfile, id string) (*Signatory, error) { } } if vague { - return s, fmt.Errorf("more than one key contain the id %q", id) + return s, errors.Errorf("more than one key contain the id %q", id) } s.Entity = candidate @@ -171,7 +169,7 @@ func (s *Signatory) DecryptKey(fn PassphraseFetcher) error { if s.Entity == nil { return errors.New("private key not found") } else if s.Entity.PrivateKey == nil { - return errors.New("provided key is not a private key") + return errors.New("provided key is not a private key. Try providing a keyring with secret keys") } // Nothing else to do if key is not encrypted. @@ -205,7 +203,7 @@ func (s *Signatory) ClearSign(chartpath string) (string, error) { if s.Entity == nil { return "", errors.New("private key not found") } else if s.Entity.PrivateKey == nil { - return "", errors.New("provided key is not a private key") + return "", errors.New("provided key is not a private key. Try providing a keyring with secret keys") } if fi, err := os.Stat(chartpath); err != nil { @@ -238,14 +236,14 @@ func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) { if fi, err := os.Stat(fname); err != nil { return ver, err } else if fi.IsDir() { - return ver, fmt.Errorf("%s cannot be a directory", fname) + return ver, errors.Errorf("%s cannot be a directory", fname) } } // First verify the signature sig, err := s.decodeSignature(sigpath) if err != nil { - return ver, fmt.Errorf("failed to decode signature: %s", err) + return ver, errors.Wrap(err, "failed to decode signature") } by, err := s.verifySignature(sig) @@ -267,9 +265,9 @@ func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) { sum = "sha256:" + sum basename := filepath.Base(chartpath) if sha, ok := sums.Files[basename]; !ok { - return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", basename) + return ver, errors.Errorf("provenance does not contain a SHA for a file named %q", basename) } else if sha != sum { - return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum) + return ver, errors.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum) } ver.FileHash = sum ver.FileName = basename @@ -319,7 +317,7 @@ func messageBlock(chartpath string) (*bytes.Buffer, error) { } // Load the archive into memory. - chart, err := chartutil.LoadFile(chartpath) + chart, err := loader.LoadFile(chartpath) if err != nil { return b, err } @@ -404,6 +402,8 @@ func DigestFile(filename string) (string, error) { // Helm uses SHA256 as its default hash for all non-cryptographic applications. func Digest(in io.Reader) (string, error) { hash := crypto.SHA256.New() - io.Copy(hash, in) + if _, err := io.Copy(hash, in); err != nil { + return "", nil + } return hex.EncodeToString(hash.Sum(nil)), nil } diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index 388941deb87..1f4d2d232a5 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -63,13 +63,14 @@ const ( ) // testMessageBlock represents the expected message block for the testdata/hashtest chart. -const testMessageBlock = `description: Test chart versioning +const testMessageBlock = `apiVersion: v1 +description: Test chart versioning name: hashtest version: 1.2.3 ... files: - hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 + hashtest-1.2.3.tgz: sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 ` func TestMessageBlock(t *testing.T) { @@ -100,7 +101,7 @@ func TestParseMessageBlock(t *testing.T) { if hash, ok := sc.Files["hashtest-1.2.3.tgz"]; !ok { t.Errorf("hashtest file not found in Files") - } else if hash != "sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75" { + } else if hash != "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888" { t.Errorf("Unexpected hash: %q", hash) } } diff --git a/pkg/provenance/testdata/hashtest-1.2.3.tgz b/pkg/provenance/testdata/hashtest-1.2.3.tgz index 1e89b524f4f..7bbc533cae1 100644 Binary files a/pkg/provenance/testdata/hashtest-1.2.3.tgz and b/pkg/provenance/testdata/hashtest-1.2.3.tgz differ diff --git a/pkg/provenance/testdata/hashtest-1.2.3.tgz.prov b/pkg/provenance/testdata/hashtest-1.2.3.tgz.prov new file mode 100755 index 00000000000..3a788cd2ebc --- /dev/null +++ b/pkg/provenance/testdata/hashtest-1.2.3.tgz.prov @@ -0,0 +1,21 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +apiVersion: v1 +description: Test chart versioning +name: hashtest +version: 1.2.3 + +... +files: + hashtest-1.2.3.tgz: sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 +-----BEGIN PGP SIGNATURE----- + +wsBcBAEBCgAQBQJcon2ICRCEO7+YH8GHYgAASEAIAHD4Rad+LF47qNydI+k7x3aC +/qkdsqxE9kCUHtTJkZObE/Zmj2w3Opq0gcQftz4aJ2G9raqPDvwOzxnTxOkGfUdK +qIye48gFHzr2a7HnMTWr+HLQc4Gg+9kysIwkW4TM8wYV10osysYjBrhcafrHzFSK +791dBHhXP/aOrJQbFRob0GRFQ4pXdaSww1+kVaZLiKSPkkMKt9uk9Po1ggJYSIDX +uzXNcr78jTWACqkAtwx8+CJ8yzcGeuXSVNABDgbmAgpY0YT+Bz/UOWq4Q7tyuWnS +x9BKrvcb+Gc/6S0oK0Ffp8K4iSWYp79uH1bZ2oBS1yajA0c5h5i7qI3N4cabREw= +=YgnR +-----END PGP SIGNATURE----- \ No newline at end of file diff --git a/pkg/provenance/testdata/hashtest.sha256 b/pkg/provenance/testdata/hashtest.sha256 index 829031f9d2f..05173edf8cb 100644 --- a/pkg/provenance/testdata/hashtest.sha256 +++ b/pkg/provenance/testdata/hashtest.sha256 @@ -1 +1 @@ -8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 hashtest-1.2.3.tgz +c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 hashtest-1.2.3.tgz diff --git a/pkg/provenance/testdata/hashtest/Chart.yaml b/pkg/provenance/testdata/hashtest/Chart.yaml index 342631ef882..6edf5f8b62b 100644 --- a/pkg/provenance/testdata/hashtest/Chart.yaml +++ b/pkg/provenance/testdata/hashtest/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: Test chart versioning name: hashtest version: 1.2.3 diff --git a/pkg/provenance/testdata/msgblock.yaml b/pkg/provenance/testdata/msgblock.yaml index 0fdbda8ce50..c16293ffc3d 100644 --- a/pkg/provenance/testdata/msgblock.yaml +++ b/pkg/provenance/testdata/msgblock.yaml @@ -1,7 +1,8 @@ +apiVersion: v1 description: Test chart versioning name: hashtest version: 1.2.3 ... files: - hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 + hashtest-1.2.3.tgz: sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 diff --git a/pkg/provenance/testdata/msgblock.yaml.asc b/pkg/provenance/testdata/msgblock.yaml.asc index 5a34d6c52cb..b4187b74230 100644 --- a/pkg/provenance/testdata/msgblock.yaml.asc +++ b/pkg/provenance/testdata/msgblock.yaml.asc @@ -1,21 +1,22 @@ -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 +apiVersion: v1 description: Test chart versioning name: hashtest version: 1.2.3 ... files: - hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 + hashtest-1.2.3.tgz: sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 -----BEGIN PGP SIGNATURE----- -Comment: GPGTools - https://gpgtools.org -iQEcBAEBCgAGBQJXlp8KAAoJEIQ7v5gfwYdiE7sIAJYDiza+asekeooSXLvQiK+G -PKnveqQpx49EZ6L7Y7UlW25SyH8EjXXHeJysDywCXF3w4luxN9n56ffU0KEW11IY -F+JSjmgIWLS6ti7ZAGEi6JInQ/30rOAIpTEBRBL2IueW3m63mezrGK6XkBlGqpor -C9WKeqLi+DWlMoBtsEy3Uk0XP6pn/qBFICYAbLQQU0sCCUT8CBA8f8aidxi7aw9t -i404yYF+Dvc6i4JlSG77SV0ZJBWllUvsWoCd9Jli0NAuaMqmE7mzcEt/dE+Fm2Ql -Bx3tr1WS4xTRiFQdcOttOl93H+OaHTh+Y0qqLTzzpCvqmttG0HfI6lMeCs7LeyA= -=vEK+ +iQFJBAEBCgAzFiEEXmFTibU8o38O5gvThDu/mB/Bh2IFAlyiiDcVHGhlbG0tdGVz +dGluZ0BoZWxtLnNoAAoJEIQ7v5gfwYdiILAH/2f3GMVh+ZY5a+szOBudcuivjTcz +0Im1MwWQZfB1po3Yu7smWZbf5tJCzvVpYtvRlfa0nguuIh763MwOh9Q7dBXOLAxm +VCxqHm3svnNenBNfOpIygaMTgMZKxI4RrsKBgwPOTmlNtKg2lVaCiJAI30TXE6bB +/DwEYX0wmTssrAcSpTzOOSC+zHnPKew+5A3SY3ms+gAtVAcLepmJjI7RS7RhQxDl +AG+rWYis5gpDrk3U9OG1EOxqbftOAMqUl/kwI9eu5cPouN85rWwMe5pvHAvuyr/y +caYdlXDHTZsXmBuvfiUX6gqXtrpPCyKTCP+RzNf3+bXJM8m3u3gbMjGvKjU= +=vHcU -----END PGP SIGNATURE----- diff --git a/pkg/release/hook.go b/pkg/release/hook.go new file mode 100644 index 00000000000..cb995558225 --- /dev/null +++ b/pkg/release/hook.go @@ -0,0 +1,106 @@ +/* +Copyright The Helm Authors. + +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 release + +import ( + "helm.sh/helm/v3/pkg/time" +) + +// HookEvent specifies the hook event +type HookEvent string + +// Hook event types +const ( + HookPreInstall HookEvent = "pre-install" + HookPostInstall HookEvent = "post-install" + HookPreDelete HookEvent = "pre-delete" + HookPostDelete HookEvent = "post-delete" + HookPreUpgrade HookEvent = "pre-upgrade" + HookPostUpgrade HookEvent = "post-upgrade" + HookPreRollback HookEvent = "pre-rollback" + HookPostRollback HookEvent = "post-rollback" + HookTest HookEvent = "test" +) + +func (x HookEvent) String() string { return string(x) } + +// HookDeletePolicy specifies the hook delete policy +type HookDeletePolicy string + +// Hook delete policy types +const ( + HookSucceeded HookDeletePolicy = "hook-succeeded" + HookFailed HookDeletePolicy = "hook-failed" + HookBeforeHookCreation HookDeletePolicy = "before-hook-creation" +) + +func (x HookDeletePolicy) String() string { return string(x) } + +// HookAnnotation is the label name for a hook +const HookAnnotation = "helm.sh/hook" + +// HookWeightAnnotation is the label name for a hook weight +const HookWeightAnnotation = "helm.sh/hook-weight" + +// HookDeleteAnnotation is the label name for the delete policy for a hook +const HookDeleteAnnotation = "helm.sh/hook-delete-policy" + +// Hook defines a hook object. +type Hook struct { + Name string `json:"name,omitempty"` + // Kind is the Kubernetes kind. + Kind string `json:"kind,omitempty"` + // Path is the chart-relative path to the template. + Path string `json:"path,omitempty"` + // Manifest is the manifest contents. + Manifest string `json:"manifest,omitempty"` + // Events are the events that this hook fires on. + Events []HookEvent `json:"events,omitempty"` + // LastRun indicates the date/time this was last run. + LastRun HookExecution `json:"last_run,omitempty"` + // Weight indicates the sort order for execution among similar Hook type + Weight int `json:"weight,omitempty"` + // DeletePolicies are the policies that indicate when to delete the hook + DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"` +} + +// A HookExecution records the result for the last execution of a hook for a given release. +type HookExecution struct { + // StartedAt indicates the date/time this hook was started + StartedAt time.Time `json:"started_at,omitempty"` + // CompletedAt indicates the date/time this hook was completed. + CompletedAt time.Time `json:"completed_at,omitempty"` + // Phase indicates whether the hook completed successfully + Phase HookPhase `json:"phase"` +} + +// A HookPhase indicates the state of a hook execution +type HookPhase string + +const ( + // HookPhaseUnknown indicates that a hook is in an unknown state + HookPhaseUnknown HookPhase = "Unknown" + // HookPhaseRunning indicates that a hook is currently executing + HookPhaseRunning HookPhase = "Running" + // HookPhaseSucceeded indicates that hook execution succeeded + HookPhaseSucceeded HookPhase = "Succeeded" + // HookPhaseFailed indicates that hook execution failed + HookPhaseFailed HookPhase = "Failed" +) + +// String converts a hook phase to a printable string +func (x HookPhase) String() string { return string(x) } diff --git a/pkg/release/info.go b/pkg/release/info.go new file mode 100644 index 00000000000..0cb2bab6443 --- /dev/null +++ b/pkg/release/info.go @@ -0,0 +1,36 @@ +/* +Copyright The Helm Authors. +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 release + +import ( + "helm.sh/helm/v3/pkg/time" +) + +// Info describes release information. +type Info struct { + // FirstDeployed is when the release was first deployed. + FirstDeployed time.Time `json:"first_deployed,omitempty"` + // LastDeployed is when the release was last deployed. + LastDeployed time.Time `json:"last_deployed,omitempty"` + // Deleted tracks when this object was deleted. + Deleted time.Time `json:"deleted"` + // Description is human-friendly "log entry" about this release. + Description string `json:"description,omitempty"` + // Status is the current state of the release + Status Status `json:"status,omitempty"` + // Contains the rendered templates/NOTES.txt if available + Notes string `json:"notes,omitempty"` +} diff --git a/pkg/release/mock.go b/pkg/release/mock.go new file mode 100644 index 00000000000..a28e1dc166f --- /dev/null +++ b/pkg/release/mock.go @@ -0,0 +1,116 @@ +/* +Copyright The Helm Authors. + +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 release + +import ( + "fmt" + "math/rand" + + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/time" +) + +// MockHookTemplate is the hook template used for all mock release objects. +var MockHookTemplate = `apiVersion: v1 +kind: Job +metadata: + annotations: + "helm.sh/hook": pre-install +` + +// MockManifest is the manifest used for all mock release objects. +var MockManifest = `apiVersion: v1 +kind: Secret +metadata: + name: fixture +` + +// MockReleaseOptions allows for user-configurable options on mock release objects. +type MockReleaseOptions struct { + Name string + Version int + Chart *chart.Chart + Status Status + Namespace string +} + +// Mock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing. +func Mock(opts *MockReleaseOptions) *Release { + date := time.Unix(242085845, 0).UTC() + + name := opts.Name + if name == "" { + name = "testrelease-" + fmt.Sprint(rand.Intn(100)) + } + + version := 1 + if opts.Version != 0 { + version = opts.Version + } + + namespace := opts.Namespace + if namespace == "" { + namespace = "default" + } + + ch := opts.Chart + if opts.Chart == nil { + ch = &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "foo", + Version: "0.1.0-beta.1", + AppVersion: "1.0", + }, + Templates: []*chart.File{ + {Name: "templates/foo.tpl", Data: []byte(MockManifest)}, + }, + } + } + + scode := StatusDeployed + if len(opts.Status) > 0 { + scode = opts.Status + } + + info := &Info{ + FirstDeployed: date, + LastDeployed: date, + Status: scode, + Description: "Release mock", + Notes: "Some mock release notes!", + } + + return &Release{ + Name: name, + Info: info, + Chart: ch, + Config: map[string]interface{}{"name": "value"}, + Version: version, + Namespace: namespace, + Hooks: []*Hook{ + { + Name: "pre-install-hook", + Kind: "Job", + Path: "pre-install-hook.yaml", + Manifest: MockHookTemplate, + LastRun: HookExecution{}, + Events: []HookEvent{HookPreInstall}, + }, + }, + Manifest: MockManifest, + } +} diff --git a/pkg/release/release.go b/pkg/release/release.go new file mode 100644 index 00000000000..b9061287362 --- /dev/null +++ b/pkg/release/release.go @@ -0,0 +1,49 @@ +/* +Copyright The Helm Authors. +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 release + +import "helm.sh/helm/v3/pkg/chart" + +// Release describes a deployment of a chart, together with the chart +// and the variables used to deploy that chart. +type Release struct { + // Name is the name of the release + Name string `json:"name,omitempty"` + // Info provides information about a release + Info *Info `json:"info,omitempty"` + // Chart is the chart that was released. + Chart *chart.Chart `json:"chart,omitempty"` + // Config is the set of extra Values added to the chart. + // These values override the default values inside of the chart. + Config map[string]interface{} `json:"config,omitempty"` + // Manifest is the string representation of the rendered template. + Manifest string `json:"manifest,omitempty"` + // Hooks are all of the hooks declared for this release. + Hooks []*Hook `json:"hooks,omitempty"` + // Version is an int which represents the revision of the release. + Version int `json:"version,omitempty"` + // Namespace is the kubernetes namespace of the release. + Namespace string `json:"namespace,omitempty"` + // Labels of the release. + // Disabled encoding into Json cause labels are stored in storage driver metadata field. + Labels map[string]string `json:"-"` +} + +// SetStatus is a helper for setting the status on a release. +func (r *Release) SetStatus(status Status, msg string) { + r.Info.Status = status + r.Info.Description = msg +} diff --git a/pkg/release/responses.go b/pkg/release/responses.go new file mode 100644 index 00000000000..7ee1fc2eeef --- /dev/null +++ b/pkg/release/responses.go @@ -0,0 +1,24 @@ +/* +Copyright The Helm Authors. +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 release + +// UninstallReleaseResponse represents a successful response to an uninstall request. +type UninstallReleaseResponse struct { + // Release is the release that was marked deleted. + Release *Release `json:"release,omitempty"` + // Info is an uninstall message + Info string `json:"info,omitempty"` +} diff --git a/pkg/release/status.go b/pkg/release/status.go new file mode 100644 index 00000000000..e0e3ed62a93 --- /dev/null +++ b/pkg/release/status.go @@ -0,0 +1,49 @@ +/* +Copyright The Helm Authors. +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 release + +// Status is the status of a release +type Status string + +// Describe the status of a release +// NOTE: Make sure to update cmd/helm/status.go when adding or modifying any of these statuses. +const ( + // StatusUnknown indicates that a release is in an uncertain state. + StatusUnknown Status = "unknown" + // StatusDeployed indicates that the release has been pushed to Kubernetes. + StatusDeployed Status = "deployed" + // StatusUninstalled indicates that a release has been uninstalled from Kubernetes. + StatusUninstalled Status = "uninstalled" + // StatusSuperseded indicates that this release object is outdated and a newer one exists. + StatusSuperseded Status = "superseded" + // StatusFailed indicates that the release was not successfully deployed. + StatusFailed Status = "failed" + // StatusUninstalling indicates that a uninstall operation is underway. + StatusUninstalling Status = "uninstalling" + // StatusPendingInstall indicates that an install operation is underway. + StatusPendingInstall Status = "pending-install" + // StatusPendingUpgrade indicates that an upgrade operation is underway. + StatusPendingUpgrade Status = "pending-upgrade" + // StatusPendingRollback indicates that an rollback operation is underway. + StatusPendingRollback Status = "pending-rollback" +) + +func (x Status) String() string { return string(x) } + +// IsPending determines if this status is a state or a transition. +func (x Status) IsPending() bool { + return x == StatusPendingInstall || x == StatusPendingUpgrade || x == StatusPendingRollback +} diff --git a/pkg/releasetesting/environment.go b/pkg/releasetesting/environment.go deleted file mode 100644 index 3b3d07933ee..00000000000 --- a/pkg/releasetesting/environment.go +++ /dev/null @@ -1,122 +0,0 @@ -/* -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 releasetesting - -import ( - "bytes" - "fmt" - "log" - "time" - - "k8s.io/kubernetes/pkg/apis/core" - - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/tiller/environment" -) - -// Environment encapsulates information about where test suite executes and returns results -type Environment struct { - Namespace string - KubeClient environment.KubeClient - Stream services.ReleaseService_RunReleaseTestServer - Timeout int64 -} - -func (env *Environment) createTestPod(test *test) error { - b := bytes.NewBufferString(test.manifest) - if err := env.KubeClient.Create(env.Namespace, b, env.Timeout, false); err != nil { - log.Printf(err.Error()) - test.result.Info = err.Error() - test.result.Status = release.TestRun_FAILURE - return err - } - - return nil -} - -func (env *Environment) getTestPodStatus(test *test) (core.PodPhase, error) { - b := bytes.NewBufferString(test.manifest) - status, err := env.KubeClient.WaitAndGetCompletedPodPhase(env.Namespace, b, time.Duration(env.Timeout)*time.Second) - if err != nil { - log.Printf("Error getting status for pod %s: %s", test.result.Name, err) - test.result.Info = err.Error() - test.result.Status = release.TestRun_UNKNOWN - return status, err - } - - return status, err -} - -func (env *Environment) streamResult(r *release.TestRun) error { - switch r.Status { - case release.TestRun_SUCCESS: - if err := env.streamSuccess(r.Name); err != nil { - return err - } - case release.TestRun_FAILURE: - if err := env.streamFailed(r.Name); err != nil { - return err - } - - default: - if err := env.streamUnknown(r.Name, r.Info); err != nil { - return err - } - } - return nil -} - -func (env *Environment) streamRunning(name string) error { - msg := "RUNNING: " + name - return env.streamMessage(msg, release.TestRun_RUNNING) -} - -func (env *Environment) streamError(info string) error { - msg := "ERROR: " + info - return env.streamMessage(msg, release.TestRun_FAILURE) -} - -func (env *Environment) streamFailed(name string) error { - msg := fmt.Sprintf("FAILED: %s, run `kubectl logs %s --namespace %s` for more info", name, name, env.Namespace) - return env.streamMessage(msg, release.TestRun_FAILURE) -} - -func (env *Environment) streamSuccess(name string) error { - msg := fmt.Sprintf("PASSED: %s", name) - return env.streamMessage(msg, release.TestRun_SUCCESS) -} - -func (env *Environment) streamUnknown(name, info string) error { - msg := fmt.Sprintf("UNKNOWN: %s: %s", name, info) - return env.streamMessage(msg, release.TestRun_UNKNOWN) -} - -func (env *Environment) streamMessage(msg string, status release.TestRun_Status) error { - resp := &services.TestReleaseResponse{Msg: msg, Status: status} - return env.Stream.Send(resp) -} - -// DeleteTestPods deletes resources given in testManifests -func (env *Environment) DeleteTestPods(testManifests []string) { - for _, testManifest := range testManifests { - err := env.KubeClient.Delete(env.Namespace, bytes.NewBufferString(testManifest)) - if err != nil { - env.streamError(err.Error()) - } - } -} diff --git a/pkg/releasetesting/environment_test.go b/pkg/releasetesting/environment_test.go deleted file mode 100644 index 0199b74eb46..00000000000 --- a/pkg/releasetesting/environment_test.go +++ /dev/null @@ -1,182 +0,0 @@ -/* -Copyright 2017 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 releasetesting - -import ( - "bytes" - "errors" - "io" - "io/ioutil" - "testing" - - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - tillerEnv "k8s.io/helm/pkg/tiller/environment" -) - -func TestCreateTestPodSuccess(t *testing.T) { - env := testEnvFixture() - test := testFixture() - - err := env.createTestPod(test) - if err != nil { - t.Errorf("Expected no error, got an error: %s", err) - } -} - -func TestCreateTestPodFailure(t *testing.T) { - env := testEnvFixture() - env.KubeClient = newCreateFailingKubeClient() - test := testFixture() - - err := env.createTestPod(test) - if err == nil { - t.Errorf("Expected error, got no error") - } - - if test.result.Info == "" { - t.Errorf("Expected error to be saved in test result info but found empty string") - } - - if test.result.Status != release.TestRun_FAILURE { - t.Errorf("Expected test result status to be failure but got: %v", test.result.Status) - } -} - -func TestDeleteTestPods(t *testing.T) { - mockTestSuite := testSuiteFixture([]string{manifestWithTestSuccessHook}) - mockTestEnv := newMockTestingEnvironment() - mockTestEnv.KubeClient = newGetFailingKubeClient() - - mockTestEnv.DeleteTestPods(mockTestSuite.TestManifests) - - stream := mockTestEnv.Stream.(*mockStream) - if len(stream.messages) != 0 { - t.Errorf("Expected 0 errors, got at least one: %v", stream.messages) - } - - for _, testManifest := range mockTestSuite.TestManifests { - if _, err := mockTestEnv.KubeClient.Get(mockTestEnv.Namespace, bytes.NewBufferString(testManifest)); err == nil { - t.Error("Expected error, got nil") - } - } -} - -func TestDeleteTestPodsFailingDelete(t *testing.T) { - mockTestSuite := testSuiteFixture([]string{manifestWithTestSuccessHook}) - mockTestEnv := newMockTestingEnvironment() - mockTestEnv.KubeClient = newDeleteFailingKubeClient() - - mockTestEnv.DeleteTestPods(mockTestSuite.TestManifests) - - stream := mockTestEnv.Stream.(*mockStream) - if len(stream.messages) != 1 { - t.Errorf("Expected 1 error, got: %v", len(stream.messages)) - } -} - -func TestStreamMessage(t *testing.T) { - mockTestEnv := newMockTestingEnvironment() - - expectedMessage := "testing streamMessage" - expectedStatus := release.TestRun_SUCCESS - err := mockTestEnv.streamMessage(expectedMessage, expectedStatus) - if err != nil { - t.Errorf("Expected no errors, got 1: %s", err) - } - - stream := mockTestEnv.Stream.(*mockStream) - if len(stream.messages) != 1 { - t.Errorf("Expected 1 message, got: %v", len(stream.messages)) - } - - if stream.messages[0].Msg != expectedMessage { - t.Errorf("Expected message: %s, got: %s", expectedMessage, stream.messages[0]) - } - if stream.messages[0].Status != expectedStatus { - t.Errorf("Expected status: %v, got: %v", expectedStatus, stream.messages[0].Status) - } -} - -type MockTestingEnvironment struct { - *Environment -} - -func newMockTestingEnvironment() *MockTestingEnvironment { - tEnv := mockTillerEnvironment() - - return &MockTestingEnvironment{ - Environment: &Environment{ - Namespace: "default", - KubeClient: tEnv.KubeClient, - Timeout: 5, - Stream: &mockStream{}, - }, - } -} - -func (mte MockTestingEnvironment) streamRunning(name string) error { return nil } -func (mte MockTestingEnvironment) streamError(info string) error { return nil } -func (mte MockTestingEnvironment) streamFailed(name string) error { return nil } -func (mte MockTestingEnvironment) streamSuccess(name string) error { return nil } -func (mte MockTestingEnvironment) streamUnknown(name, info string) error { return nil } -func (mte MockTestingEnvironment) streamMessage(msg string, status release.TestRun_Status) error { - mte.Stream.Send(&services.TestReleaseResponse{Msg: msg, Status: status}) - return nil -} - -type getFailingKubeClient struct { - tillerEnv.PrintingKubeClient -} - -func newGetFailingKubeClient() *getFailingKubeClient { - return &getFailingKubeClient{ - PrintingKubeClient: tillerEnv.PrintingKubeClient{Out: ioutil.Discard}, - } -} - -func (p *getFailingKubeClient) Get(ns string, r io.Reader) (string, error) { - return "", errors.New("in the end, they did not find Nemo") -} - -type deleteFailingKubeClient struct { - tillerEnv.PrintingKubeClient -} - -func newDeleteFailingKubeClient() *deleteFailingKubeClient { - return &deleteFailingKubeClient{ - PrintingKubeClient: tillerEnv.PrintingKubeClient{Out: ioutil.Discard}, - } -} - -func (p *deleteFailingKubeClient) Delete(ns string, r io.Reader) error { - return errors.New("delete failed") -} - -type createFailingKubeClient struct { - tillerEnv.PrintingKubeClient -} - -func newCreateFailingKubeClient() *createFailingKubeClient { - return &createFailingKubeClient{ - PrintingKubeClient: tillerEnv.PrintingKubeClient{Out: ioutil.Discard}, - } -} - -func (p *createFailingKubeClient) Create(ns string, r io.Reader, t int64, shouldWait bool) error { - return errors.New("We ran out of budget and couldn't create finding-nemo") -} diff --git a/pkg/releasetesting/test_suite.go b/pkg/releasetesting/test_suite.go deleted file mode 100644 index 2e42400ce22..00000000000 --- a/pkg/releasetesting/test_suite.go +++ /dev/null @@ -1,193 +0,0 @@ -/* -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 releasetesting - -import ( - "fmt" - "strings" - - "github.com/ghodss/yaml" - "github.com/golang/protobuf/ptypes/timestamp" - "k8s.io/kubernetes/pkg/apis/core" - - "k8s.io/helm/pkg/hooks" - "k8s.io/helm/pkg/proto/hapi/release" - util "k8s.io/helm/pkg/releaseutil" - "k8s.io/helm/pkg/timeconv" -) - -// TestSuite what tests are run, results, and metadata -type TestSuite struct { - StartedAt *timestamp.Timestamp - CompletedAt *timestamp.Timestamp - TestManifests []string - Results []*release.TestRun -} - -type test struct { - manifest string - expectedSuccess bool - result *release.TestRun -} - -// NewTestSuite takes a release object and returns a TestSuite object with test definitions -// extracted from the release -func NewTestSuite(rel *release.Release) (*TestSuite, error) { - testManifests, err := extractTestManifestsFromHooks(rel.Hooks) - if err != nil { - return nil, err - } - - results := []*release.TestRun{} - - return &TestSuite{ - TestManifests: testManifests, - Results: results, - }, nil -} - -// Run executes tests in a test suite and stores a result within a given environment -func (ts *TestSuite) Run(env *Environment) error { - ts.StartedAt = timeconv.Now() - - if len(ts.TestManifests) == 0 { - // TODO: make this better, adding test run status on test suite is weird - env.streamMessage("No Tests Found", release.TestRun_UNKNOWN) - } - - for _, testManifest := range ts.TestManifests { - test, err := newTest(testManifest) - if err != nil { - return err - } - - test.result.StartedAt = timeconv.Now() - if err := env.streamRunning(test.result.Name); err != nil { - return err - } - test.result.Status = release.TestRun_RUNNING - - resourceCreated := true - if err := env.createTestPod(test); err != nil { - resourceCreated = false - if streamErr := env.streamError(test.result.Info); streamErr != nil { - return err - } - } - - resourceCleanExit := true - status := core.PodUnknown - if resourceCreated { - status, err = env.getTestPodStatus(test) - if err != nil { - resourceCleanExit = false - if streamErr := env.streamError(test.result.Info); streamErr != nil { - return streamErr - } - } - } - - if resourceCreated && resourceCleanExit { - if err := test.assignTestResult(status); err != nil { - return err - } - - if err := env.streamResult(test.result); err != nil { - return err - } - } - - test.result.CompletedAt = timeconv.Now() - ts.Results = append(ts.Results, test.result) - } - - ts.CompletedAt = timeconv.Now() - return nil -} - -func (t *test) assignTestResult(podStatus core.PodPhase) error { - switch podStatus { - case core.PodSucceeded: - if t.expectedSuccess { - t.result.Status = release.TestRun_SUCCESS - } else { - t.result.Status = release.TestRun_FAILURE - } - case core.PodFailed: - if !t.expectedSuccess { - t.result.Status = release.TestRun_SUCCESS - } else { - t.result.Status = release.TestRun_FAILURE - } - default: - t.result.Status = release.TestRun_UNKNOWN - } - - return nil -} - -func expectedSuccess(hookTypes []string) (bool, error) { - for _, hookType := range hookTypes { - hookType = strings.ToLower(strings.TrimSpace(hookType)) - if hookType == hooks.ReleaseTestSuccess { - return true, nil - } else if hookType == hooks.ReleaseTestFailure { - return false, nil - } - } - return false, fmt.Errorf("No %s or %s hook found", hooks.ReleaseTestSuccess, hooks.ReleaseTestFailure) -} - -func extractTestManifestsFromHooks(h []*release.Hook) ([]string, error) { - testHooks := hooks.FilterTestHooks(h) - - tests := []string{} - for _, h := range testHooks { - individualTests := util.SplitManifests(h.Manifest) - for _, t := range individualTests { - tests = append(tests, t) - } - } - return tests, nil -} - -func newTest(testManifest string) (*test, error) { - var sh util.SimpleHead - err := yaml.Unmarshal([]byte(testManifest), &sh) - if err != nil { - return nil, err - } - - if sh.Kind != "Pod" { - return nil, fmt.Errorf("%s is not a pod", sh.Metadata.Name) - } - - hookTypes := sh.Metadata.Annotations[hooks.HookAnno] - expected, err := expectedSuccess(strings.Split(hookTypes, ",")) - if err != nil { - return nil, err - } - - name := strings.TrimSuffix(sh.Metadata.Name, ",") - return &test{ - manifest: testManifest, - expectedSuccess: expected, - result: &release.TestRun{ - Name: name, - }, - }, nil -} diff --git a/pkg/releasetesting/test_suite_test.go b/pkg/releasetesting/test_suite_test.go deleted file mode 100644 index e6cc8bcf59f..00000000000 --- a/pkg/releasetesting/test_suite_test.go +++ /dev/null @@ -1,343 +0,0 @@ -/* -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 releasetesting - -import ( - "io" - "io/ioutil" - "testing" - "time" - - "github.com/golang/protobuf/ptypes/timestamp" - "golang.org/x/net/context" - grpc "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - "k8s.io/kubernetes/pkg/apis/core" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/storage" - "k8s.io/helm/pkg/storage/driver" - tillerEnv "k8s.io/helm/pkg/tiller/environment" -) - -const manifestWithTestSuccessHook = ` -apiVersion: v1 -kind: Pod -metadata: - name: finding-nemo, - annotations: - "helm.sh/hook": test-success -spec: - containers: - - name: nemo-test - image: fake-image - cmd: fake-command -` - -const manifestWithTestFailureHook = ` -apiVersion: v1 -kind: Pod -metadata: - name: gold-rush, - annotations: - "helm.sh/hook": test-failure -spec: - containers: - - name: gold-finding-test - image: fake-gold-finding-image - cmd: fake-gold-finding-command -` -const manifestWithInstallHooks = `apiVersion: v1 -kind: ConfigMap -metadata: - name: test-cm - annotations: - "helm.sh/hook": post-install,pre-delete -data: - name: value -` - -func TestNewTestSuite(t *testing.T) { - rel := releaseStub() - - _, err := NewTestSuite(rel) - if err != nil { - t.Errorf("%s", err) - } -} - -func TestRun(t *testing.T) { - - testManifests := []string{manifestWithTestSuccessHook, manifestWithTestFailureHook} - ts := testSuiteFixture(testManifests) - if err := ts.Run(testEnvFixture()); err != nil { - t.Errorf("%s", err) - } - - if ts.StartedAt == nil { - t.Errorf("Expected StartedAt to not be nil. Got: %v", ts.StartedAt) - } - - if ts.CompletedAt == nil { - t.Errorf("Expected CompletedAt to not be nil. Got: %v", ts.CompletedAt) - } - - if len(ts.Results) != 2 { - t.Errorf("Expected 2 test result. Got %v", len(ts.Results)) - } - - result := ts.Results[0] - if result.StartedAt == nil { - t.Errorf("Expected test StartedAt to not be nil. Got: %v", result.StartedAt) - } - - if result.CompletedAt == nil { - t.Errorf("Expected test CompletedAt to not be nil. Got: %v", result.CompletedAt) - } - - if result.Name != "finding-nemo" { - t.Errorf("Expected test name to be finding-nemo. Got: %v", result.Name) - } - - if result.Status != release.TestRun_SUCCESS { - t.Errorf("Expected test result to be successful, got: %v", result.Status) - } - - result2 := ts.Results[1] - if result2.StartedAt == nil { - t.Errorf("Expected test StartedAt to not be nil. Got: %v", result2.StartedAt) - } - - if result2.CompletedAt == nil { - t.Errorf("Expected test CompletedAt to not be nil. Got: %v", result2.CompletedAt) - } - - if result2.Name != "gold-rush" { - t.Errorf("Expected test name to be gold-rush, Got: %v", result2.Name) - } - - if result2.Status != release.TestRun_FAILURE { - t.Errorf("Expected test result to be successful, got: %v", result2.Status) - } - -} - -func TestRunEmptyTestSuite(t *testing.T) { - ts := testSuiteFixture([]string{}) - mockTestEnv := testEnvFixture() - if err := ts.Run(mockTestEnv); err != nil { - t.Errorf("%s", err) - } - - if ts.StartedAt == nil { - t.Errorf("Expected StartedAt to not be nil. Got: %v", ts.StartedAt) - } - - if ts.CompletedAt == nil { - t.Errorf("Expected CompletedAt to not be nil. Got: %v", ts.CompletedAt) - } - - if len(ts.Results) != 0 { - t.Errorf("Expected 0 test result. Got %v", len(ts.Results)) - } - - stream := mockTestEnv.Stream.(*mockStream) - if len(stream.messages) == 0 { - t.Errorf("Expected at least one message, Got: %v", len(stream.messages)) - } else { - msg := stream.messages[0].Msg - if msg != "No Tests Found" { - t.Errorf("Expected message 'No Tests Found', Got: %v", msg) - } - } - -} - -func TestRunSuccessWithTestFailureHook(t *testing.T) { - ts := testSuiteFixture([]string{manifestWithTestFailureHook}) - env := testEnvFixture() - env.KubeClient = newPodFailedKubeClient() - if err := ts.Run(env); err != nil { - t.Errorf("%s", err) - } - - if ts.StartedAt == nil { - t.Errorf("Expected StartedAt to not be nil. Got: %v", ts.StartedAt) - } - - if ts.CompletedAt == nil { - t.Errorf("Expected CompletedAt to not be nil. Got: %v", ts.CompletedAt) - } - - if len(ts.Results) != 1 { - t.Errorf("Expected 1 test result. Got %v", len(ts.Results)) - } - - result := ts.Results[0] - if result.StartedAt == nil { - t.Errorf("Expected test StartedAt to not be nil. Got: %v", result.StartedAt) - } - - if result.CompletedAt == nil { - t.Errorf("Expected test CompletedAt to not be nil. Got: %v", result.CompletedAt) - } - - if result.Name != "gold-rush" { - t.Errorf("Expected test name to be gold-rush, Got: %v", result.Name) - } - - if result.Status != release.TestRun_SUCCESS { - t.Errorf("Expected test result to be successful, got: %v", result.Status) - } -} - -func TestExtractTestManifestsFromHooks(t *testing.T) { - rel := releaseStub() - testManifests, err := extractTestManifestsFromHooks(rel.Hooks) - if err != nil { - t.Errorf("Expected no error, Got: %s", err) - } - - if len(testManifests) != 1 { - t.Errorf("Expected 1 test manifest, Got: %v", len(testManifests)) - } -} - -func chartStub() *chart.Chart { - return &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "nemo", - }, - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithTestSuccessHook)}, - }, - } -} - -func releaseStub() *release.Release { - date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0} - return &release.Release{ - Name: "lost-fish", - Info: &release.Info{ - FirstDeployed: &date, - LastDeployed: &date, - Status: &release.Status{Code: release.Status_DEPLOYED}, - Description: "a release stub", - }, - Chart: chartStub(), - Config: &chart.Config{Raw: `name: value`}, - Version: 1, - Hooks: []*release.Hook{ - { - Name: "finding-nemo", - Kind: "Pod", - Path: "finding-nemo", - Manifest: manifestWithTestSuccessHook, - Events: []release.Hook_Event{ - release.Hook_RELEASE_TEST_SUCCESS, - }, - }, - { - Name: "test-cm", - Kind: "ConfigMap", - Path: "test-cm", - Manifest: manifestWithInstallHooks, - Events: []release.Hook_Event{ - release.Hook_POST_INSTALL, - release.Hook_PRE_DELETE, - }, - }, - }, - } -} - -func testFixture() *test { - return &test{ - manifest: manifestWithTestSuccessHook, - result: &release.TestRun{}, - } -} - -func testSuiteFixture(testManifests []string) *TestSuite { - testResults := []*release.TestRun{} - ts := &TestSuite{ - TestManifests: testManifests, - Results: testResults, - } - - return ts -} - -func testEnvFixture() *Environment { - return newMockTestingEnvironment().Environment -} - -func mockTillerEnvironment() *tillerEnv.Environment { - e := tillerEnv.New() - e.Releases = storage.Init(driver.NewMemory()) - e.KubeClient = newPodSucceededKubeClient() - return e -} - -type mockStream struct { - stream grpc.ServerStream - messages []*services.TestReleaseResponse -} - -func (rs *mockStream) Send(m *services.TestReleaseResponse) error { - rs.messages = append(rs.messages, m) - return nil -} - -func (rs mockStream) SetHeader(m metadata.MD) error { return nil } -func (rs mockStream) SendHeader(m metadata.MD) error { return nil } -func (rs mockStream) SetTrailer(m metadata.MD) {} -func (rs mockStream) SendMsg(v interface{}) error { return nil } -func (rs mockStream) RecvMsg(v interface{}) error { return nil } -func (rs mockStream) Context() context.Context { return helm.NewContext() } - -type podSucceededKubeClient struct { - tillerEnv.PrintingKubeClient -} - -func newPodSucceededKubeClient() *podSucceededKubeClient { - return &podSucceededKubeClient{ - PrintingKubeClient: tillerEnv.PrintingKubeClient{Out: ioutil.Discard}, - } -} - -func (p *podSucceededKubeClient) WaitAndGetCompletedPodPhase(ns string, r io.Reader, timeout time.Duration) (core.PodPhase, error) { - return core.PodSucceeded, nil -} - -type podFailedKubeClient struct { - tillerEnv.PrintingKubeClient -} - -func newPodFailedKubeClient() *podFailedKubeClient { - return &podFailedKubeClient{ - PrintingKubeClient: tillerEnv.PrintingKubeClient{Out: ioutil.Discard}, - } -} - -func (p *podFailedKubeClient) WaitAndGetCompletedPodPhase(ns string, r io.Reader, timeout time.Duration) (core.PodPhase, error) { - return core.PodFailed, nil -} diff --git a/pkg/releaseutil/filter.go b/pkg/releaseutil/filter.go index fdd2cc38107..dbd0df8e2dc 100644 --- a/pkg/releaseutil/filter.go +++ b/pkg/releaseutil/filter.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil // import "k8s.io/helm/pkg/releaseutil" +package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" -import rspb "k8s.io/helm/pkg/proto/hapi/release" +import rspb "helm.sh/helm/v3/pkg/release" // FilterFunc returns true if the release object satisfies // the predicate of the underlying filter func. @@ -68,11 +68,11 @@ func All(filters ...FilterFunc) FilterFunc { } // StatusFilter filters a set of releases by status code. -func StatusFilter(status rspb.Status_Code) FilterFunc { +func StatusFilter(status rspb.Status) FilterFunc { return FilterFunc(func(rls *rspb.Release) bool { if rls == nil { return true } - return rls.GetInfo().GetStatus().Code == status + return rls.Info.Status == status }) } diff --git a/pkg/releaseutil/filter_test.go b/pkg/releaseutil/filter_test.go index 5909523638c..31ac306f634 100644 --- a/pkg/releaseutil/filter_test.go +++ b/pkg/releaseutil/filter_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,33 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil // import "k8s.io/helm/pkg/releaseutil" +package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" import ( "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) func TestFilterAny(t *testing.T) { - ls := Any(StatusFilter(rspb.Status_DELETED)).Filter(releases) + ls := Any(StatusFilter(rspb.StatusUninstalled)).Filter(releases) if len(ls) != 2 { t.Fatalf("expected 2 results, got '%d'", len(ls)) } r0, r1 := ls[0], ls[1] switch { - case r0.Info.Status.Code != rspb.Status_DELETED: - t.Fatalf("expected DELETED result, got '%s'", r1.Info.Status.Code) - case r1.Info.Status.Code != rspb.Status_DELETED: - t.Fatalf("expected DELETED result, got '%s'", r1.Info.Status.Code) + case r0.Info.Status != rspb.StatusUninstalled: + t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) + case r1.Info.Status != rspb.StatusUninstalled: + t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) } } func TestFilterAll(t *testing.T) { fn := FilterFunc(func(rls *rspb.Release) bool { - // true if not deleted and version < 4 - v0 := !StatusFilter(rspb.Status_DELETED).Check(rls) + // true if not uninstalled and version < 4 + v0 := !StatusFilter(rspb.StatusUninstalled).Check(rls) v1 := rls.Version < 4 return v0 && v1 }) @@ -53,7 +53,7 @@ func TestFilterAll(t *testing.T) { switch r0 := ls[0]; { case r0.Version == 4: t.Fatal("got release with status revision 4") - case r0.Info.Status.Code == rspb.Status_DELETED: - t.Fatal("got release with status DELTED") + case r0.Info.Status == rspb.StatusUninstalled: + t.Fatal("got release with status UNINSTALLED") } } diff --git a/pkg/releaseutil/kind_sorter.go b/pkg/releaseutil/kind_sorter.go new file mode 100644 index 00000000000..a340dfc2912 --- /dev/null +++ b/pkg/releaseutil/kind_sorter.go @@ -0,0 +1,156 @@ +/* +Copyright The Helm Authors. + +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 releaseutil + +import ( + "sort" + + "helm.sh/helm/v3/pkg/release" +) + +// KindSortOrder is an ordering of Kinds. +type KindSortOrder []string + +// InstallOrder is the order in which manifests should be installed (by Kind). +// +// Those occurring earlier in the list get installed before those occurring later in the list. +var InstallOrder KindSortOrder = []string{ + "Namespace", + "NetworkPolicy", + "ResourceQuota", + "LimitRange", + "PodSecurityPolicy", + "PodDisruptionBudget", + "ServiceAccount", + "Secret", + "SecretList", + "ConfigMap", + "StorageClass", + "PersistentVolume", + "PersistentVolumeClaim", + "CustomResourceDefinition", + "ClusterRole", + "ClusterRoleList", + "ClusterRoleBinding", + "ClusterRoleBindingList", + "Role", + "RoleList", + "RoleBinding", + "RoleBindingList", + "Service", + "DaemonSet", + "Pod", + "ReplicationController", + "ReplicaSet", + "Deployment", + "HorizontalPodAutoscaler", + "StatefulSet", + "Job", + "CronJob", + "Ingress", + "APIService", +} + +// UninstallOrder is the order in which manifests should be uninstalled (by Kind). +// +// Those occurring earlier in the list get uninstalled before those occurring later in the list. +var UninstallOrder KindSortOrder = []string{ + "APIService", + "Ingress", + "Service", + "CronJob", + "Job", + "StatefulSet", + "HorizontalPodAutoscaler", + "Deployment", + "ReplicaSet", + "ReplicationController", + "Pod", + "DaemonSet", + "RoleBindingList", + "RoleBinding", + "RoleList", + "Role", + "ClusterRoleBindingList", + "ClusterRoleBinding", + "ClusterRoleList", + "ClusterRole", + "CustomResourceDefinition", + "PersistentVolumeClaim", + "PersistentVolume", + "StorageClass", + "ConfigMap", + "SecretList", + "Secret", + "ServiceAccount", + "PodDisruptionBudget", + "PodSecurityPolicy", + "LimitRange", + "ResourceQuota", + "NetworkPolicy", + "Namespace", +} + +// sort manifests by kind. +// +// Results are sorted by 'ordering', keeping order of items with equal kind/priority +func sortManifestsByKind(manifests []Manifest, ordering KindSortOrder) []Manifest { + sort.SliceStable(manifests, func(i, j int) bool { + return lessByKind(manifests[i], manifests[j], manifests[i].Head.Kind, manifests[j].Head.Kind, ordering) + }) + + return manifests +} + +// sort hooks by kind, using an out-of-place sort to preserve the input parameters. +// +// Results are sorted by 'ordering', keeping order of items with equal kind/priority +func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.Hook { + h := hooks + sort.SliceStable(h, func(i, j int) bool { + return lessByKind(h[i], h[j], h[i].Kind, h[j].Kind, ordering) + }) + + return h +} + +func lessByKind(a interface{}, b interface{}, kindA string, kindB string, o KindSortOrder) bool { + ordering := make(map[string]int, len(o)) + for v, k := range o { + ordering[k] = v + } + + first, aok := ordering[kindA] + second, bok := ordering[kindB] + + if !aok && !bok { + // if both are unknown then sort alphabetically by kind, keep original order if same kind + if kindA != kindB { + return kindA < kindB + } + return first < second + } + // unknown kind is last + if !aok { + return false + } + if !bok { + return true + } + // sort different kinds, keep original order if same priority + return first < second +} diff --git a/pkg/releaseutil/kind_sorter_test.go b/pkg/releaseutil/kind_sorter_test.go new file mode 100644 index 00000000000..71d3552106d --- /dev/null +++ b/pkg/releaseutil/kind_sorter_test.go @@ -0,0 +1,331 @@ +/* +Copyright The Helm Authors. + +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 releaseutil + +import ( + "bytes" + "testing" + + "helm.sh/helm/v3/pkg/release" +) + +func TestKindSorter(t *testing.T) { + manifests := []Manifest{ + { + Name: "E", + Head: &SimpleHead{Kind: "SecretList"}, + }, + { + Name: "i", + Head: &SimpleHead{Kind: "ClusterRole"}, + }, + { + Name: "I", + Head: &SimpleHead{Kind: "ClusterRoleList"}, + }, + { + Name: "j", + Head: &SimpleHead{Kind: "ClusterRoleBinding"}, + }, + { + Name: "J", + Head: &SimpleHead{Kind: "ClusterRoleBindingList"}, + }, + { + Name: "f", + Head: &SimpleHead{Kind: "ConfigMap"}, + }, + { + Name: "u", + Head: &SimpleHead{Kind: "CronJob"}, + }, + { + Name: "2", + Head: &SimpleHead{Kind: "CustomResourceDefinition"}, + }, + { + Name: "n", + Head: &SimpleHead{Kind: "DaemonSet"}, + }, + { + Name: "r", + Head: &SimpleHead{Kind: "Deployment"}, + }, + { + Name: "!", + Head: &SimpleHead{Kind: "HonkyTonkSet"}, + }, + { + Name: "v", + Head: &SimpleHead{Kind: "Ingress"}, + }, + { + Name: "t", + Head: &SimpleHead{Kind: "Job"}, + }, + { + Name: "c", + Head: &SimpleHead{Kind: "LimitRange"}, + }, + { + Name: "a", + Head: &SimpleHead{Kind: "Namespace"}, + }, + { + Name: "A", + Head: &SimpleHead{Kind: "NetworkPolicy"}, + }, + { + Name: "g", + Head: &SimpleHead{Kind: "PersistentVolume"}, + }, + { + Name: "h", + Head: &SimpleHead{Kind: "PersistentVolumeClaim"}, + }, + { + Name: "o", + Head: &SimpleHead{Kind: "Pod"}, + }, + { + Name: "3", + Head: &SimpleHead{Kind: "PodDisruptionBudget"}, + }, + { + Name: "C", + Head: &SimpleHead{Kind: "PodSecurityPolicy"}, + }, + { + Name: "q", + Head: &SimpleHead{Kind: "ReplicaSet"}, + }, + { + Name: "p", + Head: &SimpleHead{Kind: "ReplicationController"}, + }, + { + Name: "b", + Head: &SimpleHead{Kind: "ResourceQuota"}, + }, + { + Name: "k", + Head: &SimpleHead{Kind: "Role"}, + }, + { + Name: "K", + Head: &SimpleHead{Kind: "RoleList"}, + }, + { + Name: "l", + Head: &SimpleHead{Kind: "RoleBinding"}, + }, + { + Name: "L", + Head: &SimpleHead{Kind: "RoleBindingList"}, + }, + { + Name: "e", + Head: &SimpleHead{Kind: "Secret"}, + }, + { + Name: "m", + Head: &SimpleHead{Kind: "Service"}, + }, + { + Name: "d", + Head: &SimpleHead{Kind: "ServiceAccount"}, + }, + { + Name: "s", + Head: &SimpleHead{Kind: "StatefulSet"}, + }, + { + Name: "1", + Head: &SimpleHead{Kind: "StorageClass"}, + }, + { + Name: "w", + Head: &SimpleHead{Kind: "APIService"}, + }, + { + Name: "x", + Head: &SimpleHead{Kind: "HorizontalPodAutoscaler"}, + }, + } + + for _, test := range []struct { + description string + order KindSortOrder + expected string + }{ + {"install", InstallOrder, "aAbcC3deEf1gh2iIjJkKlLmnopqrxstuvw!"}, + {"uninstall", UninstallOrder, "wvmutsxrqponLlKkJjIi2hg1fEed3CcbAa!"}, + } { + var buf bytes.Buffer + t.Run(test.description, func(t *testing.T) { + if got, want := len(test.expected), len(manifests); got != want { + t.Fatalf("Expected %d names in order, got %d", want, got) + } + defer buf.Reset() + orig := manifests + for _, r := range sortManifestsByKind(manifests, test.order) { + buf.WriteString(r.Name) + } + if got := buf.String(); got != test.expected { + t.Errorf("Expected %q, got %q", test.expected, got) + } + for i, manifest := range orig { + if manifest != manifests[i] { + t.Fatal("Expected input to sortManifestsByKind to stay the same") + } + } + }) + } +} + +// TestKindSorterKeepOriginalOrder verifies manifests of same kind are kept in original order +func TestKindSorterKeepOriginalOrder(t *testing.T) { + manifests := []Manifest{ + { + Name: "a", + Head: &SimpleHead{Kind: "ClusterRole"}, + }, + { + Name: "A", + Head: &SimpleHead{Kind: "ClusterRole"}, + }, + { + Name: "0", + Head: &SimpleHead{Kind: "ConfigMap"}, + }, + { + Name: "1", + Head: &SimpleHead{Kind: "ConfigMap"}, + }, + { + Name: "z", + Head: &SimpleHead{Kind: "ClusterRoleBinding"}, + }, + { + Name: "!", + Head: &SimpleHead{Kind: "ClusterRoleBinding"}, + }, + { + Name: "u2", + Head: &SimpleHead{Kind: "Unknown"}, + }, + { + Name: "u1", + Head: &SimpleHead{Kind: "Unknown"}, + }, + { + Name: "t3", + Head: &SimpleHead{Kind: "Unknown2"}, + }, + } + for _, test := range []struct { + description string + order KindSortOrder + expected string + }{ + // expectation is sorted by kind (unknown is last) and within each group of same kind, the order is kept + {"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", InstallOrder, "01aAz!u2u1t3"}, + } { + var buf bytes.Buffer + t.Run(test.description, func(t *testing.T) { + defer buf.Reset() + for _, r := range sortManifestsByKind(manifests, test.order) { + buf.WriteString(r.Name) + } + if got := buf.String(); got != test.expected { + t.Errorf("Expected %q, got %q", test.expected, got) + } + }) + } +} + +func TestKindSorterNamespaceAgainstUnknown(t *testing.T) { + unknown := Manifest{ + Name: "a", + Head: &SimpleHead{Kind: "Unknown"}, + } + namespace := Manifest{ + Name: "b", + Head: &SimpleHead{Kind: "Namespace"}, + } + + manifests := []Manifest{unknown, namespace} + manifests = sortManifestsByKind(manifests, InstallOrder) + + expectedOrder := []Manifest{namespace, unknown} + for i, manifest := range manifests { + if expectedOrder[i].Name != manifest.Name { + t.Errorf("Expected %s, got %s", expectedOrder[i].Name, manifest.Name) + } + } +} + +// test hook sorting with a small subset of kinds, since it uses the same algorithm as sortManifestsByKind +func TestKindSorterForHooks(t *testing.T) { + hooks := []*release.Hook{ + { + Name: "i", + Kind: "ClusterRole", + }, + { + Name: "j", + Kind: "ClusterRoleBinding", + }, + { + Name: "c", + Kind: "LimitRange", + }, + { + Name: "a", + Kind: "Namespace", + }, + } + + for _, test := range []struct { + description string + order KindSortOrder + expected string + }{ + {"install", InstallOrder, "acij"}, + {"uninstall", UninstallOrder, "jica"}, + } { + var buf bytes.Buffer + t.Run(test.description, func(t *testing.T) { + if got, want := len(test.expected), len(hooks); got != want { + t.Fatalf("Expected %d names in order, got %d", want, got) + } + defer buf.Reset() + orig := hooks + for _, r := range sortHooksByKind(hooks, test.order) { + buf.WriteString(r.Name) + } + for i, hook := range orig { + if hook != hooks[i] { + t.Fatal("Expected input to sortHooksByKind to stay the same") + } + } + if got := buf.String(); got != test.expected { + t.Errorf("Expected %q, got %q", test.expected, got) + } + }) + } +} diff --git a/pkg/releaseutil/manifest.go b/pkg/releaseutil/manifest.go index a0449cc551c..0b04a4599b2 100644 --- a/pkg/releaseutil/manifest.go +++ b/pkg/releaseutil/manifest.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package releaseutil import ( "fmt" "regexp" + "strconv" "strings" ) @@ -37,8 +38,9 @@ var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*") // SplitManifests takes a string of manifest and returns a map contains individual manifests func SplitManifests(bigFile string) map[string]string { // Basically, we're quickly splitting a stream of YAML documents into an - // array of YAML docs. In the current implementation, the file name is just - // a place holder, and doesn't have any further meaning. + // array of YAML docs. The file name is just a place holder, but should be + // integer-sortable so that manifests get output in the same order as the + // input (see `BySplitManifestsOrder`). tpl := "manifest-%d" res := map[string]string{} // Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly. @@ -46,7 +48,6 @@ func SplitManifests(bigFile string) map[string]string { docs := sep.Split(bigFileTmp, -1) var count int for _, d := range docs { - if d == "" { continue } @@ -57,3 +58,15 @@ func SplitManifests(bigFile string) map[string]string { } return res } + +// BySplitManifestsOrder sorts by in-file manifest order, as provided in function `SplitManifests` +type BySplitManifestsOrder []string + +func (a BySplitManifestsOrder) Len() int { return len(a) } +func (a BySplitManifestsOrder) Less(i, j int) bool { + // Split `manifest-%d` + anum, _ := strconv.ParseInt(a[i][len("manifest-"):], 10, 0) + bnum, _ := strconv.ParseInt(a[j][len("manifest-"):], 10, 0) + return anum < bnum +} +func (a BySplitManifestsOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] } diff --git a/pkg/releaseutil/manifest_sorter.go b/pkg/releaseutil/manifest_sorter.go new file mode 100644 index 00000000000..e834145000c --- /dev/null +++ b/pkg/releaseutil/manifest_sorter.go @@ -0,0 +1,233 @@ +/* +Copyright The Helm Authors. + +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 releaseutil + +import ( + "log" + "path" + "sort" + "strconv" + "strings" + + "github.com/pkg/errors" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/release" +) + +// Manifest represents a manifest file, which has a name and some content. +type Manifest struct { + Name string + Content string + Head *SimpleHead +} + +// manifestFile represents a file that contains a manifest. +type manifestFile struct { + entries map[string]string + path string + apis chartutil.VersionSet +} + +// result is an intermediate structure used during sorting. +type result struct { + hooks []*release.Hook + generic []Manifest +} + +// TODO: Refactor this out. It's here because naming conventions were not followed through. +// So fix the Test hook names and then remove this. +var events = map[string]release.HookEvent{ + release.HookPreInstall.String(): release.HookPreInstall, + release.HookPostInstall.String(): release.HookPostInstall, + release.HookPreDelete.String(): release.HookPreDelete, + release.HookPostDelete.String(): release.HookPostDelete, + release.HookPreUpgrade.String(): release.HookPreUpgrade, + release.HookPostUpgrade.String(): release.HookPostUpgrade, + release.HookPreRollback.String(): release.HookPreRollback, + release.HookPostRollback.String(): release.HookPostRollback, + release.HookTest.String(): release.HookTest, + // Support test-success for backward compatibility with Helm 2 tests + "test-success": release.HookTest, +} + +// SortManifests takes a map of filename/YAML contents, splits the file +// by manifest entries, and sorts the entries 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. +// +// 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, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) { + result := &result{} + + var sortedFilePaths []string + for filePath := range files { + sortedFilePaths = append(sortedFilePaths, filePath) + } + sort.Strings(sortedFilePaths) + + for _, filePath := range sortedFilePaths { + content := files[filePath] + + // Skip partials. We could return these as a separate map, but there doesn't + // seem to be any need for that at this time. + if strings.HasPrefix(path.Base(filePath), "_") { + continue + } + // Skip empty files and log this. + if strings.TrimSpace(content) == "" { + continue + } + + manifestFile := &manifestFile{ + entries: SplitManifests(content), + path: filePath, + apis: apis, + } + + if err := manifestFile.sort(result); err != nil { + return result.hooks, result.generic, err + } + } + + return sortHooksByKind(result.hooks, ordering), sortManifestsByKind(result.generic, ordering), nil +} + +// sort takes a manifestFile object which may contain multiple resource definition +// entries and sorts each entry by hook types, and saves the resulting hooks and +// generic manifests (or non-hooks) to the result struct. +// +// To determine hook type, it looks for a YAML structure like this: +// +// kind: SomeKind +// apiVersion: v1 +// metadata: +// 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 { + // Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys) + var sortedEntryKeys []string + for entryKey := range file.entries { + sortedEntryKeys = append(sortedEntryKeys, entryKey) + } + sort.Sort(BySplitManifestsOrder(sortedEntryKeys)) + + for _, entryKey := range sortedEntryKeys { + m := file.entries[entryKey] + + var entry SimpleHead + if err := yaml.Unmarshal([]byte(m), &entry); err != nil { + return errors.Wrapf(err, "YAML parse error on %s", file.path) + } + + if !hasAnyAnnotation(entry) { + result.generic = append(result.generic, Manifest{ + Name: file.path, + Content: m, + Head: &entry, + }) + continue + } + + hookTypes, ok := entry.Metadata.Annotations[release.HookAnnotation] + if !ok { + result.generic = append(result.generic, Manifest{ + Name: file.path, + Content: m, + Head: &entry, + }) + continue + } + + hw := calculateHookWeight(entry) + + h := &release.Hook{ + Name: entry.Metadata.Name, + Kind: entry.Kind, + Path: file.path, + Manifest: m, + Events: []release.HookEvent{}, + Weight: hw, + DeletePolicies: []release.HookDeletePolicy{}, + } + + isUnknownHook := false + for _, hookType := range strings.Split(hookTypes, ",") { + hookType = strings.ToLower(strings.TrimSpace(hookType)) + e, ok := events[hookType] + if !ok { + isUnknownHook = true + break + } + h.Events = append(h.Events, e) + } + + if isUnknownHook { + log.Printf("info: skipping unknown hook: %q", hookTypes) + continue + } + + result.hooks = append(result.hooks, h) + + operateAnnotationValues(entry, release.HookDeleteAnnotation, func(value string) { + h.DeletePolicies = append(h.DeletePolicies, release.HookDeletePolicy(value)) + }) + } + + return nil +} + +// hasAnyAnnotation returns true if the given entry has any annotations at all. +func hasAnyAnnotation(entry SimpleHead) bool { + return entry.Metadata != nil && + entry.Metadata.Annotations != nil && + len(entry.Metadata.Annotations) != 0 +} + +// calculateHookWeight finds the weight in the hook weight annotation. +// +// If no weight is found, the assigned weight is 0 +func calculateHookWeight(entry SimpleHead) int { + hws := entry.Metadata.Annotations[release.HookWeightAnnotation] + hw, err := strconv.Atoi(hws) + if err != nil { + hw = 0 + } + return hw +} + +// operateAnnotationValues finds the given annotation and runs the operate function with the value of that annotation +func operateAnnotationValues(entry SimpleHead, annotation string, operate func(p string)) { + if dps, ok := entry.Metadata.Annotations[annotation]; ok { + for _, dp := range strings.Split(dps, ",") { + dp = strings.ToLower(strings.TrimSpace(dp)) + operate(dp) + } + } +} diff --git a/pkg/releaseutil/manifest_sorter_test.go b/pkg/releaseutil/manifest_sorter_test.go new file mode 100644 index 00000000000..20d809317e8 --- /dev/null +++ b/pkg/releaseutil/manifest_sorter_test.go @@ -0,0 +1,228 @@ +/* +Copyright The Helm Authors. + +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 releaseutil + +import ( + "reflect" + "testing" + + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/release" +) + +func TestSortManifests(t *testing.T) { + + data := []struct { + name []string + path string + kind []string + hooks map[string][]release.HookEvent + manifest string + }{ + { + name: []string{"first"}, + path: "one", + kind: []string{"Job"}, + hooks: map[string][]release.HookEvent{"first": {release.HookPreInstall}}, + manifest: `apiVersion: v1 +kind: Job +metadata: + name: first + labels: + doesnot: matter + annotations: + "helm.sh/hook": pre-install +`, + }, + { + name: []string{"second"}, + path: "two", + kind: []string{"ReplicaSet"}, + hooks: map[string][]release.HookEvent{"second": {release.HookPostInstall}}, + manifest: `kind: ReplicaSet +apiVersion: v1beta1 +metadata: + name: second + annotations: + "helm.sh/hook": post-install +`, + }, { + name: []string{"third"}, + path: "three", + kind: []string{"ReplicaSet"}, + hooks: map[string][]release.HookEvent{"third": nil}, + manifest: `kind: ReplicaSet +apiVersion: v1beta1 +metadata: + name: third + annotations: + "helm.sh/hook": no-such-hook +`, + }, { + name: []string{"fourth"}, + path: "four", + kind: []string{"Pod"}, + hooks: map[string][]release.HookEvent{"fourth": nil}, + manifest: `kind: Pod +apiVersion: v1 +metadata: + name: fourth + annotations: + nothing: here`, + }, { + name: []string{"fifth"}, + path: "five", + kind: []string{"ReplicaSet"}, + hooks: map[string][]release.HookEvent{"fifth": {release.HookPostDelete, release.HookPostInstall}}, + manifest: `kind: ReplicaSet +apiVersion: v1beta1 +metadata: + name: fifth + annotations: + "helm.sh/hook": post-delete, post-install +`, + }, { + // Regression test: files with an underscore in the base name should be skipped. + name: []string{"sixth"}, + path: "six/_six", + kind: []string{"ReplicaSet"}, + hooks: map[string][]release.HookEvent{"sixth": nil}, + manifest: `invalid manifest`, // This will fail if partial is not skipped. + }, { + // Regression test: files with no content should be skipped. + name: []string{"seventh"}, + path: "seven", + kind: []string{"ReplicaSet"}, + hooks: map[string][]release.HookEvent{"seventh": nil}, + manifest: "", + }, + { + name: []string{"eighth", "example-test"}, + path: "eight", + kind: []string{"ConfigMap", "Pod"}, + hooks: map[string][]release.HookEvent{"eighth": nil, "example-test": {release.HookTest}}, + manifest: `kind: ConfigMap +apiVersion: v1 +metadata: + name: eighth +data: + name: value +--- +apiVersion: v1 +kind: Pod +metadata: + name: example-test + annotations: + "helm.sh/hook": test +`, + }, + } + + manifests := make(map[string]string, len(data)) + for _, o := range data { + manifests[o.path] = o.manifest + } + + hs, generic, err := SortManifests(manifests, chartutil.VersionSet{"v1", "v1beta1"}, InstallOrder) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + // This test will fail if 'six' or 'seven' was added. + if len(generic) != 2 { + t.Errorf("Expected 2 generic manifests, got %d", len(generic)) + } + + if len(hs) != 4 { + t.Errorf("Expected 4 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) + } + nameFound := false + for _, expectedName := range expect.name { + if out.Name == expectedName { + nameFound = true + } + } + if !nameFound { + t.Errorf("Got unexpected name %s", out.Name) + } + kindFound := false + for _, expectedKind := range expect.kind { + if out.Kind == expectedKind { + kindFound = true + } + } + if !kindFound { + t.Errorf("Got unexpected kind %s", out.Kind) + } + + expectedHooks := expect.hooks[out.Name] + if !reflect.DeepEqual(expectedHooks, out.Events) { + t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events) + } + + } + } + if !found { + t.Errorf("Result not found: %v", out) + } + } + + // Verify the sort order + sorted := []Manifest{} + for _, s := range data { + manifests := SplitManifests(s.manifest) + + for _, m := range manifests { + var sh SimpleHead + if err := yaml.Unmarshal([]byte(m), &sh); err != nil { + // This is expected for manifests that are corrupt or empty. + t.Log(err) + continue + } + + name := sh.Metadata.Name + + // only keep track of non-hook manifests + if s.hooks[name] == nil { + another := Manifest{ + Content: m, + Name: name, + Head: &sh, + } + sorted = append(sorted, another) + } + } + } + + sorted = sortManifestsByKind(sorted, InstallOrder) + for i, m := range generic { + if m.Content != sorted[i].Content { + t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content) + } + } +} diff --git a/pkg/releaseutil/manifest_test.go b/pkg/releaseutil/manifest_test.go index 7906279adf7..8664d20ef80 100644 --- a/pkg/releaseutil/manifest_test.go +++ b/pkg/releaseutil/manifest_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil // import "k8s.io/helm/pkg/releaseutil" +package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" import ( "reflect" "testing" ) -const manifestFile = ` +const mockManifestFile = ` --- apiVersion: v1 @@ -29,7 +29,7 @@ kind: Pod metadata: name: finding-nemo, annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test spec: containers: - name: nemo-test @@ -42,7 +42,7 @@ kind: Pod metadata: name: finding-nemo, annotations: - "helm.sh/hook": test-success + "helm.sh/hook": test spec: containers: - name: nemo-test @@ -50,7 +50,7 @@ spec: cmd: fake-command` func TestSplitManifest(t *testing.T) { - manifests := SplitManifests(manifestFile) + manifests := SplitManifests(mockManifestFile) if len(manifests) != 1 { t.Errorf("Expected 1 manifest, got %v", len(manifests)) } diff --git a/pkg/releaseutil/sorter.go b/pkg/releaseutil/sorter.go index 1b744d72cb1..1a8aa78a621 100644 --- a/pkg/releaseutil/sorter.go +++ b/pkg/releaseutil/sorter.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,22 +14,42 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil // import "k8s.io/helm/pkg/releaseutil" +package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" import ( "sort" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) -type sorter struct { - list []*rspb.Release - less func(int, int) bool +type list []*rspb.Release + +func (s list) Len() int { return len(s) } +func (s list) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// ByName sorts releases by name +type ByName struct{ list } + +// Less compares to releases +func (s ByName) Less(i, j int) bool { return s.list[i].Name < s.list[j].Name } + +// ByDate sorts releases by date +type ByDate struct{ list } + +// Less compares to releases +func (s ByDate) Less(i, j int) bool { + ti := s.list[i].Info.LastDeployed.Unix() + tj := s.list[j].Info.LastDeployed.Unix() + return ti < tj } -func (s *sorter) Len() int { return len(s.list) } -func (s *sorter) Less(i, j int) bool { return s.less(i, j) } -func (s *sorter) Swap(i, j int) { s.list[i], s.list[j] = s.list[j], s.list[i] } +// ByRevision sorts releases by revision number +type ByRevision struct{ list } + +// Less compares to releases +func (s ByRevision) Less(i, j int) bool { + return s.list[i].Version < s.list[j].Version +} // Reverse reverses the list of releases sorted by the sort func. func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) { @@ -42,36 +62,17 @@ func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) { // SortByName returns the list of releases sorted // in lexicographical order. func SortByName(list []*rspb.Release) { - s := &sorter{list: list} - s.less = func(i, j int) bool { - ni := s.list[i].Name - nj := s.list[j].Name - return ni < nj - } - sort.Sort(s) + sort.Sort(ByName{list}) } // SortByDate returns the list of releases sorted by a // release's last deployed time (in seconds). func SortByDate(list []*rspb.Release) { - s := &sorter{list: list} - - s.less = func(i, j int) bool { - ti := s.list[i].Info.LastDeployed.Seconds - tj := s.list[j].Info.LastDeployed.Seconds - return ti < tj - } - sort.Sort(s) + sort.Sort(ByDate{list}) } // SortByRevision returns the list of releases sorted by a // release's revision number (release.Version). func SortByRevision(list []*rspb.Release) { - s := &sorter{list: list} - s.less = func(i, j int) bool { - vi := s.list[i].Version - vj := s.list[j].Version - return vi < vj - } - sort.Sort(s) + sort.Sort(ByRevision{list}) } diff --git a/pkg/releaseutil/sorter_test.go b/pkg/releaseutil/sorter_test.go index 7d4e31e2e28..9544d2018d4 100644 --- a/pkg/releaseutil/sorter_test.go +++ b/pkg/releaseutil/sorter_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,28 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil // import "k8s.io/helm/pkg/releaseutil" +package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" import ( "testing" "time" - rspb "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/timeconv" + rspb "helm.sh/helm/v3/pkg/release" + helmtime "helm.sh/helm/v3/pkg/time" ) // note: this test data is shared with filter_test.go. var releases = []*rspb.Release{ - tsRelease("quiet-bear", 2, 2000, rspb.Status_SUPERSEDED), - tsRelease("angry-bird", 4, 3000, rspb.Status_DEPLOYED), - tsRelease("happy-cats", 1, 4000, rspb.Status_DELETED), - tsRelease("vocal-dogs", 3, 6000, rspb.Status_DELETED), + tsRelease("quiet-bear", 2, 2000, rspb.StatusSuperseded), + tsRelease("angry-bird", 4, 3000, rspb.StatusDeployed), + tsRelease("happy-cats", 1, 4000, rspb.StatusUninstalled), + tsRelease("vocal-dogs", 3, 6000, rspb.StatusUninstalled), } -func tsRelease(name string, vers int32, dur time.Duration, code rspb.Status_Code) *rspb.Release { - tmsp := timeconv.Timestamp(time.Now().Add(time.Duration(dur))) - info := &rspb.Info{Status: &rspb.Status{Code: code}, LastDeployed: tmsp} +func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rspb.Release { + info := &rspb.Info{Status: status, LastDeployed: helmtime.Now().Add(dur)} return &rspb.Release{ Name: name, Version: vers, @@ -65,8 +64,8 @@ func TestSortByDate(t *testing.T) { SortByDate(releases) check(t, "ByDate", func(i, j int) bool { - ti := releases[i].Info.LastDeployed.Seconds - tj := releases[j].Info.LastDeployed.Seconds + ti := releases[i].Info.LastDeployed.Second() + tj := releases[j].Info.LastDeployed.Second() return ti < tj }) } @@ -80,3 +79,30 @@ func TestSortByRevision(t *testing.T) { return vi < vj }) } + +func TestReverseSortByName(t *testing.T) { + Reverse(releases, SortByName) + check(t, "ByName", func(i, j int) bool { + ni := releases[i].Name + nj := releases[j].Name + return ni > nj + }) +} + +func TestReverseSortByDate(t *testing.T) { + Reverse(releases, SortByDate) + check(t, "ByDate", func(i, j int) bool { + ti := releases[i].Info.LastDeployed.Second() + tj := releases[j].Info.LastDeployed.Second() + return ti > tj + }) +} + +func TestReverseSortByRevision(t *testing.T) { + Reverse(releases, SortByRevision) + check(t, "ByRevision", func(i, j int) bool { + vi := releases[i].Version + vj := releases[j].Version + return vi > vj + }) +} diff --git a/pkg/repo/chartrepo.go b/pkg/repo/chartrepo.go index bf03a68bb5f..09b94fd42b7 100644 --- a/pkg/repo/chartrepo.go +++ b/pkg/repo/chartrepo.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,33 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ -package repo // import "k8s.io/helm/pkg/repo" +package repo // import "helm.sh/helm/v3/pkg/repo" import ( + "crypto/rand" + "encoding/base64" + "encoding/json" "fmt" "io/ioutil" + "log" "net/url" "os" + "path" "path/filepath" "strings" - "github.com/ghodss/yaml" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/provenance" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/provenance" ) // Entry represents a collection of parameters for chart repository type Entry struct { - Name string `json:"name"` - Cache string `json:"cache"` - URL string `json:"url"` - Username string `json:"username"` - Password string `json:"password"` - CertFile string `json:"certFile"` - KeyFile string `json:"keyFile"` - CAFile string `json:"caFile"` + Name string `json:"name"` + URL string `json:"url"` + Username string `json:"username"` + Password string `json:"password"` + CertFile string `json:"certFile"` + KeyFile string `json:"keyFile"` + CAFile string `json:"caFile"` + InsecureSkipTLSverify bool `json:"insecure_skip_tls_verify"` } // ChartRepository represents a chart repository @@ -49,41 +56,41 @@ type ChartRepository struct { ChartPaths []string IndexFile *IndexFile Client getter.Getter + CachePath string } // NewChartRepository constructs ChartRepository func NewChartRepository(cfg *Entry, getters getter.Providers) (*ChartRepository, error) { u, err := url.Parse(cfg.URL) if err != nil { - return nil, fmt.Errorf("invalid chart URL format: %s", cfg.URL) + return nil, errors.Errorf("invalid chart URL format: %s", cfg.URL) } - getterConstructor, err := getters.ByScheme(u.Scheme) + client, err := getters.ByScheme(u.Scheme) if err != nil { - return nil, fmt.Errorf("Could not find protocol handler for: %s", u.Scheme) - } - client, err := getterConstructor(cfg.URL, cfg.CertFile, cfg.KeyFile, cfg.CAFile) - if err != nil { - return nil, fmt.Errorf("Could not construct protocol handler for: %s error: %v", u.Scheme, err) + return nil, errors.Errorf("could not find protocol handler for: %s", u.Scheme) } return &ChartRepository{ Config: cfg, IndexFile: NewIndexFile(), Client: client, + CachePath: helmpath.CachePath("repository"), }, nil } // Load loads a directory of charts as if it were a repository. // // It requires the presence of an index.yaml file in the directory. +// +// Deprecated: remove in Helm 4. func (r *ChartRepository) Load() error { dirInfo, err := os.Stat(r.Config.Name) if err != nil { return err } if !dirInfo.IsDir() { - return fmt.Errorf("%q is not a directory", r.Config.Name) + return errors.Errorf("%q is not a directory", r.Config.Name) } // FIXME: Why are we recursively walking directories? @@ -94,7 +101,7 @@ func (r *ChartRepository) Load() error { if strings.Contains(f.Name(), "-index.yaml") { i, err := LoadIndexFile(path) if err != nil { - return nil + return err } r.IndexFile = i } else if strings.HasSuffix(f.Name(), ".tgz") { @@ -107,49 +114,49 @@ func (r *ChartRepository) Load() error { } // DownloadIndexFile fetches the index from a repository. -// -// cachePath is prepended to any index that does not have an absolute path. This -// is for pre-2.2.0 repo files. -func (r *ChartRepository) DownloadIndexFile(cachePath string) error { - var indexURL string +func (r *ChartRepository) DownloadIndexFile() (string, error) { parsedURL, err := url.Parse(r.Config.URL) if err != nil { - return err - } - parsedURL.Path = strings.TrimSuffix(parsedURL.Path, "/") + "/index.yaml" - - indexURL = parsedURL.String() - g, err := getter.NewHTTPGetter(indexURL, r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile) - if err != nil { - return err + return "", err } - g.SetCredentials(r.Config.Username, r.Config.Password) - resp, err := g.Get(indexURL) + parsedURL.RawPath = path.Join(parsedURL.RawPath, "index.yaml") + parsedURL.Path = path.Join(parsedURL.Path, "index.yaml") + + indexURL := parsedURL.String() + // TODO add user-agent + resp, err := r.Client.Get(indexURL, + getter.WithURL(r.Config.URL), + getter.WithInsecureSkipVerifyTLS(r.Config.InsecureSkipTLSverify), + getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile), + getter.WithBasicAuth(r.Config.Username, r.Config.Password), + ) if err != nil { - return err + return "", err } index, err := ioutil.ReadAll(resp) if err != nil { - return err + return "", err } - if _, err := loadIndex(index); err != nil { - return err + indexFile, err := loadIndex(index, r.Config.URL) + if err != nil { + return "", err } - // In Helm 2.2.0 the config.cache was accidentally switched to an absolute - // path, which broke backward compatibility. This fixes it by prepending a - // global cache path to relative paths. - // - // It is changed on DownloadIndexFile because that was the method that - // originally carried the cache path. - cp := r.Config.Cache - if !filepath.IsAbs(cp) { - cp = filepath.Join(cachePath, cp) + // Create the chart list file in the cache directory + var charts strings.Builder + for name := range indexFile.Entries { + fmt.Fprintln(&charts, name) } - - return ioutil.WriteFile(cp, index, 0644) + chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) + os.MkdirAll(filepath.Dir(chartsFile), 0755) + ioutil.WriteFile(chartsFile, []byte(charts.String()), 0644) + + // Create the index file in the cache directory + fname := filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name)) + os.MkdirAll(filepath.Dir(fname), 0755) + return fname, ioutil.WriteFile(fname, index, 0644) } // Index generates an index for the chart repository and writes an index.yaml file. @@ -171,7 +178,7 @@ func (r *ChartRepository) saveIndexFile() error { func (r *ChartRepository) generateIndex() error { for _, path := range r.ChartPaths { - ch, err := chartutil.Load(path) + ch, err := loader.Load(path) if err != nil { return err } @@ -181,8 +188,10 @@ func (r *ChartRepository) generateIndex() error { return err } - if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) { - r.IndexFile.Add(ch.Metadata, path, r.Config.URL, digest) + if !r.IndexFile.Has(ch.Name(), ch.Metadata.Version) { + if err := r.IndexFile.MustAdd(ch.Metadata, path, r.Config.URL, digest); err != nil { + return errors.Wrapf(err, "failed adding to %s to index", path) + } } // TODO: If a chart exists, but has a different Digest, should we error? } @@ -200,32 +209,41 @@ func FindChartInRepoURL(repoURL, chartName, chartVersion, certFile, keyFile, caF // without adding repo to repositories, like FindChartInRepoURL, // but it also receives credentials for the chart repository. func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) { + return FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, false, getters) +} + +// FindChartInAuthAndTLSRepoURL finds chart in chart repository pointed by repoURL +// without adding repo to repositories, like FindChartInRepoURL, +// but it also receives credentials and TLS verify flag for the chart repository. +// TODO Helm 4, FindChartInAuthAndTLSRepoURL should be integrated into FindChartInAuthRepoURL. +func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify bool, getters getter.Providers) (string, error) { // Download and write the index file to a temporary location - tempIndexFile, err := ioutil.TempFile("", "tmp-repo-file") - if err != nil { - return "", fmt.Errorf("cannot write index file for repository requested") - } - defer os.Remove(tempIndexFile.Name()) + buf := make([]byte, 20) + rand.Read(buf) + name := strings.ReplaceAll(base64.StdEncoding.EncodeToString(buf), "/", "-") c := Entry{ - URL: repoURL, - Username: username, - Password: password, - CertFile: certFile, - KeyFile: keyFile, - CAFile: caFile, + URL: repoURL, + Username: username, + Password: password, + CertFile: certFile, + KeyFile: keyFile, + CAFile: caFile, + Name: name, + InsecureSkipTLSverify: insecureSkipTLSverify, } r, err := NewChartRepository(&c, getters) if err != nil { return "", err } - if err := r.DownloadIndexFile(tempIndexFile.Name()); err != nil { - return "", fmt.Errorf("Looks like %q is not a valid chart repository or cannot be reached: %s", repoURL, err) + idx, err := r.DownloadIndexFile() + if err != nil { + return "", errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", repoURL) } // Read the index file for the repository to get chart information and return chart URL - repoIndex, err := LoadIndexFile(tempIndexFile.Name()) + repoIndex, err := LoadIndexFile(idx) if err != nil { return "", err } @@ -236,18 +254,18 @@ func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion } cv, err := repoIndex.Get(chartName, chartVersion) if err != nil { - return "", fmt.Errorf("%s not found in %s repository", errMsg, repoURL) + return "", errors.Errorf("%s not found in %s repository", errMsg, repoURL) } if len(cv.URLs) == 0 { - return "", fmt.Errorf("%s has no downloadable URLs", errMsg) + return "", errors.Errorf("%s has no downloadable URLs", errMsg) } chartURL := cv.URLs[0] absoluteChartURL, err := ResolveReferenceURL(repoURL, chartURL) if err != nil { - return "", fmt.Errorf("failed to make chart URL absolute: %v", err) + return "", errors.Wrap(err, "failed to make chart URL absolute") } return absoluteChartURL, nil @@ -258,13 +276,23 @@ func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion func ResolveReferenceURL(baseURL, refURL string) (string, error) { parsedBaseURL, err := url.Parse(baseURL) if err != nil { - return "", fmt.Errorf("failed to parse %s as URL: %v", baseURL, err) + return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL) } parsedRefURL, err := url.Parse(refURL) if err != nil { - return "", fmt.Errorf("failed to parse %s as URL: %v", refURL, err) + return "", errors.Wrapf(err, "failed to parse %s as URL", refURL) } + // We need a trailing slash for ResolveReference to work, but make sure there isn't already one + parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/" return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil } + +func (e *Entry) String() string { + buf, err := json.Marshal(e) + if err != nil { + log.Panic(err) + } + return string(buf) +} diff --git a/pkg/repo/chartrepo_test.go b/pkg/repo/chartrepo_test.go index 948ee12d340..7bd563460ee 100644 --- a/pkg/repo/chartrepo_test.go +++ b/pkg/repo/chartrepo_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ limitations under the License. package repo import ( + "bytes" "io/ioutil" "net/http" "net/http/httptest" @@ -27,9 +28,12 @@ import ( "testing" "time" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/proto/hapi/chart" + "sigs.k8s.io/yaml" + + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" ) const ( @@ -41,7 +45,7 @@ func TestLoadChartRepository(t *testing.T) { r, err := NewChartRepository(&Entry{ Name: testRepository, URL: testURL, - }, getter.All(environment.EnvSettings{})) + }, getter.All(&cli.EnvSettings{})) if err != nil { t.Errorf("Problem creating chart repository from %s: %v", testRepository, err) } @@ -74,7 +78,7 @@ func TestIndex(t *testing.T) { r, err := NewChartRepository(&Entry{ Name: testRepository, URL: testURL, - }, getter.All(environment.EnvSettings{})) + }, getter.All(&cli.EnvSettings{})) if err != nil { t.Errorf("Problem creating chart repository from %s: %v", testRepository, err) } @@ -108,9 +112,68 @@ func TestIndex(t *testing.T) { verifyIndex(t, second) } +type CustomGetter struct { + repoUrls []string +} + +func (g *CustomGetter) Get(href string, options ...getter.Option) (*bytes.Buffer, error) { + index := &IndexFile{ + APIVersion: "v1", + Generated: time.Now(), + } + indexBytes, err := yaml.Marshal(index) + if err != nil { + return nil, err + } + g.repoUrls = append(g.repoUrls, href) + return bytes.NewBuffer(indexBytes), nil +} + +func TestIndexCustomSchemeDownload(t *testing.T) { + repoName := "gcs-repo" + repoURL := "gs://some-gcs-bucket" + myCustomGetter := &CustomGetter{} + customGetterConstructor := func(options ...getter.Option) (getter.Getter, error) { + return myCustomGetter, nil + } + providers := getter.Providers{{ + Schemes: []string{"gs"}, + New: customGetterConstructor, + }} + repo, err := NewChartRepository(&Entry{ + Name: repoName, + URL: repoURL, + }, providers) + if err != nil { + t.Fatalf("Problem loading chart repository from %s: %v", repoURL, err) + } + repo.CachePath = ensure.TempDir(t) + defer os.RemoveAll(repo.CachePath) + + tempIndexFile, err := ioutil.TempFile("", "test-repo") + if err != nil { + t.Fatalf("Failed to create temp index file: %v", err) + } + defer os.Remove(tempIndexFile.Name()) + + idx, err := repo.DownloadIndexFile() + if err != nil { + t.Fatalf("Failed to download index file to %s: %v", idx, err) + } + + if len(myCustomGetter.repoUrls) != 1 { + t.Fatalf("Custom Getter.Get should be called once") + } + + expectedRepoIndexURL := repoURL + "/index.yaml" + if myCustomGetter.repoUrls[0] != expectedRepoIndexURL { + t.Fatalf("Custom Getter.Get should be called with %s", expectedRepoIndexURL) + } +} + func verifyIndex(t *testing.T, actual *IndexFile) { var empty time.Time - if actual.Generated == empty { + if actual.Generated.Equal(empty) { t.Errorf("Generated should be greater than 0: %s", actual.Generated) } @@ -175,12 +238,12 @@ func verifyIndex(t *testing.T, actual *IndexFile) { t.Errorf("Expected %q, got %q", e.Version, g.Version) } if len(g.Keywords) != 3 { - t.Error("Expected 3 keyrwords.") + t.Error("Expected 3 keywords.") } if len(g.Maintainers) != 2 { t.Error("Expected 2 maintainers.") } - if g.Created == empty { + if g.Created.Equal(empty) { t.Error("Expected created to be non-empty") } if g.Description == "" { @@ -214,6 +277,44 @@ func startLocalServerForTests(handler http.Handler) (*httptest.Server, error) { return httptest.NewServer(handler), nil } +// startLocalTLSServerForTests Start the local helm server with TLS +func startLocalTLSServerForTests(handler http.Handler) (*httptest.Server, error) { + if handler == nil { + fileBytes, err := ioutil.ReadFile("testdata/local-index.yaml") + if err != nil { + return nil, err + } + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(fileBytes) + }) + } + + return httptest.NewTLSServer(handler), nil +} + +func TestFindChartInAuthAndTLSRepoURL(t *testing.T) { + srv, err := startLocalTLSServerForTests(nil) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + chartURL, err := FindChartInAuthAndTLSRepoURL(srv.URL, "", "", "nginx", "", "", "", "", true, getter.All(&cli.EnvSettings{})) + if err != nil { + t.Fatalf("%v", err) + } + if chartURL != "https://charts.helm.sh/stable/nginx-0.2.0.tgz" { + t.Errorf("%s is not the valid URL", chartURL) + } + + // If the insecureSkipTLsverify is false, it will return an error that contains "x509: certificate signed by unknown authority". + _, err = FindChartInAuthAndTLSRepoURL(srv.URL, "", "", "nginx", "0.1.0", "", "", "", false, getter.All(&cli.EnvSettings{})) + + if !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { + t.Errorf("Expected TLS error for function FindChartInAuthAndTLSRepoURL not found, but got a different error (%v)", err) + } +} + func TestFindChartInRepoURL(t *testing.T) { srv, err := startLocalServerForTests(nil) if err != nil { @@ -221,29 +322,32 @@ func TestFindChartInRepoURL(t *testing.T) { } defer srv.Close() - chartURL, err := FindChartInRepoURL(srv.URL, "nginx", "", "", "", "", getter.All(environment.EnvSettings{})) + chartURL, err := FindChartInRepoURL(srv.URL, "nginx", "", "", "", "", getter.All(&cli.EnvSettings{})) if err != nil { - t.Errorf("%s", err) + t.Fatalf("%v", err) } - if chartURL != "https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz" { + if chartURL != "https://charts.helm.sh/stable/nginx-0.2.0.tgz" { t.Errorf("%s is not the valid URL", chartURL) } - chartURL, err = FindChartInRepoURL(srv.URL, "nginx", "0.1.0", "", "", "", getter.All(environment.EnvSettings{})) + chartURL, err = FindChartInRepoURL(srv.URL, "nginx", "0.1.0", "", "", "", getter.All(&cli.EnvSettings{})) if err != nil { t.Errorf("%s", err) } - if chartURL != "https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz" { + if chartURL != "https://charts.helm.sh/stable/nginx-0.1.0.tgz" { t.Errorf("%s is not the valid URL", chartURL) } } func TestErrorFindChartInRepoURL(t *testing.T) { - _, err := FindChartInRepoURL("http://someserver/something", "nginx", "", "", "", "", getter.All(environment.EnvSettings{})) - if err == nil { + + g := getter.All(&cli.EnvSettings{ + RepositoryCache: ensure.TempDir(t), + }) + + if _, err := FindChartInRepoURL("http://someserver/something", "nginx", "", "", "", "", g); err == nil { t.Errorf("Expected error for bad chart URL, but did not get any errors") - } - if err != nil && !strings.Contains(err.Error(), `Looks like "http://someserver/something" is not a valid chart repository or cannot be reached: Get http://someserver/something/index.yaml`) { + } else if !strings.Contains(err.Error(), `looks like "http://someserver/something" is not a valid chart repository or cannot be reached`) { t.Errorf("Expected error for bad chart URL, but got a different error (%v)", err) } @@ -253,27 +357,21 @@ func TestErrorFindChartInRepoURL(t *testing.T) { } defer srv.Close() - _, err = FindChartInRepoURL(srv.URL, "nginx1", "", "", "", "", getter.All(environment.EnvSettings{})) - if err == nil { + if _, err = FindChartInRepoURL(srv.URL, "nginx1", "", "", "", "", g); err == nil { t.Errorf("Expected error for chart not found, but did not get any errors") - } - if err != nil && err.Error() != `chart "nginx1" not found in `+srv.URL+` repository` { + } else if err.Error() != `chart "nginx1" not found in `+srv.URL+` repository` { t.Errorf("Expected error for chart not found, but got a different error (%v)", err) } - _, err = FindChartInRepoURL(srv.URL, "nginx1", "0.1.0", "", "", "", getter.All(environment.EnvSettings{})) - if err == nil { + if _, err = FindChartInRepoURL(srv.URL, "nginx1", "0.1.0", "", "", "", g); err == nil { t.Errorf("Expected error for chart not found, but did not get any errors") - } - if err != nil && err.Error() != `chart "nginx1" version "0.1.0" not found in `+srv.URL+` repository` { + } else if err.Error() != `chart "nginx1" version "0.1.0" not found in `+srv.URL+` repository` { t.Errorf("Expected error for chart not found, but got a different error (%v)", err) } - _, err = FindChartInRepoURL(srv.URL, "chartWithNoURL", "", "", "", "", getter.All(environment.EnvSettings{})) - if err == nil { + if _, err = FindChartInRepoURL(srv.URL, "chartWithNoURL", "", "", "", "", g); err == nil { t.Errorf("Expected error for no chart URLs available, but did not get any errors") - } - if err != nil && err.Error() != `chart "chartWithNoURL" has no downloadable URLs` { + } else if err.Error() != `chart "chartWithNoURL" has no downloadable URLs` { t.Errorf("Expected error for chart not found, but got a different error (%v)", err) } } @@ -287,11 +385,19 @@ func TestResolveReferenceURL(t *testing.T) { t.Errorf("%s", chartURL) } - chartURL, err = ResolveReferenceURL("http://localhost:8123", "https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz") + chartURL, err = ResolveReferenceURL("http://localhost:8123/charts-with-no-trailing-slash", "nginx-0.2.0.tgz") + if err != nil { + t.Errorf("%s", err) + } + if chartURL != "http://localhost:8123/charts-with-no-trailing-slash/nginx-0.2.0.tgz" { + t.Errorf("%s", chartURL) + } + + chartURL, err = ResolveReferenceURL("http://localhost:8123", "https://charts.helm.sh/stable/nginx-0.2.0.tgz") if err != nil { t.Errorf("%s", err) } - if chartURL != "https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz" { + if chartURL != "https://charts.helm.sh/stable/nginx-0.2.0.tgz" { t.Errorf("%s", chartURL) } } diff --git a/pkg/repo/doc.go b/pkg/repo/doc.go index fb8b3f4b2d3..05650100b9c 100644 --- a/pkg/repo/doc.go +++ b/pkg/repo/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ The first is the 'index.yaml' format, which is expressed like this: entries: frobnitz: - created: 2016-09-29T12:14:34.830161306-06:00 - description: This is a frobniz. + description: This is a frobnitz. digest: 587bd19a9bd9d2bc4a6d25ab91c8c8e7042c47b4ac246e37bf8e1e74386190f4 home: http://example.com keywords: diff --git a/pkg/repo/index.go b/pkg/repo/index.go index 174ceea018b..e86b17349fe 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,23 +17,25 @@ limitations under the License. package repo import ( - "encoding/json" - "errors" - "fmt" + "bytes" "io/ioutil" + "log" "os" + "path" "path/filepath" "sort" "strings" "time" - "github.com/Masterminds/semver" - "github.com/ghodss/yaml" + "github.com/Masterminds/semver/v3" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/provenance" - "k8s.io/helm/pkg/urlutil" + "helm.sh/helm/v3/internal/fileutil" + "helm.sh/helm/v3/internal/urlutil" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/provenance" ) var indexPath = "index.yaml" @@ -76,10 +78,16 @@ func (c ChartVersions) Less(a, b int) bool { // IndexFile represents the index file in a chart repository type IndexFile struct { + // This is used ONLY for validation against chartmuseum's index files and is discarded after validation. + ServerInfo map[string]interface{} `json:"serverInfo,omitempty"` APIVersion string `json:"apiVersion"` Generated time.Time `json:"generated"` Entries map[string]ChartVersions `json:"entries"` PublicKeys []string `json:"publicKeys,omitempty"` + + // Annotations are additional mappings uninterpreted by Helm. They are made available for + // other applications to add information to the index file. + Annotations map[string]string `json:"annotations,omitempty"` } // NewIndexFile initializes an index. @@ -98,19 +106,30 @@ func LoadIndexFile(path string) (*IndexFile, error) { if err != nil { return nil, err } - return loadIndex(b) + i, err := loadIndex(b, path) + if err != nil { + return nil, errors.Wrapf(err, "error loading %s", path) + } + return i, nil } -// Add adds a file to the index +// MustAdd adds a file to the index // This can leave the index in an unsorted state -func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { +func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) error { + if md.APIVersion == "" { + md.APIVersion = chart.APIVersionV1 + } + if err := md.Validate(); err != nil { + return errors.Wrapf(err, "validate failed for %s", filename) + } + u := filename if baseURL != "" { - var err error _, file := filepath.Split(filename) + var err error u, err = urlutil.URLJoin(baseURL, file) if err != nil { - u = filepath.Join(baseURL, file) + u = path.Join(baseURL, file) } } cr := &ChartVersion{ @@ -119,10 +138,17 @@ func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { Digest: digest, Created: time.Now(), } - if ee, ok := i.Entries[md.Name]; !ok { - i.Entries[md.Name] = ChartVersions{cr} - } else { - i.Entries[md.Name] = append(ee, cr) + ee := i.Entries[md.Name] + i.Entries[md.Name] = append(ee, cr) + return nil +} + +// Add adds a file to the index and logs an error. +// +// Deprecated: Use index.MustAdd instead. +func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { + if err := i.MustAdd(md, filename, baseURL, digest); err != nil { + log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", md.Name, md.Version, filename, err) } } @@ -146,7 +172,8 @@ func (i IndexFile) SortEntries() { // Get returns the ChartVersion for the given name. // -// If version is empty, this will return the chart with the highest version. +// If version is empty, this will return the chart with the latest stable version, +// prerelease versions will be skipped. func (i IndexFile) Get(name, version string) (*ChartVersion, error) { vs, ok := i.Entries[name] if !ok { @@ -157,7 +184,7 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { } var constraint *semver.Constraints - if len(version) == 0 { + if version == "" { constraint, _ = semver.NewConstraint("*") } else { var err error @@ -167,6 +194,15 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { } } + // when customer input exact version, check whether have exact match one first + if len(version) != 0 { + for _, ver := range vs { + if version == ver.Version { + return ver, nil + } + } + } + for _, ver := range vs { test, err := semver.NewVersion(ver.Version) if err != nil { @@ -177,7 +213,7 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { return ver, nil } } - return nil, fmt.Errorf("No chart version found for %s-%s", name, version) + return nil, errors.Errorf("no chart version found for %s-%s", name, version) } // WriteFile writes an index file to the given destination path. @@ -188,7 +224,7 @@ func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { if err != nil { return err } - return ioutil.WriteFile(dest, b, mode) + return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode) } // Merge merges the given index file into this index. @@ -210,8 +246,6 @@ func (i *IndexFile) Merge(f *IndexFile) { } } -// Need both JSON and YAML annotations until we get rid of gopkg.in/yaml.v2 - // ChartVersion represents a chart entry in the IndexFile type ChartVersion struct { *chart.Metadata @@ -219,6 +253,23 @@ type ChartVersion struct { Created time.Time `json:"created,omitempty"` Removed bool `json:"removed,omitempty"` Digest string `json:"digest,omitempty"` + + // ChecksumDeprecated is deprecated in Helm 3, and therefore ignored. Helm 3 replaced + // this with Digest. However, with a strict YAML parser enabled, a field must be + // present on the struct for backwards compatibility. + ChecksumDeprecated string `json:"checksum,omitempty"` + + // EngineDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict + // YAML parser enabled, this field must be present. + EngineDeprecated string `json:"engine,omitempty"` + + // TillerVersionDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict + // YAML parser enabled, this field must be present. + TillerVersionDeprecated string `json:"tillerVersion,omitempty"` + + // URLDeprecated is deprecated in Helm 3, superseded by URLs. It is ignored. However, + // with a strict YAML parser enabled, this must be present on the struct. + URLDeprecated string `json:"url,omitempty"` } // IndexDirectory reads a (flat) directory and generates an index. @@ -246,12 +297,14 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) { var parentDir string parentDir, fname = filepath.Split(fname) + // filepath.Split appends an extra slash to the end of parentDir. We want to strip that out. + parentDir = strings.TrimSuffix(parentDir, string(os.PathSeparator)) parentURL, err := urlutil.URLJoin(baseURL, parentDir) if err != nil { - parentURL = filepath.Join(baseURL, parentDir) + parentURL = path.Join(baseURL, parentDir) } - c, err := chartutil.Load(arch) + c, err := loader.Load(arch) if err != nil { // Assume this is not a chart. continue @@ -260,70 +313,37 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) { if err != nil { return index, err } - index.Add(c.Metadata, fname, parentURL, hash) + if err := index.MustAdd(c.Metadata, fname, parentURL, hash); err != nil { + return index, errors.Wrapf(err, "failed adding to %s to index", fname) + } } return index, nil } // loadIndex loads an index file and does minimal validity checking. // +// The source parameter is only used for logging. // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. -func loadIndex(data []byte) (*IndexFile, error) { +func loadIndex(data []byte, source string) (*IndexFile, error) { i := &IndexFile{} - if err := yaml.Unmarshal(data, i); err != nil { + if err := yaml.UnmarshalStrict(data, i); err != nil { return i, err } - i.SortEntries() - if i.APIVersion == "" { - // When we leave Beta, we should remove legacy support and just - // return this error: - //return i, ErrNoAPIVersion - return loadUnversionedIndex(data) - } - return i, nil -} - -// unversionedEntry represents a deprecated pre-Alpha.5 format. -// -// This will be removed prior to v2.0.0 -type unversionedEntry struct { - Checksum string `json:"checksum"` - URL string `json:"url"` - Chartfile *chart.Metadata `json:"chartfile"` -} -// loadUnversionedIndex loads a pre-Alpha.5 index.yaml file. -// -// This format is deprecated. This function will be removed prior to v2.0.0. -func loadUnversionedIndex(data []byte) (*IndexFile, error) { - fmt.Fprintln(os.Stderr, "WARNING: Deprecated index file format. Try 'helm repo update'") - i := map[string]unversionedEntry{} - - // This gets around an error in the YAML parser. Instead of parsing as YAML, - // we convert to JSON, and then decode again. - var err error - data, err = yaml.YAMLToJSON(data) - if err != nil { - return nil, err - } - if err := json.Unmarshal(data, &i); err != nil { - return nil, err - } - - if len(i) == 0 { - return nil, ErrNoAPIVersion - } - ni := NewIndexFile() - for n, item := range i { - if item.Chartfile == nil || item.Chartfile.Name == "" { - parts := strings.Split(n, "-") - ver := "" - if len(parts) > 1 { - ver = strings.TrimSuffix(parts[1], ".tgz") + for name, cvs := range i.Entries { + for idx := len(cvs) - 1; idx >= 0; idx-- { + if cvs[idx].APIVersion == "" { + cvs[idx].APIVersion = chart.APIVersionV1 + } + if err := cvs[idx].Validate(); err != nil { + log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err) + cvs = append(cvs[:idx], cvs[idx+1:]...) } - item.Chartfile = &chart.Metadata{Name: parts[0], Version: ver} } - ni.Add(item.Chartfile, item.URL, "", item.Checksum) } - return ni, nil + i.SortEntries() + if i.APIVersion == "" { + return i, ErrNoAPIVersion + } + return i, nil } diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index ba426b17482..47f3c6b2e3f 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,36 +17,79 @@ limitations under the License. package repo import ( + "bufio" + "bytes" "io/ioutil" + "net/http" "os" "path/filepath" + "sort" + "strings" "testing" - "k8s.io/helm/pkg/getter" - "k8s.io/helm/pkg/helm/environment" - "k8s.io/helm/pkg/proto/hapi/chart" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/helmpath" ) const ( - testfile = "testdata/local-index.yaml" - unorderedTestfile = "testdata/local-index-unordered.yaml" - testRepo = "test-repo" + testfile = "testdata/local-index.yaml" + annotationstestfile = "testdata/local-index-annotations.yaml" + chartmuseumtestfile = "testdata/chartmuseum-index.yaml" + unorderedTestfile = "testdata/local-index-unordered.yaml" + testRepo = "test-repo" + indexWithDuplicates = ` +apiVersion: v1 +entries: + nginx: + - urls: + - https://charts.helm.sh/stable/nginx-0.2.0.tgz + name: nginx + description: string + version: 0.2.0 + home: https://github.com/something/else + digest: "sha256:1234567890abcdef" + nginx: + - urls: + - https://charts.helm.sh/stable/alpine-1.0.0.tgz + - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz + name: alpine + description: string + version: 1.0.0 + home: https://github.com/something + digest: "sha256:1234567890abcdef" +` ) func TestIndexFile(t *testing.T) { i := NewIndexFile() - i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") - i.Add(&chart.Metadata{Name: "cutter", Version: "0.1.1"}, "cutter-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc") - i.Add(&chart.Metadata{Name: "cutter", Version: "0.1.0"}, "cutter-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc") - i.Add(&chart.Metadata{Name: "cutter", Version: "0.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc") + for _, x := range []struct { + md *chart.Metadata + filename string + baseURL string + digest string + }{ + {&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"}, + {&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.1.1"}, "cutter-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc"}, + {&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.1.0"}, "cutter-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc"}, + {&chart.Metadata{APIVersion: "v2", Name: "cutter", Version: "0.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc"}, + {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+alpha"}, "setter-0.1.9+alpha.tgz", "http://example.com/charts", "sha256:1234567890abc"}, + {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.9+beta"}, "setter-0.1.9+beta.tgz", "http://example.com/charts", "sha256:1234567890abc"}, + } { + if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil { + t.Errorf("unexpected error adding to index: %s", err) + } + } + i.SortEntries() if i.APIVersion != APIVersionV1 { t.Error("Expected API version v1") } - if len(i.Entries) != 2 { - t.Errorf("Expected 2 charts. Got %d", len(i.Entries)) + if len(i.Entries) != 3 { + t.Errorf("Expected 3 charts. Got %d", len(i.Entries)) } if i.Entries["clipper"][0].Name != "clipper" { @@ -54,41 +97,78 @@ func TestIndexFile(t *testing.T) { } if len(i.Entries["cutter"]) != 3 { - t.Error("Expected two cutters.") + t.Error("Expected three cutters.") } // Test that the sort worked. 0.2 should be at the first index for Cutter. if v := i.Entries["cutter"][0].Version; v != "0.2.0" { t.Errorf("Unexpected first version: %s", v) } + + cv, err := i.Get("setter", "0.1.9") + if err == nil && !strings.Contains(cv.Metadata.Version, "0.1.9") { + t.Errorf("Unexpected version: %s", cv.Metadata.Version) + } + + cv, err = i.Get("setter", "0.1.9+alpha") + if err != nil || cv.Metadata.Version != "0.1.9+alpha" { + t.Errorf("Expected version: 0.1.9+alpha") + } } func TestLoadIndex(t *testing.T) { - b, err := ioutil.ReadFile(testfile) - if err != nil { - t.Fatal(err) + + tests := []struct { + Name string + Filename string + }{ + { + Name: "regular index file", + Filename: testfile, + }, + { + Name: "chartmuseum index file", + Filename: chartmuseumtestfile, + }, } - i, err := loadIndex(b) - if err != nil { - t.Fatal(err) + + for _, tc := range tests { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + i, err := LoadIndexFile(tc.Filename) + if err != nil { + t.Fatal(err) + } + verifyLocalIndex(t, i) + }) } - verifyLocalIndex(t, i) } -func TestLoadIndexFile(t *testing.T) { - i, err := LoadIndexFile(testfile) +// TestLoadIndex_Duplicates is a regression to make sure that we don't non-deterministically allow duplicate packages. +func TestLoadIndex_Duplicates(t *testing.T) { + if _, err := loadIndex([]byte(indexWithDuplicates), "indexWithDuplicates"); err == nil { + t.Errorf("Expected an error when duplicate entries are present") + } +} + +func TestLoadIndexFileAnnotations(t *testing.T) { + i, err := LoadIndexFile(annotationstestfile) if err != nil { t.Fatal(err) } verifyLocalIndex(t, i) + + if len(i.Annotations) != 1 { + t.Fatalf("Expected 1 annotation but got %d", len(i.Annotations)) + } + if i.Annotations["helm.sh/test"] != "foo bar" { + t.Error("Did not get expected value for helm.sh/test annotation") + } } func TestLoadUnorderedIndex(t *testing.T) { - b, err := ioutil.ReadFile(unorderedTestfile) - if err != nil { - t.Fatal(err) - } - i, err := loadIndex(b) + i, err := LoadIndexFile(unorderedTestfile) if err != nil { t.Fatal(err) } @@ -97,26 +177,32 @@ func TestLoadUnorderedIndex(t *testing.T) { func TestMerge(t *testing.T) { ind1 := NewIndexFile() - ind1.Add(&chart.Metadata{ - Name: "dreadnought", - Version: "0.1.0", - }, "dreadnought-0.1.0.tgz", "http://example.com", "aaaa") + + if err := ind1.MustAdd(&chart.Metadata{APIVersion: "v2", Name: "dreadnought", Version: "0.1.0"}, "dreadnought-0.1.0.tgz", "http://example.com", "aaaa"); err != nil { + t.Fatalf("unexpected error: %s", err) + } ind2 := NewIndexFile() - ind2.Add(&chart.Metadata{ - Name: "dreadnought", - Version: "0.2.0", - }, "dreadnought-0.2.0.tgz", "http://example.com", "aaaabbbb") - ind2.Add(&chart.Metadata{ - Name: "doughnut", - Version: "0.2.0", - }, "doughnut-0.2.0.tgz", "http://example.com", "ccccbbbb") + + for _, x := range []struct { + md *chart.Metadata + filename string + baseURL string + digest string + }{ + {&chart.Metadata{APIVersion: "v2", Name: "dreadnought", Version: "0.2.0"}, "dreadnought-0.2.0.tgz", "http://example.com", "aaaabbbb"}, + {&chart.Metadata{APIVersion: "v2", Name: "doughnut", Version: "0.2.0"}, "doughnut-0.2.0.tgz", "http://example.com", "ccccbbbb"}, + } { + if err := ind2.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil { + t.Errorf("unexpected error: %s", err) + } + } ind1.Merge(ind2) if len(ind1.Entries) != 2 { t.Errorf("Expected 2 entries, got %d", len(ind1.Entries)) - vs := ind1.Entries["dreadnaught"] + vs := ind1.Entries["dreadnought"] if len(vs) != 2 { t.Errorf("Expected 2 versions, got %d", len(vs)) } @@ -129,48 +215,101 @@ func TestMerge(t *testing.T) { } func TestDownloadIndexFile(t *testing.T) { - srv, err := startLocalServerForTests(nil) - if err != nil { - t.Fatal(err) - } - defer srv.Close() + t.Run("should download index file", func(t *testing.T) { + srv, err := startLocalServerForTests(nil) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + r, err := NewChartRepository(&Entry{ + Name: testRepo, + URL: srv.URL, + }, getter.All(&cli.EnvSettings{})) + if err != nil { + t.Errorf("Problem creating chart repository from %s: %v", testRepo, err) + } - dirName, err := ioutil.TempDir("", "tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dirName) + idx, err := r.DownloadIndexFile() + if err != nil { + t.Fatalf("Failed to download index file to %s: %#v", idx, err) + } - indexFilePath := filepath.Join(dirName, testRepo+"-index.yaml") - r, err := NewChartRepository(&Entry{ - Name: testRepo, - URL: srv.URL, - Cache: indexFilePath, - }, getter.All(environment.EnvSettings{})) - if err != nil { - t.Errorf("Problem creating chart repository from %s: %v", testRepo, err) - } + if _, err := os.Stat(idx); err != nil { + t.Fatalf("error finding created index file: %#v", err) + } - if err := r.DownloadIndexFile(""); err != nil { - t.Errorf("%#v", err) - } + i, err := LoadIndexFile(idx) + if err != nil { + t.Fatalf("Index %q failed to parse: %s", testfile, err) + } + verifyLocalIndex(t, i) - if _, err := os.Stat(indexFilePath); err != nil { - t.Errorf("error finding created index file: %#v", err) - } + // Check that charts file is also created + idx = filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) + if _, err := os.Stat(idx); err != nil { + t.Fatalf("error finding created charts file: %#v", err) + } - b, err := ioutil.ReadFile(indexFilePath) - if err != nil { - t.Errorf("error reading index file: %#v", err) - } + b, err := ioutil.ReadFile(idx) + if err != nil { + t.Fatalf("error reading charts file: %#v", err) + } + verifyLocalChartsFile(t, b, i) + }) + + t.Run("should not decode the path in the repo url while downloading index", func(t *testing.T) { + chartRepoURLPath := "/some%2Fpath/test" + fileBytes, err := ioutil.ReadFile("testdata/local-index.yaml") + if err != nil { + t.Fatal(err) + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.RawPath == chartRepoURLPath+"/index.yaml" { + w.Write(fileBytes) + } + }) + srv, err := startLocalServerForTests(handler) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + r, err := NewChartRepository(&Entry{ + Name: testRepo, + URL: srv.URL + chartRepoURLPath, + }, getter.All(&cli.EnvSettings{})) + if err != nil { + t.Errorf("Problem creating chart repository from %s: %v", testRepo, err) + } - i, err := loadIndex(b) - if err != nil { - t.Errorf("Index %q failed to parse: %s", testfile, err) - return - } + idx, err := r.DownloadIndexFile() + if err != nil { + t.Fatalf("Failed to download index file to %s: %#v", idx, err) + } - verifyLocalIndex(t, i) + if _, err := os.Stat(idx); err != nil { + t.Fatalf("error finding created index file: %#v", err) + } + + i, err := LoadIndexFile(idx) + if err != nil { + t.Fatalf("Index %q failed to parse: %s", testfile, err) + } + verifyLocalIndex(t, i) + + // Check that charts file is also created + idx = filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) + if _, err := os.Stat(idx); err != nil { + t.Fatalf("error finding created charts file: %#v", err) + } + + b, err := ioutil.ReadFile(idx) + if err != nil { + t.Fatalf("error reading charts file: %#v", err) + } + verifyLocalChartsFile(t, b, i) + }) } func verifyLocalIndex(t *testing.T, i *IndexFile) { @@ -181,24 +320,22 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) { alpine, ok := i.Entries["alpine"] if !ok { - t.Errorf("'alpine' section not found.") - return + t.Fatalf("'alpine' section not found.") } if l := len(alpine); l != 1 { - t.Errorf("'alpine' should have 1 chart, got %d", l) - return + t.Fatalf("'alpine' should have 1 chart, got %d", l) } nginx, ok := i.Entries["nginx"] if !ok || len(nginx) != 2 { - t.Error("Expected 2 nginx entries") - return + t.Fatalf("Expected 2 nginx entries") } expects := []*ChartVersion{ { Metadata: &chart.Metadata{ + APIVersion: "v2", Name: "alpine", Description: "string", Version: "1.0.0", @@ -206,13 +343,14 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) { Home: "https://github.com/something", }, URLs: []string{ - "https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz", + "https://charts.helm.sh/stable/alpine-1.0.0.tgz", "http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz", }, Digest: "sha256:1234567890abcdef", }, { Metadata: &chart.Metadata{ + APIVersion: "v2", Name: "nginx", Description: "string", Version: "0.2.0", @@ -220,12 +358,13 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) { Home: "https://github.com/something/else", }, URLs: []string{ - "https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz", + "https://charts.helm.sh/stable/nginx-0.2.0.tgz", }, Digest: "sha256:1234567890abcdef", }, { Metadata: &chart.Metadata{ + APIVersion: "v2", Name: "nginx", Description: "string", Version: "0.1.0", @@ -233,7 +372,7 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) { Home: "https://github.com/something", }, URLs: []string{ - "https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz", + "https://charts.helm.sh/stable/nginx-0.1.0.tgz", }, Digest: "sha256:1234567890abcdef", }, @@ -271,6 +410,24 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) { } } +func verifyLocalChartsFile(t *testing.T, chartsContent []byte, indexContent *IndexFile) { + var expected, real []string + for chart := range indexContent.Entries { + expected = append(expected, chart) + } + sort.Strings(expected) + + scanner := bufio.NewScanner(bytes.NewReader(chartsContent)) + for scanner.Scan() { + real = append(real, scanner.Text()) + } + sort.Strings(real) + + if strings.Join(expected, " ") != strings.Join(real, " ") { + t.Errorf("Cached charts file content unexpected. Expected:\n%s\ngot:\n%s", expected, real) + } +} + func TestIndexDirectory(t *testing.T) { dir := "testdata/repository" index, err := IndexDirectory(dir, "http://localhost:8080") @@ -298,7 +455,7 @@ func TestIndexDirectory(t *testing.T) { } frob := frobs[0] - if len(frob.Digest) == 0 { + if frob.Digest == "" { t.Errorf("Missing digest of file %s.", frob.Name) } if frob.URLs[0] != test.downloadLink { @@ -310,43 +467,59 @@ func TestIndexDirectory(t *testing.T) { } } -func TestLoadUnversionedIndex(t *testing.T) { - data, err := ioutil.ReadFile("testdata/unversioned-index.yaml") - if err != nil { - t.Fatal(err) - } +func TestIndexAdd(t *testing.T) { + i := NewIndexFile() - ind, err := loadUnversionedIndex(data) - if err != nil { - t.Fatal(err) + for _, x := range []struct { + md *chart.Metadata + filename string + baseURL string + digest string + }{ + + {&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"}, + {&chart.Metadata{APIVersion: "v2", Name: "alpine", Version: "0.1.0"}, "/home/charts/alpine-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"}, + {&chart.Metadata{APIVersion: "v2", Name: "deis", Version: "0.1.0"}, "/home/charts/deis-0.1.0.tgz", "http://example.com/charts/", "sha256:1234567890"}, + } { + if err := i.MustAdd(x.md, x.filename, x.baseURL, x.digest); err != nil { + t.Errorf("unexpected error adding to index: %s", err) + } } - if l := len(ind.Entries); l != 2 { - t.Fatalf("Expected 2 entries, got %d", l) + if i.Entries["clipper"][0].URLs[0] != "http://example.com/charts/clipper-0.1.0.tgz" { + t.Errorf("Expected http://example.com/charts/clipper-0.1.0.tgz, got %s", i.Entries["clipper"][0].URLs[0]) + } + if i.Entries["alpine"][0].URLs[0] != "http://example.com/charts/alpine-0.1.0.tgz" { + t.Errorf("Expected http://example.com/charts/alpine-0.1.0.tgz, got %s", i.Entries["alpine"][0].URLs[0]) + } + if i.Entries["deis"][0].URLs[0] != "http://example.com/charts/deis-0.1.0.tgz" { + t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0]) } - if l := len(ind.Entries["mysql"]); l != 3 { - t.Fatalf("Expected 3 mysql versions, got %d", l) + // test error condition + if err := i.MustAdd(&chart.Metadata{}, "error-0.1.0.tgz", "", ""); err == nil { + t.Fatal("expected error adding to index") } } -func TestIndexAdd(t *testing.T) { +func TestIndexWrite(t *testing.T) { i := NewIndexFile() - i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") - - if i.Entries["clipper"][0].URLs[0] != "http://example.com/charts/clipper-0.1.0.tgz" { - t.Errorf("Expected http://example.com/charts/clipper-0.1.0.tgz, got %s", i.Entries["clipper"][0].URLs[0]) + if err := i.MustAdd(&chart.Metadata{APIVersion: "v2", Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890"); err != nil { + t.Fatalf("unexpected error: %s", err) } - - i.Add(&chart.Metadata{Name: "alpine", Version: "0.1.0"}, "/home/charts/alpine-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") - - if i.Entries["alpine"][0].URLs[0] != "http://example.com/charts/alpine-0.1.0.tgz" { - t.Errorf("Expected http://example.com/charts/alpine-0.1.0.tgz, got %s", i.Entries["alpine"][0].URLs[0]) + dir, err := ioutil.TempDir("", "helm-tmp") + if err != nil { + t.Fatal(err) } + defer os.RemoveAll(dir) + testpath := filepath.Join(dir, "test") + i.WriteFile(testpath, 0600) - i.Add(&chart.Metadata{Name: "deis", Version: "0.1.0"}, "/home/charts/deis-0.1.0.tgz", "http://example.com/charts/", "sha256:1234567890") - - if i.Entries["deis"][0].URLs[0] != "http://example.com/charts/deis-0.1.0.tgz" { - t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0]) + got, err := ioutil.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(got), "clipper-0.1.0.tgz") { + t.Fatal("Index files doesn't contain expected content") } } diff --git a/pkg/repo/local.go b/pkg/repo/local.go deleted file mode 100644 index f13a4d0ac30..00000000000 --- a/pkg/repo/local.go +++ /dev/null @@ -1,137 +0,0 @@ -/* -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 repo - -import ( - "fmt" - htemplate "html/template" - "io/ioutil" - "net/http" - "path/filepath" - "strings" - - "github.com/ghodss/yaml" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/provenance" -) - -const indexHTMLTemplate = ` - - - Helm Repository - -

Helm Charts Repository

-
    -{{range $name, $ver := .Index.Entries}} -
  • {{$name}} -
  • -{{end}} -
- -

Last Generated: {{.Index.Generated}}

- - -` - -// RepositoryServer is an HTTP handler for serving a chart repository. -type RepositoryServer struct { - RepoPath string -} - -// ServeHTTP implements the http.Handler interface. -func (s *RepositoryServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - uri := r.URL.Path - switch uri { - case "/", "/charts/", "/charts/index.html", "/charts/index": - w.Header().Set("Content-Type", "text/html; charset=utf-8") - s.htmlIndex(w, r) - default: - file := strings.TrimPrefix(uri, "/charts/") - http.ServeFile(w, r, filepath.Join(s.RepoPath, file)) - } -} - -// StartLocalRepo starts a web server and serves files from the given path -func StartLocalRepo(path, address string) error { - if address == "" { - address = "127.0.0.1:8879" - } - s := &RepositoryServer{RepoPath: path} - return http.ListenAndServe(address, s) -} - -func (s *RepositoryServer) htmlIndex(w http.ResponseWriter, r *http.Request) { - t := htemplate.Must(htemplate.New("index.html").Parse(indexHTMLTemplate)) - // load index - lrp := filepath.Join(s.RepoPath, "index.yaml") - i, err := LoadIndexFile(lrp) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - data := map[string]interface{}{ - "Index": i, - } - if err := t.Execute(w, data); err != nil { - fmt.Fprintf(w, "Template error: %s", err) - } -} - -// AddChartToLocalRepo saves a chart in the given path and then reindexes the index file -func AddChartToLocalRepo(ch *chart.Chart, path string) error { - _, err := chartutil.Save(ch, path) - if err != nil { - return err - } - return Reindex(ch, path+"/index.yaml") -} - -// Reindex adds an entry to the index file at the given path -func Reindex(ch *chart.Chart, path string) error { - name := ch.Metadata.Name + "-" + ch.Metadata.Version - y, err := LoadIndexFile(path) - if err != nil { - return err - } - found := false - for k := range y.Entries { - if k == name { - found = true - break - } - } - if !found { - dig, err := provenance.DigestFile(path) - if err != nil { - return err - } - - y.Add(ch.Metadata, name+".tgz", "http://127.0.0.1:8879/charts", "sha256:"+dig) - - out, err := yaml.Marshal(y) - if err != nil { - return err - } - - ioutil.WriteFile(path, out, 0644) - } - return nil -} diff --git a/pkg/repo/local_test.go b/pkg/repo/local_test.go deleted file mode 100644 index 1e5359dee6e..00000000000 --- a/pkg/repo/local_test.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -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 repo - -import ( - "io/ioutil" - "net/http" - "strings" - "testing" -) - -func TestRepositoryServer(t *testing.T) { - expectedIndexYAML, err := ioutil.ReadFile("testdata/server/index.yaml") - if err != nil { - t.Fatal(err) - } - - tests := []struct { - name string - path string - expect string - }{ - {"index YAML", "/charts/index.yaml", string(expectedIndexYAML)}, - {"index HTML", "/charts/index.html", ""}, - {"charts root", "/charts/", ""}, - {"root", "/", ""}, - {"file", "/test.txt", "Hello World"}, - } - - s := &RepositoryServer{RepoPath: "testdata/server"} - srv, err := startLocalServerForTests(s) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - - for _, tt := range tests { - res, err := http.Get(srv.URL + tt.path) - if err != nil { - t.Errorf("%s: error getting %s: %s", tt.name, tt.path, err) - continue - } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Errorf("%s: error reading %s: %s", tt.name, tt.path, err) - } - res.Body.Close() - if !strings.Contains(string(body), tt.expect) { - t.Errorf("%s: expected to find %q in %q", tt.name, tt.expect, string(body)) - } - } - -} diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index b5bba164e07..6f1e90dad24 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,119 +14,89 @@ See the License for the specific language governing permissions and limitations under the License. */ -package repo // import "k8s.io/helm/pkg/repo" +package repo // import "helm.sh/helm/v3/pkg/repo" import ( - "errors" - "fmt" "io/ioutil" "os" + "path/filepath" "time" - "github.com/ghodss/yaml" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" ) -// ErrRepoOutOfDate indicates that the repository file is out of date, but -// is fixable. -var ErrRepoOutOfDate = errors.New("repository file is out of date") - -// RepoFile represents the repositories.yaml file in $HELM_HOME -// TODO: change type name to File in Helm 3 to resolve linter warning -type RepoFile struct { // nolint +// File represents the repositories.yaml file +type File struct { APIVersion string `json:"apiVersion"` Generated time.Time `json:"generated"` Repositories []*Entry `json:"repositories"` } -// NewRepoFile generates an empty repositories file. +// NewFile generates an empty repositories file. // // Generated and APIVersion are automatically set. -func NewRepoFile() *RepoFile { - return &RepoFile{ +func NewFile() *File { + return &File{ APIVersion: APIVersionV1, Generated: time.Now(), Repositories: []*Entry{}, } } -// LoadRepositoriesFile takes a file at the given path and returns a RepoFile object -// -// If this returns ErrRepoOutOfDate, it also returns a recovered RepoFile that -// can be saved as a replacement to the out of date file. -func LoadRepositoriesFile(path string) (*RepoFile, error) { +// LoadFile takes a file at the given path and returns a File object +func LoadFile(path string) (*File, error) { + r := new(File) b, err := ioutil.ReadFile(path) if err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf( - "Couldn't load repositories file (%s).\n"+ - "You might need to run `helm init` (or "+ - "`helm init --client-only` if tiller is "+ - "already installed)", path) - } - return nil, err + return r, errors.Wrapf(err, "couldn't load repositories file (%s)", path) } - r := &RepoFile{} err = yaml.Unmarshal(b, r) - if err != nil { - return nil, err - } - - // File is either corrupt, or is from before v2.0.0-Alpha.5 - if r.APIVersion == "" { - m := map[string]string{} - if err = yaml.Unmarshal(b, &m); err != nil { - return nil, err - } - r := NewRepoFile() - for k, v := range m { - r.Add(&Entry{ - Name: k, - URL: v, - Cache: fmt.Sprintf("%s-index.yaml", k), - }) - } - return r, ErrRepoOutOfDate - } - - return r, nil + return r, err } // Add adds one or more repo entries to a repo file. -func (r *RepoFile) Add(re ...*Entry) { +func (r *File) Add(re ...*Entry) { r.Repositories = append(r.Repositories, re...) } // Update attempts to replace one or more repo entries in a repo file. If an // entry with the same name doesn't exist in the repo file it will add it. -func (r *RepoFile) Update(re ...*Entry) { +func (r *File) Update(re ...*Entry) { for _, target := range re { - found := false - for j, repo := range r.Repositories { - if repo.Name == target.Name { - r.Repositories[j] = target - found = true - break - } - } - if !found { - r.Add(target) + r.update(target) + } +} + +func (r *File) update(e *Entry) { + for j, repo := range r.Repositories { + if repo.Name == e.Name { + r.Repositories[j] = e + return } } + r.Add(e) } // Has returns true if the given name is already a repository name. -func (r *RepoFile) Has(name string) bool { - for _, rf := range r.Repositories { - if rf.Name == name { - return true +func (r *File) Has(name string) bool { + entry := r.Get(name) + return entry != nil +} + +// Get returns an entry with the given name if it exists, otherwise returns nil +func (r *File) Get(name string) *Entry { + for _, entry := range r.Repositories { + if entry.Name == name { + return entry } } - return false + return nil } // Remove removes the entry from the list of repositories. -func (r *RepoFile) Remove(name string) bool { +func (r *File) Remove(name string) bool { cp := []*Entry{} found := false for _, rf := range r.Repositories { @@ -141,10 +111,13 @@ func (r *RepoFile) Remove(name string) bool { } // WriteFile writes a repositories file to the given path. -func (r *RepoFile) WriteFile(path string, perm os.FileMode) error { +func (r *File) WriteFile(path string, perm os.FileMode) error { data, err := yaml.Marshal(r) if err != nil { return err } + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } return ioutil.WriteFile(path, data, perm) } diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go index 4b5bcdbf5e9..f87d2c202bc 100644 --- a/pkg/repo/repo_test.go +++ b/pkg/repo/repo_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,25 +16,25 @@ limitations under the License. package repo -import "testing" -import "io/ioutil" -import "os" -import "strings" +import ( + "io/ioutil" + "os" + "strings" + "testing" +) const testRepositoriesFile = "testdata/repositories.yaml" -func TestRepoFile(t *testing.T) { - rf := NewRepoFile() +func TestFile(t *testing.T) { + rf := NewFile() rf.Add( &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - Cache: "stable-index.yaml", + Name: "stable", + URL: "https://example.com/stable/charts", }, &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - Cache: "incubator-index.yaml", + Name: "incubator", + URL: "https://example.com/incubator", }, ) @@ -56,82 +56,89 @@ func TestRepoFile(t *testing.T) { if stable.URL != "https://example.com/stable/charts" { t.Error("Wrong URL for stable") } - if stable.Cache != "stable-index.yaml" { - t.Error("Wrong cache name for stable") - } } -func TestNewRepositoriesFile(t *testing.T) { - expects := NewRepoFile() +func TestNewFile(t *testing.T) { + expects := NewFile() expects.Add( &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - Cache: "stable-index.yaml", + Name: "stable", + URL: "https://example.com/stable/charts", }, &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - Cache: "incubator-index.yaml", + Name: "incubator", + URL: "https://example.com/incubator", }, ) - repofile, err := LoadRepositoriesFile(testRepositoriesFile) + file, err := LoadFile(testRepositoriesFile) if err != nil { t.Errorf("%q could not be loaded: %s", testRepositoriesFile, err) } - if len(expects.Repositories) != len(repofile.Repositories) { - t.Fatalf("Unexpected repo data: %#v", repofile.Repositories) + if len(expects.Repositories) != len(file.Repositories) { + t.Fatalf("Unexpected repo data: %#v", file.Repositories) } for i, expect := range expects.Repositories { - got := repofile.Repositories[i] + got := file.Repositories[i] if expect.Name != got.Name { t.Errorf("Expected name %q, got %q", expect.Name, got.Name) } if expect.URL != got.URL { t.Errorf("Expected url %q, got %q", expect.URL, got.URL) } - if expect.Cache != got.Cache { - t.Errorf("Expected cache %q, got %q", expect.Cache, got.Cache) - } } } -func TestNewPreV1RepositoriesFile(t *testing.T) { - r, err := LoadRepositoriesFile("testdata/old-repositories.yaml") - if err != nil && err != ErrRepoOutOfDate { - t.Fatal(err) - } - if len(r.Repositories) != 3 { - t.Fatalf("Expected 3 repos: %#v", r) +func TestRepoFile_Get(t *testing.T) { + repo := NewFile() + repo.Add( + &Entry{ + Name: "first", + URL: "https://example.com/first", + }, + &Entry{ + Name: "second", + URL: "https://example.com/second", + }, + &Entry{ + Name: "third", + URL: "https://example.com/third", + }, + &Entry{ + Name: "fourth", + URL: "https://example.com/fourth", + }, + ) + + name := "second" + + entry := repo.Get(name) + if entry == nil { + t.Fatalf("Expected repo entry %q to be found", name) } - // Because they are parsed as a map, we lose ordering. - found := false - for _, rr := range r.Repositories { - if rr.Name == "best-charts-ever" { - found = true - } + if entry.URL != "https://example.com/second" { + t.Errorf("Expected repo URL to be %q but got %q", "https://example.com/second", entry.URL) } - if !found { - t.Errorf("expected the best charts ever. Got %#v", r.Repositories) + + entry = repo.Get("nonexistent") + if entry != nil { + t.Errorf("Got unexpected entry %+v", entry) } } func TestRemoveRepository(t *testing.T) { - sampleRepository := NewRepoFile() + sampleRepository := NewFile() sampleRepository.Add( &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - Cache: "stable-index.yaml", + Name: "stable", + URL: "https://example.com/stable/charts", }, &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - Cache: "incubator-index.yaml", + Name: "incubator", + URL: "https://example.com/incubator", }, ) @@ -148,23 +155,20 @@ func TestRemoveRepository(t *testing.T) { } func TestUpdateRepository(t *testing.T) { - sampleRepository := NewRepoFile() + sampleRepository := NewFile() sampleRepository.Add( &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - Cache: "stable-index.yaml", + Name: "stable", + URL: "https://example.com/stable/charts", }, &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - Cache: "incubator-index.yaml", + Name: "incubator", + URL: "https://example.com/incubator", }, ) newRepoName := "sample" sampleRepository.Update(&Entry{Name: newRepoName, - URL: "https://example.com/sample", - Cache: "sample-index.yaml", + URL: "https://example.com/sample", }) if !sampleRepository.Has(newRepoName) { @@ -173,8 +177,7 @@ func TestUpdateRepository(t *testing.T) { repoCount := len(sampleRepository.Repositories) sampleRepository.Update(&Entry{Name: newRepoName, - URL: "https://example.com/sample", - Cache: "sample-index.yaml", + URL: "https://example.com/sample", }) if repoCount != len(sampleRepository.Repositories) { @@ -183,30 +186,28 @@ func TestUpdateRepository(t *testing.T) { } func TestWriteFile(t *testing.T) { - sampleRepository := NewRepoFile() + sampleRepository := NewFile() sampleRepository.Add( &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - Cache: "stable-index.yaml", + Name: "stable", + URL: "https://example.com/stable/charts", }, &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - Cache: "incubator-index.yaml", + Name: "incubator", + URL: "https://example.com/incubator", }, ) - repoFile, err := ioutil.TempFile("", "helm-repo") + file, err := ioutil.TempFile("", "helm-repo") if err != nil { t.Errorf("failed to create test-file (%v)", err) } - defer os.Remove(repoFile.Name()) - if err := sampleRepository.WriteFile(repoFile.Name(), 744); err != nil { + defer os.Remove(file.Name()) + if err := sampleRepository.WriteFile(file.Name(), 0644); err != nil { t.Errorf("failed to write file (%v)", err) } - repos, err := LoadRepositoriesFile(repoFile.Name()) + repos, err := LoadFile(file.Name()) if err != nil { t.Errorf("failed to load file (%v)", err) } @@ -218,10 +219,9 @@ func TestWriteFile(t *testing.T) { } func TestRepoNotExists(t *testing.T) { - _, err := LoadRepositoriesFile("/this/path/does/not/exist.yaml") - if err == nil { + if _, err := LoadFile("/this/path/does/not/exist.yaml"); err == nil { t.Errorf("expected err to be non-nil when path does not exist") - } else if !strings.Contains(err.Error(), "You might need to run `helm init`") { - t.Errorf("expected prompt to run `helm init` when repositories file does not exist") + } else if !strings.Contains(err.Error(), "couldn't load repositories file") { + t.Errorf("expected prompt `couldn't load repositories file`") } } diff --git a/pkg/repo/repotest/doc.go b/pkg/repo/repotest/doc.go index 34d4bc6b041..3bf98aa7e9d 100644 --- a/pkg/repo/repotest/doc.go +++ b/pkg/repo/repotest/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go index 8ea9103a019..94b5ce7f973 100644 --- a/pkg/repo/repotest/server.go +++ b/pkg/repo/repotest/server.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -16,18 +16,206 @@ limitations under the License. package repotest import ( + "context" + "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" + "testing" + "time" - "github.com/ghodss/yaml" + auth "github.com/deislabs/oras/pkg/auth/docker" + "github.com/docker/distribution/configuration" + "github.com/docker/distribution/registry" + _ "github.com/docker/distribution/registry/auth/htpasswd" // used for docker test registry + _ "github.com/docker/distribution/registry/storage/driver/inmemory" // used for docker test registry + "github.com/phayes/freeport" + "golang.org/x/crypto/bcrypt" + "sigs.k8s.io/yaml" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/repo" + ociRegistry "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/internal/tlsutil" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/repo" ) +// NewTempServerWithCleanup creates a server inside of a temp dir. +// +// If the passed in string is not "", it will be treated as a shell glob, and files +// will be copied from that path to the server's docroot. +// +// The caller is responsible for stopping the server. +// The temp dir will be removed by testing package automatically when test finished. +func NewTempServerWithCleanup(t *testing.T, glob string) (*Server, error) { + srv, err := NewTempServer(glob) + t.Cleanup(func() { os.RemoveAll(srv.docroot) }) + return srv, err +} + +type OCIServer struct { + *registry.Registry + RegistryURL string + Dir string + TestUsername string + TestPassword string + Client *ociRegistry.Client +} + +type OCIServerRunConfig struct { + DependingChart *chart.Chart +} + +type OCIServerOpt func(config *OCIServerRunConfig) + +func WithDependingChart(c *chart.Chart) OCIServerOpt { + return func(config *OCIServerRunConfig) { + config.DependingChart = c + } +} + +func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) { + testHtpasswdFileBasename := "authtest.htpasswd" + testUsername, testPassword := "username", "password" + + pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) + if err != nil { + t.Fatal("error generating bcrypt password for test htpasswd file") + } + htpasswdPath := filepath.Join(dir, testHtpasswdFileBasename) + err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644) + if err != nil { + t.Fatalf("error creating test htpasswd file") + } + + // Registry config + config := &configuration.Configuration{} + port, err := freeport.GetFreePort() + if err != nil { + t.Fatalf("error finding free port for test registry") + } + + config.HTTP.Addr = fmt.Sprintf(":%d", port) + config.HTTP.DrainTimeout = time.Duration(10) * time.Second + config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} + config.Auth = configuration.Auth{ + "htpasswd": configuration.Parameters{ + "realm": "localhost", + "path": htpasswdPath, + }, + } + + registryURL := fmt.Sprintf("localhost:%d", port) + + r, err := registry.NewRegistry(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + return &OCIServer{ + Registry: r, + RegistryURL: registryURL, + TestUsername: testUsername, + TestPassword: testPassword, + Dir: dir, + }, nil +} + +func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) { + cfg := &OCIServerRunConfig{} + for _, fn := range opts { + fn(cfg) + } + + go srv.ListenAndServe() + + credentialsFile := filepath.Join(srv.Dir, "config.json") + + client, err := auth.NewClient(credentialsFile) + if err != nil { + t.Fatalf("error creating auth client") + } + + resolver, err := client.Resolver(context.Background(), http.DefaultClient, false) + if err != nil { + t.Fatalf("error creating resolver") + } + + // init test client + registryClient, err := ociRegistry.NewClient( + ociRegistry.ClientOptDebug(true), + ociRegistry.ClientOptWriter(os.Stdout), + ociRegistry.ClientOptAuthorizer(&ociRegistry.Authorizer{ + Client: client, + }), + ociRegistry.ClientOptResolver(&ociRegistry.Resolver{ + Resolver: resolver, + }), + ) + if err != nil { + t.Fatalf("error creating registry client") + } + + err = registryClient.Login(srv.RegistryURL, srv.TestUsername, srv.TestPassword, false) + if err != nil { + t.Fatalf("error logging into registry with good credentials") + } + + ref, err := ociRegistry.ParseReference(fmt.Sprintf("%s/u/ocitestuser/oci-dependent-chart:0.1.0", srv.RegistryURL)) + if err != nil { + t.Fatalf("") + } + + err = chartutil.ExpandFile(srv.Dir, filepath.Join(srv.Dir, "oci-dependent-chart-0.1.0.tgz")) + if err != nil { + t.Fatal(err) + } + + // valid chart + ch, err := loader.LoadDir(filepath.Join(srv.Dir, "oci-dependent-chart")) + if err != nil { + t.Fatal("error loading chart") + } + + err = os.RemoveAll(filepath.Join(srv.Dir, "oci-dependent-chart")) + if err != nil { + t.Fatal("error removing chart before push") + } + + err = registryClient.SaveChart(ch, ref) + if err != nil { + t.Fatal("error saving chart") + } + + err = registryClient.PushChart(ref) + if err != nil { + t.Fatal("error pushing chart") + } + + if cfg.DependingChart != nil { + c := cfg.DependingChart + dependingRef, err := ociRegistry.ParseReference(fmt.Sprintf("%s/u/ocitestuser/oci-depending-chart:1.2.3", srv.RegistryURL)) + if err != nil { + t.Fatal("error parsing reference for depending chart reference") + } + + err = registryClient.SaveChart(c, dependingRef) + if err != nil { + t.Fatal("error saving depending chart") + } + + err = registryClient.PushChart(dependingRef) + if err != nil { + t.Fatal("error pushing depending chart") + } + } + + srv.Client = registryClient +} + // NewTempServer creates a server inside of a temp dir. // // If the passed in string is not "", it will be treated as a shell glob, and files @@ -35,22 +223,23 @@ import ( // // The caller is responsible for destroying the temp directory as well as stopping // the server. -func NewTempServer(glob string) (*Server, helmpath.Home, error) { +// +// Deprecated: use NewTempServerWithCleanup +func NewTempServer(glob string) (*Server, error) { tdir, err := ioutil.TempDir("", "helm-repotest-") - tdirh := helmpath.Home(tdir) if err != nil { - return nil, tdirh, err + return nil, err } srv := NewServer(tdir) if glob != "" { if _, err := srv.CopyCharts(glob); err != nil { srv.Stop() - return srv, tdirh, err + return srv, err } } - return srv, tdirh, nil + return srv, nil } // NewServer creates a repository server for testing. @@ -69,9 +258,9 @@ func NewServer(docroot string) *Server { srv := &Server{ docroot: root, } - srv.start() + srv.Start() // Add the testing repository as the only repo. - if err := setTestingRepository(helmpath.Home(docroot), "test", srv.URL()); err != nil { + if err := setTestingRepository(srv.URL(), filepath.Join(root, "repositories.yaml")); err != nil { panic(err) } return srv @@ -79,8 +268,15 @@ func NewServer(docroot string) *Server { // Server is an implementation of a repository server for testing. type Server struct { - docroot string - srv *httptest.Server + docroot string + srv *httptest.Server + middleware http.HandlerFunc +} + +// WithMiddleware injects middleware in front of the server. This can be used to inject +// additional functionality like layering in an authentication frontend. +func (s *Server) WithMiddleware(middleware http.HandlerFunc) { + s.middleware = middleware } // Root gets the docroot for the server. @@ -102,7 +298,7 @@ func (s *Server) CopyCharts(origin string) ([]string, error) { if err != nil { return []string{}, err } - if err := ioutil.WriteFile(newname, data, 0755); err != nil { + if err := ioutil.WriteFile(newname, data, 0644); err != nil { return []string{}, err } copied[i] = newname @@ -126,11 +322,50 @@ func (s *Server) CreateIndex() error { } ifile := filepath.Join(s.docroot, "index.yaml") - return ioutil.WriteFile(ifile, d, 0755) + return ioutil.WriteFile(ifile, d, 0644) +} + +func (s *Server) Start() { + s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.middleware != nil { + s.middleware.ServeHTTP(w, r) + } + http.FileServer(http.Dir(s.docroot)).ServeHTTP(w, r) + })) } -func (s *Server) start() { - s.srv = httptest.NewServer(http.FileServer(http.Dir(s.docroot))) +func (s *Server) StartTLS() { + cd := "../../testdata" + ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem") + + s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.middleware != nil { + s.middleware.ServeHTTP(w, r) + } + http.FileServer(http.Dir(s.Root())).ServeHTTP(w, r) + })) + tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca) + if err != nil { + panic(err) + } + tlsConf.BuildNameToCertificate() + tlsConf.ServerName = "helm.sh" + s.srv.TLS = tlsConf + s.srv.StartTLS() + + // Set up repositories config with ca file + repoConfig := filepath.Join(s.Root(), "repositories.yaml") + + r := repo.NewFile() + r.Add(&repo.Entry{ + Name: "test", + URL: s.URL(), + CAFile: filepath.Join("../../testdata", "rootca.crt"), + }) + + if err := r.WriteFile(repoConfig, 0644); err != nil { + panic(err) + } } // Stop stops the server and closes all connections. @@ -148,25 +383,21 @@ func (s *Server) URL() string { return s.srv.URL } -// LinkIndices links the index created with CreateIndex and makes a symboic link to the repositories/cache directory. +// LinkIndices links the index created with CreateIndex and makes a symbolic link to the cache index. // // This makes it possible to simulate a local cache of a repository. func (s *Server) LinkIndices() error { - destfile := "test-index.yaml" - // Link the index.yaml file to the lstart := filepath.Join(s.docroot, "index.yaml") - ldest := filepath.Join(s.docroot, "repository/cache", destfile) + ldest := filepath.Join(s.docroot, "test-index.yaml") return os.Symlink(lstart, ldest) } -// setTestingRepository sets up a testing repository.yaml with only the given name/URL. -func setTestingRepository(home helmpath.Home, name, url string) error { - r := repo.NewRepoFile() +// setTestingRepository sets up a testing repository.yaml with only the given URL. +func setTestingRepository(url, fname string) error { + r := repo.NewFile() r.Add(&repo.Entry{ - Name: name, - URL: url, - Cache: home.CacheIndex(name), + Name: "test", + URL: url, }) - os.MkdirAll(filepath.Join(home.Repository(), name), 0755) - return r.WriteFile(home.RepositoryFile(), 0644) + return r.WriteFile(fname, 0644) } diff --git a/pkg/repo/repotest/server_test.go b/pkg/repo/repotest/server_test.go index 61c056172b9..1ad979fdc4d 100644 --- a/pkg/repo/repotest/server_test.go +++ b/pkg/repo/repotest/server_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -22,28 +22,27 @@ import ( "path/filepath" "testing" - "github.com/ghodss/yaml" + "sigs.k8s.io/yaml" - "k8s.io/helm/pkg/repo" + "helm.sh/helm/v3/internal/test/ensure" + "helm.sh/helm/v3/pkg/repo" ) // Young'n, in these here parts, we test our tests. func TestServer(t *testing.T) { - docroot, err := ioutil.TempDir("", "helm-repotest-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(docroot) + defer ensure.HelmHome(t)() - srv := NewServer(docroot) + rootDir := ensure.TempDir(t) + defer os.RemoveAll(rootDir) + + srv := NewServer(rootDir) defer srv.Stop() c, err := srv.CopyCharts("testdata/*.tgz") if err != nil { // Some versions of Go don't correctly fire defer on Fatal. - t.Error(err) - return + t.Fatal(err) } if len(c) != 1 { @@ -55,9 +54,9 @@ func TestServer(t *testing.T) { } res, err := http.Get(srv.URL() + "/examplechart-0.1.0.tgz") + res.Body.Close() if err != nil { - t.Error(err) - return + t.Fatal(err) } if res.ContentLength < 500 { @@ -66,26 +65,22 @@ func TestServer(t *testing.T) { res, err = http.Get(srv.URL() + "/index.yaml") if err != nil { - t.Error(err) - return + t.Fatal(err) } data, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { - t.Error(err) - return + t.Fatal(err) } m := repo.NewIndexFile() if err := yaml.Unmarshal(data, m); err != nil { - t.Error(err) - return + t.Fatal(err) } if l := len(m.Entries); l != 1 { - t.Errorf("Expected 1 entry, got %d", l) - return + t.Fatalf("Expected 1 entry, got %d", l) } expect := "examplechart" @@ -94,30 +89,26 @@ func TestServer(t *testing.T) { } res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing") + res.Body.Close() if err != nil { - t.Error(err) - return + t.Fatal(err) } if res.StatusCode != 404 { - t.Errorf("Expected 404, got %d", res.StatusCode) + t.Fatalf("Expected 404, got %d", res.StatusCode) } } func TestNewTempServer(t *testing.T) { - srv, tdir, err := NewTempServer("testdata/examplechart-0.1.0.tgz") - if err != nil { - t.Fatal(err) - } - defer func() { - srv.Stop() - os.RemoveAll(tdir.String()) - }() + defer ensure.HelmHome(t)() - if _, err := os.Stat(tdir.String()); err != nil { + srv, err := NewTempServerWithCleanup(t, "testdata/examplechart-0.1.0.tgz") + if err != nil { t.Fatal(err) } + defer srv.Stop() res, err := http.Head(srv.URL() + "/examplechart-0.1.0.tgz") + res.Body.Close() if err != nil { t.Error(err) } diff --git a/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz b/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz index aec86c64002..c5ea741eb90 100644 Binary files a/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz and b/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz differ diff --git a/pkg/repo/repotest/testdata/examplechart/Chart.yaml b/pkg/repo/repotest/testdata/examplechart/Chart.yaml index 8e06de64847..a7d29728512 100644 --- a/pkg/repo/repotest/testdata/examplechart/Chart.yaml +++ b/pkg/repo/repotest/testdata/examplechart/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: examplechart version: 0.1.0 diff --git a/pkg/repo/testdata/chartmuseum-index.yaml b/pkg/repo/testdata/chartmuseum-index.yaml new file mode 100644 index 00000000000..349a529aaf1 --- /dev/null +++ b/pkg/repo/testdata/chartmuseum-index.yaml @@ -0,0 +1,54 @@ +serverInfo: + contextPath: /v1/helm +apiVersion: v1 +entries: + nginx: + - urls: + - https://charts.helm.sh/stable/nginx-0.2.0.tgz + name: nginx + description: string + version: 0.2.0 + home: https://github.com/something/else + digest: "sha256:1234567890abcdef" + keywords: + - popular + - web server + - proxy + apiVersion: v2 + - urls: + - https://charts.helm.sh/stable/nginx-0.1.0.tgz + name: nginx + description: string + version: 0.1.0 + home: https://github.com/something + digest: "sha256:1234567890abcdef" + keywords: + - popular + - web server + - proxy + apiVersion: v2 + alpine: + - urls: + - https://charts.helm.sh/stable/alpine-1.0.0.tgz + - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz + name: alpine + description: string + version: 1.0.0 + home: https://github.com/something + keywords: + - linux + - alpine + - small + - sumtin + digest: "sha256:1234567890abcdef" + apiVersion: v2 + chartWithNoURL: + - name: chartWithNoURL + description: string + version: 1.0.0 + home: https://github.com/something + keywords: + - small + - sumtin + digest: "sha256:1234567890abcdef" + apiVersion: v2 diff --git a/pkg/repo/testdata/local-index-annotations.yaml b/pkg/repo/testdata/local-index-annotations.yaml new file mode 100644 index 00000000000..833ab854b0f --- /dev/null +++ b/pkg/repo/testdata/local-index-annotations.yaml @@ -0,0 +1,54 @@ +apiVersion: v1 +entries: + nginx: + - urls: + - https://charts.helm.sh/stable/nginx-0.2.0.tgz + name: nginx + description: string + version: 0.2.0 + home: https://github.com/something/else + digest: "sha256:1234567890abcdef" + keywords: + - popular + - web server + - proxy + apiVersion: v2 + - urls: + - https://charts.helm.sh/stable/nginx-0.1.0.tgz + name: nginx + description: string + version: 0.1.0 + home: https://github.com/something + digest: "sha256:1234567890abcdef" + keywords: + - popular + - web server + - proxy + apiVersion: v2 + alpine: + - urls: + - https://charts.helm.sh/stable/alpine-1.0.0.tgz + - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz + name: alpine + description: string + version: 1.0.0 + home: https://github.com/something + keywords: + - linux + - alpine + - small + - sumtin + digest: "sha256:1234567890abcdef" + apiVersion: v2 + chartWithNoURL: + - name: chartWithNoURL + description: string + version: 1.0.0 + home: https://github.com/something + keywords: + - small + - sumtin + digest: "sha256:1234567890abcdef" + apiVersion: v2 +annotations: + helm.sh/test: foo bar diff --git a/pkg/repo/testdata/local-index-unordered.yaml b/pkg/repo/testdata/local-index-unordered.yaml index 7482baaae85..cdfaa7f24fe 100644 --- a/pkg/repo/testdata/local-index-unordered.yaml +++ b/pkg/repo/testdata/local-index-unordered.yaml @@ -2,7 +2,7 @@ apiVersion: v1 entries: nginx: - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz + - https://charts.helm.sh/stable/nginx-0.1.0.tgz name: nginx description: string version: 0.1.0 @@ -12,8 +12,9 @@ entries: - popular - web server - proxy + apiVersion: v2 - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz + - https://charts.helm.sh/stable/nginx-0.2.0.tgz name: nginx description: string version: 0.2.0 @@ -23,9 +24,10 @@ entries: - popular - web server - proxy + apiVersion: v2 alpine: - urls: - - https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz + - https://charts.helm.sh/stable/alpine-1.0.0.tgz - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz name: alpine description: string @@ -37,6 +39,7 @@ entries: - small - sumtin digest: "sha256:1234567890abcdef" + apiVersion: v2 chartWithNoURL: - name: chartWithNoURL description: string @@ -46,3 +49,4 @@ entries: - small - sumtin digest: "sha256:1234567890abcdef" + apiVersion: v2 diff --git a/pkg/repo/testdata/local-index.yaml b/pkg/repo/testdata/local-index.yaml index e680d2a3e7b..d61f40ddafb 100644 --- a/pkg/repo/testdata/local-index.yaml +++ b/pkg/repo/testdata/local-index.yaml @@ -2,7 +2,7 @@ apiVersion: v1 entries: nginx: - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz + - https://charts.helm.sh/stable/nginx-0.2.0.tgz name: nginx description: string version: 0.2.0 @@ -12,8 +12,9 @@ entries: - popular - web server - proxy + apiVersion: v2 - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz + - https://charts.helm.sh/stable/nginx-0.1.0.tgz name: nginx description: string version: 0.1.0 @@ -23,9 +24,10 @@ entries: - popular - web server - proxy + apiVersion: v2 alpine: - urls: - - https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz + - https://charts.helm.sh/stable/alpine-1.0.0.tgz - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz name: alpine description: string @@ -37,6 +39,7 @@ entries: - small - sumtin digest: "sha256:1234567890abcdef" + apiVersion: v2 chartWithNoURL: - name: chartWithNoURL description: string @@ -46,3 +49,4 @@ entries: - small - sumtin digest: "sha256:1234567890abcdef" + apiVersion: v2 diff --git a/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz b/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz index fc8cacec2a4..8731dce02cc 100644 Binary files a/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz and b/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz differ diff --git a/pkg/repo/testdata/repository/sprocket-1.1.0.tgz b/pkg/repo/testdata/repository/sprocket-1.1.0.tgz index 595e9cc0397..48d65f49154 100644 Binary files a/pkg/repo/testdata/repository/sprocket-1.1.0.tgz and b/pkg/repo/testdata/repository/sprocket-1.1.0.tgz differ diff --git a/pkg/repo/testdata/repository/sprocket-1.2.0.tgz b/pkg/repo/testdata/repository/sprocket-1.2.0.tgz index 82188a99b03..6fdc73c2b5c 100644 Binary files a/pkg/repo/testdata/repository/sprocket-1.2.0.tgz and b/pkg/repo/testdata/repository/sprocket-1.2.0.tgz differ diff --git a/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz b/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz index 90cb34bd5f3..6f1e8564cd7 100644 Binary files a/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz and b/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz differ diff --git a/pkg/repo/testdata/server/index.yaml b/pkg/repo/testdata/server/index.yaml index ec529f11075..d627928b239 100644 --- a/pkg/repo/testdata/server/index.yaml +++ b/pkg/repo/testdata/server/index.yaml @@ -2,7 +2,7 @@ apiVersion: v1 entries: nginx: - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz + - https://charts.helm.sh/stable/nginx-0.1.0.tgz name: nginx description: string version: 0.1.0 @@ -13,7 +13,7 @@ entries: - web server - proxy - urls: - - https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz + - https://charts.helm.sh/stable/nginx-0.2.0.tgz name: nginx description: string version: 0.2.0 @@ -25,7 +25,7 @@ entries: - proxy alpine: - urls: - - https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz + - https://charts.helm.sh/stable/alpine-1.0.0.tgz - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz name: alpine description: string diff --git a/pkg/repo/testdata/unversioned-index.yaml b/pkg/repo/testdata/unversioned-index.yaml deleted file mode 100644 index 7299c66dc73..00000000000 --- a/pkg/repo/testdata/unversioned-index.yaml +++ /dev/null @@ -1,64 +0,0 @@ -memcached-0.1.0: - name: memcached - url: https://mumoshu.github.io/charts/memcached-0.1.0.tgz - created: 2016-08-04 02:05:02.259205055 +0000 UTC - checksum: ce9b76576c4b4eb74286fa30a978c56d69e7a522 - chartfile: - name: memcached - home: http://https://hub.docker.com/_/memcached/ - sources: [] - version: 0.1.0 - description: A simple Memcached cluster - keywords: [] - maintainers: - - name: Matt Butcher - email: mbutcher@deis.com - engine: "" -mysql-0.2.0: - name: mysql - url: https://mumoshu.github.io/charts/mysql-0.2.0.tgz - created: 2016-08-04 00:42:47.517342022 +0000 UTC - checksum: aa5edd2904d639b0b6295f1c7cf4c0a8e4f77dd3 - chartfile: - name: mysql - home: https://www.mysql.com/ - sources: [] - version: 0.2.0 - description: Chart running MySQL. - keywords: [] - maintainers: - - name: Matt Fisher - email: mfisher@deis.com - engine: "" -mysql-0.2.1: - name: mysql - url: https://mumoshu.github.io/charts/mysql-0.2.1.tgz - created: 2016-08-04 02:40:29.717829534 +0000 UTC - checksum: 9d9f056171beefaaa04db75680319ca4edb6336a - chartfile: - name: mysql - home: https://www.mysql.com/ - sources: [] - version: 0.2.1 - description: Chart running MySQL. - keywords: [] - maintainers: - - name: Matt Fisher - email: mfisher@deis.com - engine: "" -mysql-0.2.2: - name: mysql - url: https://mumoshu.github.io/charts/mysql-0.2.2.tgz - created: 2016-08-04 02:40:29.71841952 +0000 UTC - checksum: 6d6810e76a5987943faf0040ec22990d9fb141c7 - chartfile: - name: mysql - home: https://www.mysql.com/ - sources: [] - version: 0.2.2 - description: Chart running MySQL. - keywords: [] - maintainers: - - name: Matt Fisher - email: mfisher@deis.com - engine: "" diff --git a/pkg/resolver/resolver.go b/pkg/resolver/resolver.go deleted file mode 100644 index ec8ea2ccec0..00000000000 --- a/pkg/resolver/resolver.go +++ /dev/null @@ -1,153 +0,0 @@ -/* -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 resolver - -import ( - "bytes" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - "github.com/Masterminds/semver" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/helm/helmpath" - "k8s.io/helm/pkg/provenance" - "k8s.io/helm/pkg/repo" -) - -// Resolver resolves dependencies from semantic version ranges to a particular version. -type Resolver struct { - chartpath string - helmhome helmpath.Home -} - -// New creates a new resolver for a given chart and a given helm home. -func New(chartpath string, helmhome helmpath.Home) *Resolver { - return &Resolver{ - chartpath: chartpath, - helmhome: helmhome, - } -} - -// Resolve resolves dependencies and returns a lock file with the resolution. -func (r *Resolver) Resolve(reqs *chartutil.Requirements, repoNames map[string]string, d string) (*chartutil.RequirementsLock, error) { - - // Now we clone the dependencies, locking as we go. - locked := make([]*chartutil.Dependency, len(reqs.Dependencies)) - missing := []string{} - for i, d := range reqs.Dependencies { - if strings.HasPrefix(d.Repository, "file://") { - - if _, err := GetLocalPath(d.Repository, r.chartpath); err != nil { - return nil, err - } - - locked[i] = &chartutil.Dependency{ - Name: d.Name, - Repository: d.Repository, - Version: d.Version, - } - continue - } - constraint, err := semver.NewConstraint(d.Version) - if err != nil { - return nil, fmt.Errorf("dependency %q has an invalid version/constraint format: %s", d.Name, err) - } - - repoIndex, err := repo.LoadIndexFile(r.helmhome.CacheIndex(repoNames[d.Name])) - if err != nil { - return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) - } - - vs, ok := repoIndex.Entries[d.Name] - if !ok { - return nil, fmt.Errorf("%s chart not found in repo %s", d.Name, d.Repository) - } - - locked[i] = &chartutil.Dependency{ - Name: d.Name, - Repository: d.Repository, - } - found := false - // The version are already sorted and hence the first one to satisfy the constraint is used - for _, ver := range vs { - v, err := semver.NewVersion(ver.Version) - if err != nil || len(ver.URLs) == 0 { - // Not a legit entry. - continue - } - if constraint.Check(v) { - found = true - locked[i].Version = v.Original() - break - } - } - - if !found { - missing = append(missing, d.Name) - } - } - if len(missing) > 0 { - return nil, fmt.Errorf("Can't get a valid version for repositories %s. Try changing the version constraint in requirements.yaml", strings.Join(missing, ", ")) - } - return &chartutil.RequirementsLock{ - Generated: time.Now(), - Digest: d, - Dependencies: locked, - }, nil -} - -// HashReq generates a hash of the requirements. -// -// This should be used only to compare against another hash generated by this -// function. -func HashReq(req *chartutil.Requirements) (string, error) { - data, err := json.Marshal(req) - if err != nil { - return "", err - } - s, err := provenance.Digest(bytes.NewBuffer(data)) - return "sha256:" + s, err -} - -// GetLocalPath generates absolute local path when use -// "file://" in repository of requirements -func GetLocalPath(repo string, chartpath string) (string, error) { - var depPath string - var err error - p := strings.TrimPrefix(repo, "file://") - - // root path is absolute - if strings.HasPrefix(p, "/") { - if depPath, err = filepath.Abs(p); err != nil { - return "", err - } - } else { - depPath = filepath.Join(chartpath, p) - } - - if _, err = os.Stat(depPath); os.IsNotExist(err) { - return "", fmt.Errorf("directory %s not found", depPath) - } else if err != nil { - return "", err - } - - return depPath, nil -} diff --git a/pkg/resolver/resolver_test.go b/pkg/resolver/resolver_test.go deleted file mode 100644 index 78a0bc46c50..00000000000 --- a/pkg/resolver/resolver_test.go +++ /dev/null @@ -1,171 +0,0 @@ -/* -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 resolver - -import ( - "testing" - - "k8s.io/helm/pkg/chartutil" -) - -func TestResolve(t *testing.T) { - tests := []struct { - name string - req *chartutil.Requirements - expect *chartutil.RequirementsLock - err bool - }{ - { - name: "version failure", - req: &chartutil.Requirements{ - Dependencies: []*chartutil.Dependency{ - {Name: "oedipus-rex", Repository: "http://example.com", Version: ">a1"}, - }, - }, - err: true, - }, - { - name: "cache index failure", - req: &chartutil.Requirements{ - Dependencies: []*chartutil.Dependency{ - {Name: "oedipus-rex", Repository: "http://example.com", Version: "1.0.0"}, - }, - }, - err: true, - }, - { - name: "chart not found failure", - req: &chartutil.Requirements{ - Dependencies: []*chartutil.Dependency{ - {Name: "redis", Repository: "http://example.com", Version: "1.0.0"}, - }, - }, - err: true, - }, - { - name: "constraint not satisfied failure", - req: &chartutil.Requirements{ - Dependencies: []*chartutil.Dependency{ - {Name: "alpine", Repository: "http://example.com", Version: ">=1.0.0"}, - }, - }, - err: true, - }, - { - name: "valid lock", - req: &chartutil.Requirements{ - Dependencies: []*chartutil.Dependency{ - {Name: "alpine", Repository: "http://example.com", Version: ">=0.1.0"}, - }, - }, - expect: &chartutil.RequirementsLock{ - Dependencies: []*chartutil.Dependency{ - {Name: "alpine", Repository: "http://example.com", Version: "0.2.0"}, - }, - }, - }, - { - name: "repo from valid local path", - req: &chartutil.Requirements{ - Dependencies: []*chartutil.Dependency{ - {Name: "signtest", Repository: "file://../../../../cmd/helm/testdata/testcharts/signtest", Version: "0.1.0"}, - }, - }, - expect: &chartutil.RequirementsLock{ - Dependencies: []*chartutil.Dependency{ - {Name: "signtest", Repository: "file://../../../../cmd/helm/testdata/testcharts/signtest", Version: "0.1.0"}, - }, - }, - }, - { - name: "repo from invalid local path", - req: &chartutil.Requirements{ - Dependencies: []*chartutil.Dependency{ - {Name: "notexist", Repository: "file://../testdata/notexist", Version: "0.1.0"}, - }, - }, - err: true, - }, - } - - repoNames := map[string]string{"alpine": "kubernetes-charts", "redis": "kubernetes-charts"} - r := New("testdata/chartpath", "testdata/helmhome") - for _, tt := range tests { - hash, err := HashReq(tt.req) - if err != nil { - t.Fatal(err) - } - - l, err := r.Resolve(tt.req, repoNames, hash) - if err != nil { - if tt.err { - continue - } - t.Fatal(err) - } - - if tt.err { - t.Fatalf("Expected error in test %q", tt.name) - } - - if h, err := HashReq(tt.req); err != nil { - t.Fatal(err) - } else if h != l.Digest { - t.Errorf("%q: hashes don't match.", tt.name) - } - - // Check fields. - if len(l.Dependencies) != len(tt.req.Dependencies) { - t.Errorf("%s: wrong number of dependencies in lock", tt.name) - } - d0 := l.Dependencies[0] - e0 := tt.expect.Dependencies[0] - if d0.Name != e0.Name { - t.Errorf("%s: expected name %s, got %s", tt.name, e0.Name, d0.Name) - } - if d0.Repository != e0.Repository { - t.Errorf("%s: expected repo %s, got %s", tt.name, e0.Repository, d0.Repository) - } - if d0.Version != e0.Version { - t.Errorf("%s: expected version %s, got %s", tt.name, e0.Version, d0.Version) - } - } -} - -func TestHashReq(t *testing.T) { - expect := "sha256:e70e41f8922e19558a8bf62f591a8b70c8e4622e3c03e5415f09aba881f13885" - req := &chartutil.Requirements{ - Dependencies: []*chartutil.Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"}, - }, - } - h, err := HashReq(req) - if err != nil { - t.Fatal(err) - } - if expect != h { - t.Errorf("Expected %q, got %q", expect, h) - } - - req = &chartutil.Requirements{Dependencies: []*chartutil.Dependency{}} - h, err = HashReq(req) - if err != nil { - t.Fatal(err) - } - if expect == h { - t.Errorf("Expected %q != %q", expect, h) - } -} diff --git a/pkg/resolver/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml b/pkg/resolver/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml deleted file mode 100644 index e2d438701d8..00000000000 --- a/pkg/resolver/testdata/helmhome/repository/cache/kubernetes-charts-index.yaml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: v1 -entries: - alpine: - - name: alpine - urls: - - https://kubernetes-charts.storage.googleapis.com/alpine-0.1.0.tgz - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 0.2.0 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" - - name: alpine - urls: - - https://kubernetes-charts.storage.googleapis.com/alpine-0.2.0.tgz - checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d - home: https://k8s.io/helm - sources: - - https://github.com/kubernetes/helm - version: 0.1.0 - description: Deploy a basic Alpine Linux pod - keywords: [] - maintainers: [] - engine: "" - icon: "" - mariadb: - - name: mariadb - urls: - - https://kubernetes-charts.storage.googleapis.com/mariadb-0.3.0.tgz - checksum: 65229f6de44a2be9f215d11dbff311673fc8ba56 - home: https://mariadb.org - sources: - - https://github.com/bitnami/bitnami-docker-mariadb - version: 0.3.0 - description: Chart for MariaDB - keywords: - - mariadb - - mysql - - database - - sql - maintainers: - - name: Bitnami - email: containers@bitnami.com - engine: gotpl - icon: "" diff --git a/pkg/rudder/client.go b/pkg/rudder/client.go deleted file mode 100644 index 219bb010ac6..00000000000 --- a/pkg/rudder/client.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2017 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 rudder // import "k8s.io/helm/pkg/rudder" - -import ( - "fmt" - - "golang.org/x/net/context" - "google.golang.org/grpc" - - rudderAPI "k8s.io/helm/pkg/proto/hapi/rudder" -) - -// GrpcPort specifies port on which rudder will spawn a server -const ( - GrpcPort = 10001 -) - -var grpcAddr = fmt.Sprintf("127.0.0.1:%d", GrpcPort) - -// InstallRelease calls Rudder InstallRelease method which should create provided release -func InstallRelease(rel *rudderAPI.InstallReleaseRequest) (*rudderAPI.InstallReleaseResponse, error) { - //TODO(mkwiek): parametrize this - conn, err := grpc.Dial(grpcAddr, grpc.WithInsecure()) - if err != nil { - return nil, err - } - - defer conn.Close() - client := rudderAPI.NewReleaseModuleServiceClient(conn) - return client.InstallRelease(context.Background(), rel) -} - -// UpgradeRelease calls Rudder UpgradeRelease method which should perform update -func UpgradeRelease(req *rudderAPI.UpgradeReleaseRequest) (*rudderAPI.UpgradeReleaseResponse, error) { - conn, err := grpc.Dial(grpcAddr, grpc.WithInsecure()) - if err != nil { - return nil, err - } - defer conn.Close() - client := rudderAPI.NewReleaseModuleServiceClient(conn) - return client.UpgradeRelease(context.Background(), req) -} - -// RollbackRelease calls Rudder RollbackRelease method which should perform update -func RollbackRelease(req *rudderAPI.RollbackReleaseRequest) (*rudderAPI.RollbackReleaseResponse, error) { - conn, err := grpc.Dial(grpcAddr, grpc.WithInsecure()) - if err != nil { - return nil, err - } - defer conn.Close() - client := rudderAPI.NewReleaseModuleServiceClient(conn) - return client.RollbackRelease(context.Background(), req) -} - -// ReleaseStatus calls Rudder ReleaseStatus method which should perform update -func ReleaseStatus(req *rudderAPI.ReleaseStatusRequest) (*rudderAPI.ReleaseStatusResponse, error) { - conn, err := grpc.Dial(grpcAddr, grpc.WithInsecure()) - if err != nil { - return nil, err - } - defer conn.Close() - client := rudderAPI.NewReleaseModuleServiceClient(conn) - return client.ReleaseStatus(context.Background(), req) -} - -// DeleteRelease calls Rudder DeleteRelease method which should uninstall provided release -func DeleteRelease(rel *rudderAPI.DeleteReleaseRequest) (*rudderAPI.DeleteReleaseResponse, error) { - conn, err := grpc.Dial(grpcAddr, grpc.WithInsecure()) - if err != nil { - return nil, err - } - - defer conn.Close() - client := rudderAPI.NewReleaseModuleServiceClient(conn) - return client.DeleteRelease(context.Background(), rel) -} diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 51fa8f8f623..94c278875e9 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,22 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "k8s.io/helm/pkg/storage/driver" +package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( - "fmt" + "context" "strconv" "strings" "time" + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kblabels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/validation" - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) var _ Driver = (*ConfigMaps)(nil) @@ -40,13 +41,13 @@ const ConfigMapsDriverName = "ConfigMap" // ConfigMaps is a wrapper around an implementation of a kubernetes // ConfigMapsInterface. type ConfigMaps struct { - impl internalversion.ConfigMapInterface + impl corev1.ConfigMapInterface Log func(string, ...interface{}) } // NewConfigMaps initializes a new ConfigMaps wrapping an implementation of // the kubernetes ConfigMapsInterface. -func NewConfigMaps(impl internalversion.ConfigMapInterface) *ConfigMaps { +func NewConfigMaps(impl corev1.ConfigMapInterface) *ConfigMaps { return &ConfigMaps{ impl: impl, Log: func(_ string, _ ...interface{}) {}, @@ -62,10 +63,10 @@ func (cfgmaps *ConfigMaps) Name() string { // or error if not found. func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { // fetch the configmap holding the release named by key - obj, err := cfgmaps.impl.Get(key, metav1.GetOptions{}) + obj, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { - return nil, ErrReleaseNotFound(key) + return nil, ErrReleaseNotFound } cfgmaps.Log("get: failed to get %q: %s", key, err) @@ -85,10 +86,10 @@ func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { // that filter(release) == true. An error is returned if the // configmap fails to retrieve the releases. func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { - lsel := kblabels.Set{"OWNER": "TILLER"}.AsSelector() + lsel := kblabels.Set{"owner": "helm"}.AsSelector() opts := metav1.ListOptions{LabelSelector: lsel.String()} - list, err := cfgmaps.impl.List(opts) + list, err := cfgmaps.impl.List(context.Background(), opts) if err != nil { cfgmaps.Log("list: failed to list: %s", err) return nil, err @@ -104,6 +105,9 @@ func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Releas cfgmaps.Log("list: failed to decode release: %v: %s", item, err) continue } + + rls.Labels = item.ObjectMeta.Labels + if filter(rls) { results = append(results, rls) } @@ -117,21 +121,21 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err ls := kblabels.Set{} for k, v := range labels { if errs := validation.IsValidLabelValue(v); len(errs) != 0 { - return nil, fmt.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; ")) + return nil, errors.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; ")) } ls[k] = v } opts := metav1.ListOptions{LabelSelector: ls.AsSelector().String()} - list, err := cfgmaps.impl.List(opts) + list, err := cfgmaps.impl.List(context.Background(), opts) if err != nil { cfgmaps.Log("query: failed to query with labels: %s", err) return nil, err } if len(list.Items) == 0 { - return nil, ErrReleaseNotFound(labels["NAME"]) + return nil, ErrReleaseNotFound } var results []*rspb.Release @@ -153,7 +157,7 @@ func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { var lbs labels lbs.init() - lbs.set("CREATED_AT", strconv.Itoa(int(time.Now().Unix()))) + lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix()))) // create a new configmap to hold the release obj, err := newConfigMapsObject(key, rls, lbs) @@ -162,9 +166,9 @@ func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { return err } // push the configmap object out into the kubiverse - if _, err := cfgmaps.impl.Create(obj); err != nil { + if _, err := cfgmaps.impl.Create(context.Background(), obj, metav1.CreateOptions{}); err != nil { if apierrors.IsAlreadyExists(err) { - return ErrReleaseExists(key) + return ErrReleaseExists } cfgmaps.Log("create: failed to create: %s", err) @@ -180,7 +184,7 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { var lbs labels lbs.init() - lbs.set("MODIFIED_AT", strconv.Itoa(int(time.Now().Unix()))) + lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix()))) // create a new configmap object to hold the release obj, err := newConfigMapsObject(key, rls, lbs) @@ -189,7 +193,7 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { return err } // push the configmap object out into the kubiverse - _, err = cfgmaps.impl.Update(obj) + _, err = cfgmaps.impl.Update(context.Background(), obj, metav1.UpdateOptions{}) if err != nil { cfgmaps.Log("update: failed to update: %s", err) return err @@ -201,15 +205,10 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { // fetch the release to check existence if rls, err = cfgmaps.Get(key); err != nil { - if apierrors.IsNotFound(err) { - return nil, ErrReleaseExists(rls.Name) - } - - cfgmaps.Log("delete: failed to get release %q: %s", key, err) return nil, err } // delete the release - if err = cfgmaps.impl.Delete(key, &metav1.DeleteOptions{}); err != nil { + if err = cfgmaps.impl.Delete(context.Background(), key, metav1.DeleteOptions{}); err != nil { return rls, err } return rls, nil @@ -217,19 +216,19 @@ func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { // newConfigMapsObject constructs a kubernetes ConfigMap object // to store a release. Each configmap data entry is the base64 -// encoded string of a release's binary protobuf encoding. +// encoded gzipped string of a release. // // The following labels are used within each configmap: // -// "MODIFIED_AT" - timestamp indicating when this configmap was last modified. (set in Update) -// "CREATED_AT" - timestamp indicating when this configmap was created. (set in Create) -// "VERSION" - version of the release. -// "STATUS" - status of the release (see proto/hapi/release.status.pb.go for variants) -// "OWNER" - owner of the configmap, currently "TILLER". -// "NAME" - name of the release. +// "modifiedAt" - timestamp indicating when this configmap was last modified. (set in Update) +// "createdAt" - timestamp indicating when this configmap was created. (set in Create) +// "version" - version of the release. +// "status" - status of the release (see pkg/release/status.go for variants) +// "owner" - owner of the configmap, currently "helm". +// "name" - name of the release. // -func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*core.ConfigMap, error) { - const owner = "TILLER" +func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigMap, error) { + const owner = "helm" // encode the release s, err := encodeRelease(rls) @@ -242,13 +241,13 @@ func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*core.Confi } // apply labels - lbs.set("NAME", rls.Name) - lbs.set("OWNER", owner) - lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) - lbs.set("VERSION", strconv.Itoa(int(rls.Version))) + lbs.set("name", rls.Name) + lbs.set("owner", owner) + lbs.set("status", rls.Info.Status.String()) + lbs.set("version", strconv.Itoa(rls.Version)) // create and return configmap object - return &core.ConfigMap{ + return &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: key, Labels: lbs.toMap(), diff --git a/pkg/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index 7501ad9cbdf..626c36cb94d 100644 --- a/pkg/storage/driver/cfgmaps_test.go +++ b/pkg/storage/driver/cfgmaps_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -15,13 +15,13 @@ package driver import ( "encoding/base64" + "encoding/json" "reflect" "testing" - "github.com/gogo/protobuf/proto" - "k8s.io/kubernetes/pkg/apis/core" + v1 "k8s.io/api/core/v1" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) func TestConfigMapName(t *testing.T) { @@ -32,11 +32,11 @@ func TestConfigMapName(t *testing.T) { } func TestConfigMapGet(t *testing.T) { - vers := int32(1) + vers := 1 name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) @@ -47,29 +47,29 @@ func TestConfigMapGet(t *testing.T) { } // compare fetched release with original if !reflect.DeepEqual(rel, got) { - t.Errorf("Expected {%q}, got {%q}", rel, got) + t.Errorf("Expected {%v}, got {%v}", rel, got) } } -func TestUNcompressedConfigMapGet(t *testing.T) { - vers := int32(1) +func TestUncompressedConfigMapGet(t *testing.T) { + vers := 1 name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) // Create a test fixture which contains an uncompressed release cfgmap, err := newConfigMapsObject(key, rel, nil) if err != nil { t.Fatalf("Failed to create configmap: %s", err) } - b, err := proto.Marshal(rel) + b, err := json.Marshal(rel) if err != nil { t.Fatalf("Failed to marshal release: %s", err) } cfgmap.Data["release"] = base64.StdEncoding.EncodeToString(b) var mock MockConfigMapsInterface - mock.objects = map[string]*core.ConfigMap{key: cfgmap} + mock.objects = map[string]*v1.ConfigMap{key: cfgmap} cfgmaps := NewConfigMaps(&mock) // get release with key @@ -79,23 +79,23 @@ func TestUNcompressedConfigMapGet(t *testing.T) { } // compare fetched release with original if !reflect.DeepEqual(rel, got) { - t.Errorf("Expected {%q}, got {%q}", rel, got) + t.Errorf("Expected {%v}, got {%v}", rel, got) } } func TestConfigMapList(t *testing.T) { cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ - releaseStub("key-1", 1, "default", rspb.Status_DELETED), - releaseStub("key-2", 1, "default", rspb.Status_DELETED), - releaseStub("key-3", 1, "default", rspb.Status_DEPLOYED), - releaseStub("key-4", 1, "default", rspb.Status_DEPLOYED), - releaseStub("key-5", 1, "default", rspb.Status_SUPERSEDED), - releaseStub("key-6", 1, "default", rspb.Status_SUPERSEDED), + releaseStub("key-1", 1, "default", rspb.StatusUninstalled), + releaseStub("key-2", 1, "default", rspb.StatusUninstalled), + releaseStub("key-3", 1, "default", rspb.StatusDeployed), + releaseStub("key-4", 1, "default", rspb.StatusDeployed), + releaseStub("key-5", 1, "default", rspb.StatusSuperseded), + releaseStub("key-6", 1, "default", rspb.StatusSuperseded), }...) // list all deleted releases del, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_DELETED + return rel.Info.Status == rspb.StatusUninstalled }) // check if err != nil { @@ -107,7 +107,7 @@ func TestConfigMapList(t *testing.T) { // list all deployed releases dpl, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_DEPLOYED + return rel.Info.Status == rspb.StatusDeployed }) // check if err != nil { @@ -119,7 +119,7 @@ func TestConfigMapList(t *testing.T) { // list all superseded releases ssd, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_SUPERSEDED + return rel.Info.Status == rspb.StatusSuperseded }) // check if err != nil { @@ -130,14 +130,38 @@ func TestConfigMapList(t *testing.T) { } } +func TestConfigMapQuery(t *testing.T) { + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ + releaseStub("key-1", 1, "default", rspb.StatusUninstalled), + releaseStub("key-2", 1, "default", rspb.StatusUninstalled), + releaseStub("key-3", 1, "default", rspb.StatusDeployed), + releaseStub("key-4", 1, "default", rspb.StatusDeployed), + releaseStub("key-5", 1, "default", rspb.StatusSuperseded), + releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + }...) + + rls, err := cfgmaps.Query(map[string]string{"status": "deployed"}) + if err != nil { + t.Errorf("Failed to query: %s", err) + } + if len(rls) != 2 { + t.Errorf("Expected 2 results, got %d", len(rls)) + } + + _, err = cfgmaps.Query(map[string]string{"name": "notExist"}) + if err != ErrReleaseNotFound { + t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) + } +} + func TestConfigMapCreate(t *testing.T) { cfgmaps := newTestFixtureCfgMaps(t) - vers := int32(1) + vers := 1 name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) // store the release in a configmap if err := cfgmaps.Create(key, rel); err != nil { @@ -152,21 +176,21 @@ func TestConfigMapCreate(t *testing.T) { // compare created release with original if !reflect.DeepEqual(rel, got) { - t.Errorf("Expected {%q}, got {%q}", rel, got) + t.Errorf("Expected {%v}, got {%v}", rel, got) } } func TestConfigMapUpdate(t *testing.T) { - vers := int32(1) + vers := 1 name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) // modify release status code - rel.Info.Status.Code = rspb.Status_SUPERSEDED + rel.Info.Status = rspb.StatusSuperseded // perform the update if err := cfgmaps.Update(key, rel); err != nil { @@ -180,7 +204,38 @@ func TestConfigMapUpdate(t *testing.T) { } // check release has actually been updated by comparing modified fields - if rel.Info.Status.Code != got.Info.Status.Code { - t.Errorf("Expected status %s, got status %s", rel.Info.Status.Code, got.Info.Status.Code) + if rel.Info.Status != got.Info.Status { + t.Errorf("Expected status %s, got status %s", rel.Info.Status.String(), got.Info.Status.String()) + } +} + +func TestConfigMapDelete(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) + + // perform the delete on a non-existent release + _, err := cfgmaps.Delete("nonexistent") + if err != ErrReleaseNotFound { + t.Fatalf("Expected ErrReleaseNotFound: got {%v}", err) + } + + // perform the delete + rls, err := cfgmaps.Delete(key) + if err != nil { + t.Fatalf("Failed to delete release with key %q: %s", key, err) + } + if !reflect.DeepEqual(rel, rls) { + t.Errorf("Expected {%v}, got {%v}", rel, rls) + } + + // fetch the deleted release + _, err = cfgmaps.Get(key) + if !reflect.DeepEqual(ErrReleaseNotFound, err) { + t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) } } diff --git a/pkg/storage/driver/driver.go b/pkg/storage/driver/driver.go index e01d35d64fd..9c01f376609 100644 --- a/pkg/storage/driver/driver.go +++ b/pkg/storage/driver/driver.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,23 +14,46 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "k8s.io/helm/pkg/storage/driver" +package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( "fmt" - rspb "k8s.io/helm/pkg/proto/hapi/release" + "github.com/pkg/errors" + + rspb "helm.sh/helm/v3/pkg/release" ) var ( // ErrReleaseNotFound indicates that a release is not found. - ErrReleaseNotFound = func(release string) error { return fmt.Errorf("release: %q not found", release) } + ErrReleaseNotFound = errors.New("release: not found") // ErrReleaseExists indicates that a release already exists. - ErrReleaseExists = func(release string) error { return fmt.Errorf("release: %q already exists", release) } + ErrReleaseExists = errors.New("release: already exists") // ErrInvalidKey indicates that a release key could not be parsed. - ErrInvalidKey = func(release string) error { return fmt.Errorf("release: %q invalid key", release) } + ErrInvalidKey = errors.New("release: invalid key") + // ErrNoDeployedReleases indicates that there are no releases with the given key in the deployed state + ErrNoDeployedReleases = errors.New("has no deployed releases") ) +// StorageDriverError records an error and the release name that caused it +type StorageDriverError struct { + ReleaseName string + Err error +} + +func (e *StorageDriverError) Error() string { + return fmt.Sprintf("%q %s", e.ReleaseName, e.Err.Error()) +} + +func (e *StorageDriverError) Unwrap() error { return e.Err } + +func NewErrNoDeployedReleases(releaseName string) error { + return &StorageDriverError{ + ReleaseName: releaseName, + Err: ErrNoDeployedReleases, + } +} + // Creator is the interface that wraps the Create method. // // Create stores the release or returns ErrReleaseExists @@ -71,7 +94,7 @@ type Queryor interface { // Driver is the interface composed of Creator, Updator, Deletor, and Queryor // interfaces. It defines the behavior for storing, updating, deleted, -// and retrieving Tiller releases from some underlying storage mechanism, +// and retrieving Helm releases from some underlying storage mechanism, // e.g. memory, configmaps. type Driver interface { Creator diff --git a/pkg/storage/driver/labels.go b/pkg/storage/driver/labels.go index 8668d665bc9..eb7118fe5fa 100644 --- a/pkg/storage/driver/labels.go +++ b/pkg/storage/driver/labels.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/pkg/storage/driver/labels_test.go b/pkg/storage/driver/labels_test.go index af0bd24e549..bfd80911be9 100644 --- a/pkg/storage/driver/labels_test.go +++ b/pkg/storage/driver/labels_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "k8s.io/helm/pkg/storage/driver" +package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( "testing" diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index ceb0d67ddd3..91378f58852 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,23 +21,38 @@ import ( "strings" "sync" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) var _ Driver = (*Memory)(nil) -// MemoryDriverName is the string name of this driver. -const MemoryDriverName = "Memory" +const ( + // MemoryDriverName is the string name of this driver. + MemoryDriverName = "Memory" + + defaultNamespace = "default" +) + +// A map of release names to list of release records +type memReleases map[string]records // Memory is the in-memory storage driver implementation. type Memory struct { sync.RWMutex - cache map[string]records + namespace string + // A map of namespaces to releases + cache map[string]memReleases } // NewMemory initializes a new memory driver. func NewMemory() *Memory { - return &Memory{cache: map[string]records{}} + return &Memory{cache: map[string]memReleases{}, namespace: "default"} +} + +// SetNamespace sets a specific namespace in which releases will be accessed. +// An empty string indicates all namespaces (for the list operation) +func (mem *Memory) SetNamespace(ns string) { + mem.namespace = ns } // Name returns the name of the driver. @@ -49,20 +64,21 @@ func (mem *Memory) Name() string { func (mem *Memory) Get(key string) (*rspb.Release, error) { defer unlock(mem.rlock()) - switch elems := strings.Split(key, ".v"); len(elems) { + keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.") + switch elems := strings.Split(keyWithoutPrefix, ".v"); len(elems) { case 2: name, ver := elems[0], elems[1] if _, err := strconv.Atoi(ver); err != nil { - return nil, ErrInvalidKey(key) + return nil, ErrInvalidKey } - if recs, ok := mem.cache[name]; ok { + if recs, ok := mem.cache[mem.namespace][name]; ok { if r := recs.Get(key); r != nil { return r.rls, nil } } - return nil, ErrReleaseNotFound(key) + return nil, ErrReleaseNotFound default: - return nil, ErrInvalidKey(key) + return nil, ErrInvalidKey } } @@ -71,13 +87,23 @@ func (mem *Memory) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error defer unlock(mem.rlock()) var ls []*rspb.Release - for _, recs := range mem.cache { - recs.Iter(func(_ int, rec *record) bool { - if filter(rec.rls) { - ls = append(ls, rec.rls) - } - return true - }) + for namespace := range mem.cache { + if mem.namespace != "" { + // Should only list releases of this namespace + namespace = mem.namespace + } + for _, recs := range mem.cache[namespace] { + recs.Iter(func(_ int, rec *record) bool { + if filter(rec.rls) { + ls = append(ls, rec.rls) + } + return true + }) + } + if mem.namespace != "" { + // Should only list releases of this namespace + break + } } return ls, nil } @@ -92,19 +118,34 @@ func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { lbs.fromMap(keyvals) var ls []*rspb.Release - for _, recs := range mem.cache { - recs.Iter(func(_ int, rec *record) bool { - // A query for a release name that doesn't exist (has been deleted) - // can cause rec to be nil. - if rec == nil { - return false - } - if rec.lbs.match(lbs) { - ls = append(ls, rec.rls) - } - return true - }) + for namespace := range mem.cache { + if mem.namespace != "" { + // Should only query releases of this namespace + namespace = mem.namespace + } + for _, recs := range mem.cache[namespace] { + recs.Iter(func(_ int, rec *record) bool { + // A query for a release name that doesn't exist (has been deleted) + // can cause rec to be nil. + if rec == nil { + return false + } + if rec.lbs.match(lbs) { + ls = append(ls, rec.rls) + } + return true + }) + } + if mem.namespace != "" { + // Should only query releases of this namespace + break + } } + + if len(ls) == 0 { + return nil, ErrReleaseNotFound + } + return ls, nil } @@ -112,14 +153,25 @@ func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { func (mem *Memory) Create(key string, rls *rspb.Release) error { defer unlock(mem.wlock()) - if recs, ok := mem.cache[rls.Name]; ok { + // For backwards compatibility, we protect against an unset namespace + namespace := rls.Namespace + if namespace == "" { + namespace = defaultNamespace + } + mem.SetNamespace(namespace) + + if _, ok := mem.cache[namespace]; !ok { + mem.cache[namespace] = memReleases{} + } + + if recs, ok := mem.cache[namespace][rls.Name]; ok { if err := recs.Add(newRecord(key, rls)); err != nil { return err } - mem.cache[rls.Name] = recs + mem.cache[namespace][rls.Name] = recs return nil } - mem.cache[rls.Name] = records{newRecord(key, rls)} + mem.cache[namespace][rls.Name] = records{newRecord(key, rls)} return nil } @@ -127,35 +179,47 @@ func (mem *Memory) Create(key string, rls *rspb.Release) error { func (mem *Memory) Update(key string, rls *rspb.Release) error { defer unlock(mem.wlock()) - if rs, ok := mem.cache[rls.Name]; ok && rs.Exists(key) { - rs.Replace(key, newRecord(key, rls)) - return nil + // For backwards compatibility, we protect against an unset namespace + namespace := rls.Namespace + if namespace == "" { + namespace = defaultNamespace + } + mem.SetNamespace(namespace) + + if _, ok := mem.cache[namespace]; ok { + if rs, ok := mem.cache[namespace][rls.Name]; ok && rs.Exists(key) { + rs.Replace(key, newRecord(key, rls)) + return nil + } } - return ErrReleaseNotFound(rls.Name) + return ErrReleaseNotFound } // Delete deletes a release or returns ErrReleaseNotFound. func (mem *Memory) Delete(key string) (*rspb.Release, error) { defer unlock(mem.wlock()) - elems := strings.Split(key, ".v") + keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.") + elems := strings.Split(keyWithoutPrefix, ".v") if len(elems) != 2 { - return nil, ErrInvalidKey(key) + return nil, ErrInvalidKey } name, ver := elems[0], elems[1] if _, err := strconv.Atoi(ver); err != nil { - return nil, ErrInvalidKey(key) + return nil, ErrInvalidKey } - if recs, ok := mem.cache[name]; ok { - if r := recs.Remove(key); r != nil { - // recs.Remove changes the slice reference, so we have to re-assign it. - mem.cache[name] = recs - return r.rls, nil + if _, ok := mem.cache[mem.namespace]; ok { + if recs, ok := mem.cache[mem.namespace][name]; ok { + if r := recs.Remove(key); r != nil { + // recs.Remove changes the slice reference, so we have to re-assign it. + mem.cache[mem.namespace][name] = recs + return r.rls, nil + } } } - return nil, ErrReleaseNotFound(key) + return nil, ErrReleaseNotFound } // wlock locks mem for writing diff --git a/pkg/storage/driver/memory_test.go b/pkg/storage/driver/memory_test.go index 1062071e75d..7a2e8578e8a 100644 --- a/pkg/storage/driver/memory_test.go +++ b/pkg/storage/driver/memory_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import ( "reflect" "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) func TestMemoryName(t *testing.T) { @@ -37,13 +37,23 @@ func TestMemoryCreate(t *testing.T) { err bool }{ { - "create should success", - releaseStub("rls-c", 1, "default", rspb.Status_DEPLOYED), + "create should succeed", + releaseStub("rls-c", 1, "default", rspb.StatusDeployed), false, }, { "create should fail (release already exists)", - releaseStub("rls-a", 1, "default", rspb.Status_DEPLOYED), + releaseStub("rls-a", 1, "default", rspb.StatusDeployed), + true, + }, + { + "create in namespace should succeed", + releaseStub("rls-a", 1, "mynamespace", rspb.StatusDeployed), + false, + }, + { + "create in other namespace should fail (release already exists)", + releaseStub("rls-c", 1, "mynamespace", rspb.StatusDeployed), true, }, } @@ -57,45 +67,103 @@ func TestMemoryCreate(t *testing.T) { if !tt.err { t.Fatalf("failed to create %q: %s", tt.desc, err) } + } else if tt.err { + t.Fatalf("Did not get expected error for %q\n", tt.desc) } } } func TestMemoryGet(t *testing.T) { var tests = []struct { - desc string - key string - err bool + desc string + key string + namespace string + err bool }{ - {"release key should exist", "rls-a.v1", false}, - {"release key should not exist", "rls-a.v5", true}, + {"release key should exist", "rls-a.v1", "default", false}, + {"release key should not exist", "rls-a.v5", "default", true}, + {"release key in namespace should exist", "rls-c.v1", "mynamespace", false}, + {"release key in namespace should not exist", "rls-a.v1", "mynamespace", true}, } ts := tsFixtureMemory(t) for _, tt := range tests { + ts.SetNamespace(tt.namespace) if _, err := ts.Get(tt.key); err != nil { if !tt.err { t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) } + } else if tt.err { + t.Fatalf("Did not get expected error for %q '%s'\n", tt.desc, tt.key) } } } +func TestMemoryList(t *testing.T) { + ts := tsFixtureMemory(t) + ts.SetNamespace("default") + + // list all deployed releases + dpl, err := ts.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusDeployed + }) + // check + if err != nil { + t.Errorf("Failed to list deployed releases: %s", err) + } + if len(dpl) != 2 { + t.Errorf("Expected 2 deployed, got %d", len(dpl)) + } + + // list all superseded releases + ssd, err := ts.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusSuperseded + }) + // check + if err != nil { + t.Errorf("Failed to list superseded releases: %s", err) + } + if len(ssd) != 6 { + t.Errorf("Expected 6 superseded, got %d", len(ssd)) + } + + // list all deleted releases + del, err := ts.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusUninstalled + }) + // check + if err != nil { + t.Errorf("Failed to list deleted releases: %s", err) + } + if len(del) != 0 { + t.Errorf("Expected 0 deleted, got %d", len(del)) + } +} + func TestMemoryQuery(t *testing.T) { var tests = []struct { - desc string - xlen int - lbs map[string]string + desc string + xlen int + namespace string + lbs map[string]string }{ { "should be 2 query results", 2, - map[string]string{"STATUS": "DEPLOYED"}, + "default", + map[string]string{"status": "deployed"}, + }, + { + "should be 1 query result", + 1, + "mynamespace", + map[string]string{"status": "deployed"}, }, } ts := tsFixtureMemory(t) for _, tt := range tests { + ts.SetNamespace(tt.namespace) l, err := ts.Query(tt.lbs) if err != nil { t.Fatalf("Failed to query: %s\n", err) @@ -117,13 +185,25 @@ func TestMemoryUpdate(t *testing.T) { { "update release status", "rls-a.v4", - releaseStub("rls-a", 4, "default", rspb.Status_SUPERSEDED), + releaseStub("rls-a", 4, "default", rspb.StatusSuperseded), false, }, { "update release does not exist", - "rls-z.v1", - releaseStub("rls-z", 1, "default", rspb.Status_DELETED), + "rls-c.v1", + releaseStub("rls-c", 1, "default", rspb.StatusUninstalled), + true, + }, + { + "update release status in namespace", + "rls-c.v4", + releaseStub("rls-c", 4, "mynamespace", rspb.StatusSuperseded), + false, + }, + { + "update release in namespace does not exist", + "rls-a.v1", + releaseStub("rls-a", 1, "mynamespace", rspb.StatusUninstalled), true, }, } @@ -135,41 +215,53 @@ func TestMemoryUpdate(t *testing.T) { t.Fatalf("Failed %q: %s\n", tt.desc, err) } continue + } else if tt.err { + t.Fatalf("Did not get expected error for %q '%s'\n", tt.desc, tt.key) } + ts.SetNamespace(tt.rls.Namespace) r, err := ts.Get(tt.key) if err != nil { t.Fatalf("Failed to get: %s\n", err) } if !reflect.DeepEqual(r, tt.rls) { - t.Fatalf("Expected %s, actual %s\n", tt.rls, r) + t.Fatalf("Expected %v, actual %v\n", tt.rls, r) } } } func TestMemoryDelete(t *testing.T) { var tests = []struct { - desc string - key string - err bool + desc string + key string + namespace string + err bool }{ - {"release key should exist", "rls-a.v1", false}, - {"release key should not exist", "rls-a.v5", true}, + {"release key should exist", "rls-a.v4", "default", false}, + {"release key should not exist", "rls-a.v5", "default", true}, + {"release key from other namespace should not exist", "rls-c.v4", "default", true}, + {"release key from namespace should exist", "rls-c.v4", "mynamespace", false}, + {"release key from namespace should not exist", "rls-c.v5", "mynamespace", true}, + {"release key from namespace2 should not exist", "rls-a.v4", "mynamespace", true}, } ts := tsFixtureMemory(t) - start, err := ts.Query(map[string]string{"NAME": "rls-a"}) + ts.SetNamespace("") + start, err := ts.Query(map[string]string{"status": "deployed"}) if err != nil { t.Errorf("Query failed: %s", err) } startLen := len(start) for _, tt := range tests { + ts.SetNamespace(tt.namespace) if rel, err := ts.Delete(tt.key); err != nil { if !tt.err { t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) } continue + } else if tt.err { + t.Fatalf("Did not get expected error for %q '%s'\n", tt.desc, tt.key) } else if fmt.Sprintf("%s.v%d", rel.Name, rel.Version) != tt.key { t.Fatalf("Asked for delete on %s, but deleted %d", tt.key, rel.Version) } @@ -180,14 +272,15 @@ func TestMemoryDelete(t *testing.T) { } // Make sure that the deleted records are gone. - end, err := ts.Query(map[string]string{"NAME": "rls-a"}) + ts.SetNamespace("") + end, err := ts.Query(map[string]string{"status": "deployed"}) if err != nil { t.Errorf("Query failed: %s", err) } endLen := len(end) - if startLen <= endLen { - t.Errorf("expected start %d to be less than end %d", startLen, endLen) + if startLen-2 != endLen { + t.Errorf("expected end to be %d instead of %d", startLen-2, endLen) for _, ee := range end { t.Logf("Name: %s, Version: %d", ee.Name, ee.Version) } diff --git a/pkg/storage/driver/mock_test.go b/pkg/storage/driver/mock_test.go index 979d11cb6f2..c0236ece852 100644 --- a/pkg/storage/driver/mock_test.go +++ b/pkg/storage/driver/mock_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,45 +14,56 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "k8s.io/helm/pkg/storage/driver" +package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( + "context" "fmt" "testing" + sqlmock "github.com/DATA-DOG/go-sqlmock" + sq "github.com/Masterminds/squirrel" + "github.com/jmoiron/sqlx" + + v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" + kblabels "k8s.io/apimachinery/pkg/labels" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) -func releaseStub(name string, vers int32, namespace string, code rspb.Status_Code) *rspb.Release { +func releaseStub(name string, vers int, namespace string, status rspb.Status) *rspb.Release { return &rspb.Release{ Name: name, Version: vers, Namespace: namespace, - Info: &rspb.Info{Status: &rspb.Status{Code: code}}, + Info: &rspb.Info{Status: status}, } } -func testKey(name string, vers int32) string { +func testKey(name string, vers int) string { return fmt.Sprintf("%s.v%d", name, vers) } func tsFixtureMemory(t *testing.T) *Memory { hs := []*rspb.Release{ // rls-a - releaseStub("rls-a", 4, "default", rspb.Status_DEPLOYED), - releaseStub("rls-a", 1, "default", rspb.Status_SUPERSEDED), - releaseStub("rls-a", 3, "default", rspb.Status_SUPERSEDED), - releaseStub("rls-a", 2, "default", rspb.Status_SUPERSEDED), + releaseStub("rls-a", 4, "default", rspb.StatusDeployed), + releaseStub("rls-a", 1, "default", rspb.StatusSuperseded), + releaseStub("rls-a", 3, "default", rspb.StatusSuperseded), + releaseStub("rls-a", 2, "default", rspb.StatusSuperseded), // rls-b - releaseStub("rls-b", 4, "default", rspb.Status_DEPLOYED), - releaseStub("rls-b", 1, "default", rspb.Status_SUPERSEDED), - releaseStub("rls-b", 3, "default", rspb.Status_SUPERSEDED), - releaseStub("rls-b", 2, "default", rspb.Status_SUPERSEDED), + releaseStub("rls-b", 4, "default", rspb.StatusDeployed), + releaseStub("rls-b", 1, "default", rspb.StatusSuperseded), + releaseStub("rls-b", 3, "default", rspb.StatusSuperseded), + releaseStub("rls-b", 2, "default", rspb.StatusSuperseded), + // rls-c in other namespace + releaseStub("rls-c", 4, "mynamespace", rspb.StatusDeployed), + releaseStub("rls-c", 1, "mynamespace", rspb.StatusSuperseded), + releaseStub("rls-c", 3, "mynamespace", rspb.StatusSuperseded), + releaseStub("rls-c", 2, "mynamespace", rspb.StatusSuperseded), } mem := NewMemory() @@ -76,14 +87,14 @@ func newTestFixtureCfgMaps(t *testing.T, releases ...*rspb.Release) *ConfigMaps // MockConfigMapsInterface mocks a kubernetes ConfigMapsInterface type MockConfigMapsInterface struct { - internalversion.ConfigMapInterface + corev1.ConfigMapInterface - objects map[string]*core.ConfigMap + objects map[string]*v1.ConfigMap } // Init initializes the MockConfigMapsInterface with the set of releases. func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Release) { - mock.objects = map[string]*core.ConfigMap{} + mock.objects = map[string]*v1.ConfigMap{} for _, rls := range releases { objkey := testKey(rls.Name, rls.Version) @@ -97,47 +108,55 @@ func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Releas } // Get returns the ConfigMap by name. -func (mock *MockConfigMapsInterface) Get(name string, options metav1.GetOptions) (*core.ConfigMap, error) { +func (mock *MockConfigMapsInterface) Get(_ context.Context, name string, _ metav1.GetOptions) (*v1.ConfigMap, error) { object, ok := mock.objects[name] if !ok { - return nil, apierrors.NewNotFound(core.Resource("tests"), name) + return nil, apierrors.NewNotFound(v1.Resource("tests"), name) } return object, nil } // List returns the a of ConfigMaps. -func (mock *MockConfigMapsInterface) List(opts metav1.ListOptions) (*core.ConfigMapList, error) { - var list core.ConfigMapList +func (mock *MockConfigMapsInterface) List(_ context.Context, opts metav1.ListOptions) (*v1.ConfigMapList, error) { + var list v1.ConfigMapList + + labelSelector, err := kblabels.Parse(opts.LabelSelector) + if err != nil { + return nil, err + } + for _, cfgmap := range mock.objects { - list.Items = append(list.Items, *cfgmap) + if labelSelector.Matches(kblabels.Set(cfgmap.ObjectMeta.Labels)) { + list.Items = append(list.Items, *cfgmap) + } } return &list, nil } // Create creates a new ConfigMap. -func (mock *MockConfigMapsInterface) Create(cfgmap *core.ConfigMap) (*core.ConfigMap, error) { +func (mock *MockConfigMapsInterface) Create(_ context.Context, cfgmap *v1.ConfigMap, _ metav1.CreateOptions) (*v1.ConfigMap, error) { name := cfgmap.ObjectMeta.Name if object, ok := mock.objects[name]; ok { - return object, apierrors.NewAlreadyExists(core.Resource("tests"), name) + return object, apierrors.NewAlreadyExists(v1.Resource("tests"), name) } mock.objects[name] = cfgmap return cfgmap, nil } // Update updates a ConfigMap. -func (mock *MockConfigMapsInterface) Update(cfgmap *core.ConfigMap) (*core.ConfigMap, error) { +func (mock *MockConfigMapsInterface) Update(_ context.Context, cfgmap *v1.ConfigMap, _ metav1.UpdateOptions) (*v1.ConfigMap, error) { name := cfgmap.ObjectMeta.Name if _, ok := mock.objects[name]; !ok { - return nil, apierrors.NewNotFound(core.Resource("tests"), name) + return nil, apierrors.NewNotFound(v1.Resource("tests"), name) } mock.objects[name] = cfgmap return cfgmap, nil } // Delete deletes a ConfigMap by name. -func (mock *MockConfigMapsInterface) Delete(name string, opts *metav1.DeleteOptions) error { +func (mock *MockConfigMapsInterface) Delete(_ context.Context, name string, _ metav1.DeleteOptions) error { if _, ok := mock.objects[name]; !ok { - return apierrors.NewNotFound(core.Resource("tests"), name) + return apierrors.NewNotFound(v1.Resource("tests"), name) } delete(mock.objects, name) return nil @@ -154,14 +173,14 @@ func newTestFixtureSecrets(t *testing.T, releases ...*rspb.Release) *Secrets { // MockSecretsInterface mocks a kubernetes SecretsInterface type MockSecretsInterface struct { - internalversion.SecretInterface + corev1.SecretInterface - objects map[string]*core.Secret + objects map[string]*v1.Secret } // Init initializes the MockSecretsInterface with the set of releases. func (mock *MockSecretsInterface) Init(t *testing.T, releases ...*rspb.Release) { - mock.objects = map[string]*core.Secret{} + mock.objects = map[string]*v1.Secret{} for _, rls := range releases { objkey := testKey(rls.Name, rls.Version) @@ -175,48 +194,72 @@ func (mock *MockSecretsInterface) Init(t *testing.T, releases ...*rspb.Release) } // Get returns the Secret by name. -func (mock *MockSecretsInterface) Get(name string, options metav1.GetOptions) (*core.Secret, error) { +func (mock *MockSecretsInterface) Get(_ context.Context, name string, _ metav1.GetOptions) (*v1.Secret, error) { object, ok := mock.objects[name] if !ok { - return nil, apierrors.NewNotFound(core.Resource("tests"), name) + return nil, apierrors.NewNotFound(v1.Resource("tests"), name) } return object, nil } // List returns the a of Secret. -func (mock *MockSecretsInterface) List(opts metav1.ListOptions) (*core.SecretList, error) { - var list core.SecretList +func (mock *MockSecretsInterface) List(_ context.Context, opts metav1.ListOptions) (*v1.SecretList, error) { + var list v1.SecretList + + labelSelector, err := kblabels.Parse(opts.LabelSelector) + if err != nil { + return nil, err + } + for _, secret := range mock.objects { - list.Items = append(list.Items, *secret) + if labelSelector.Matches(kblabels.Set(secret.ObjectMeta.Labels)) { + list.Items = append(list.Items, *secret) + } } return &list, nil } // Create creates a new Secret. -func (mock *MockSecretsInterface) Create(secret *core.Secret) (*core.Secret, error) { +func (mock *MockSecretsInterface) Create(_ context.Context, secret *v1.Secret, _ metav1.CreateOptions) (*v1.Secret, error) { name := secret.ObjectMeta.Name if object, ok := mock.objects[name]; ok { - return object, apierrors.NewAlreadyExists(core.Resource("tests"), name) + return object, apierrors.NewAlreadyExists(v1.Resource("tests"), name) } mock.objects[name] = secret return secret, nil } // Update updates a Secret. -func (mock *MockSecretsInterface) Update(secret *core.Secret) (*core.Secret, error) { +func (mock *MockSecretsInterface) Update(_ context.Context, secret *v1.Secret, _ metav1.UpdateOptions) (*v1.Secret, error) { name := secret.ObjectMeta.Name if _, ok := mock.objects[name]; !ok { - return nil, apierrors.NewNotFound(core.Resource("tests"), name) + return nil, apierrors.NewNotFound(v1.Resource("tests"), name) } mock.objects[name] = secret return secret, nil } // Delete deletes a Secret by name. -func (mock *MockSecretsInterface) Delete(name string, opts *metav1.DeleteOptions) error { +func (mock *MockSecretsInterface) Delete(_ context.Context, name string, _ metav1.DeleteOptions) error { if _, ok := mock.objects[name]; !ok { - return apierrors.NewNotFound(core.Resource("tests"), name) + return apierrors.NewNotFound(v1.Resource("tests"), name) } delete(mock.objects, name) return nil } + +// newTestFixtureSQL mocks the SQL database (for testing purposes) +func newTestFixtureSQL(t *testing.T, releases ...*rspb.Release) (*SQL, sqlmock.Sqlmock) { + sqlDB, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("error when opening stub database connection: %v", err) + } + + sqlxDB := sqlx.NewDb(sqlDB, "sqlmock") + return &SQL{ + db: sqlxDB, + Log: func(a string, b ...interface{}) {}, + namespace: "default", + statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar), + }, mock +} diff --git a/pkg/storage/driver/records.go b/pkg/storage/driver/records.go index ce72308a884..9df173384c7 100644 --- a/pkg/storage/driver/records.go +++ b/pkg/storage/driver/records.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,15 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "k8s.io/helm/pkg/storage/driver" +package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( "sort" "strconv" - "github.com/golang/protobuf/proto" - - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) // records holds a list of in-memory release records @@ -38,7 +36,7 @@ func (rs *records) Add(r *record) error { } if rs.Exists(r.key) { - return ErrReleaseExists(r.key) + return ErrReleaseExists } *rs = append(*rs, r) @@ -95,16 +93,6 @@ func (rs *records) Replace(key string, rec *record) *record { return nil } -func (rs records) FindByVersion(vers int32) (int, bool) { - i := sort.Search(len(rs), func(i int) bool { - return rs[i].rls.Version == vers - }) - if i < len(rs) && rs[i].rls.Version == vers { - return i, true - } - return i, false -} - func (rs *records) removeAt(index int) *record { r := (*rs)[index] (*rs)[index] = nil @@ -126,10 +114,11 @@ func newRecord(key string, rls *rspb.Release) *record { var lbs labels lbs.init() - lbs.set("NAME", rls.Name) - lbs.set("OWNER", "TILLER") - lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) - lbs.set("VERSION", strconv.Itoa(int(rls.Version))) + lbs.set("name", rls.Name) + lbs.set("owner", "helm") + lbs.set("status", rls.Info.Status.String()) + lbs.set("version", strconv.Itoa(rls.Version)) - return &record{key: key, lbs: lbs, rls: proto.Clone(rls).(*rspb.Release)} + // return &record{key: key, lbs: lbs, rls: proto.Clone(rls).(*rspb.Release)} + return &record{key: key, lbs: lbs, rls: rls} } diff --git a/pkg/storage/driver/records_test.go b/pkg/storage/driver/records_test.go index 79380afb8a4..0a27839cc99 100644 --- a/pkg/storage/driver/records_test.go +++ b/pkg/storage/driver/records_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,18 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "k8s.io/helm/pkg/storage/driver" +package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( + "reflect" "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) func TestRecordsAdd(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.Status_SUPERSEDED)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.Status_DEPLOYED)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), }) var tests = []struct { @@ -38,13 +39,13 @@ func TestRecordsAdd(t *testing.T) { "add valid key", "rls-a.v3", false, - newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.Status_SUPERSEDED)), + newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.StatusSuperseded)), }, { "add already existing key", "rls-a.v1", true, - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.Status_DEPLOYED)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusDeployed)), }, } @@ -69,8 +70,8 @@ func TestRecordsRemove(t *testing.T) { } rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.Status_SUPERSEDED)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.Status_DEPLOYED)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), }) startLen := rs.Len() @@ -97,8 +98,8 @@ func TestRecordsRemove(t *testing.T) { func TestRecordsRemoveAt(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.Status_SUPERSEDED)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.Status_DEPLOYED)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), }) if len(rs) != 2 { @@ -110,3 +111,130 @@ func TestRecordsRemoveAt(t *testing.T) { t.Fatalf("Expected length of rs to be 1, got %d", len(rs)) } } + +func TestRecordsGet(t *testing.T) { + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + }) + + var tests = []struct { + desc string + key string + rec *record + }{ + { + "get valid key", + "rls-a.v1", + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + }, + { + "get invalid key", + "rls-a.v3", + nil, + }, + } + + for _, tt := range tests { + got := rs.Get(tt.key) + if !reflect.DeepEqual(tt.rec, got) { + t.Fatalf("Expected %v, got %v", tt.rec, got) + } + } +} + +func TestRecordsIndex(t *testing.T) { + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + }) + + var tests = []struct { + desc string + key string + sort int + }{ + { + "get valid key", + "rls-a.v1", + 0, + }, + { + "get invalid key", + "rls-a.v3", + -1, + }, + } + + for _, tt := range tests { + got, _ := rs.Index(tt.key) + if got != tt.sort { + t.Fatalf("Expected %d, got %d", tt.sort, got) + } + } +} + +func TestRecordsExists(t *testing.T) { + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + }) + + var tests = []struct { + desc string + key string + ok bool + }{ + { + "get valid key", + "rls-a.v1", + true, + }, + { + "get invalid key", + "rls-a.v3", + false, + }, + } + + for _, tt := range tests { + got := rs.Exists(tt.key) + if got != tt.ok { + t.Fatalf("Expected %t, got %t", tt.ok, got) + } + } +} + +func TestRecordsReplace(t *testing.T) { + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + }) + + var tests = []struct { + desc string + key string + rec *record + expected *record + }{ + { + "replace with existing key", + "rls-a.v2", + newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + }, + { + "replace with non existing key", + "rls-a.v4", + newRecord("rls-a.v4", releaseStub("rls-a", 4, "default", rspb.StatusDeployed)), + nil, + }, + } + + for _, tt := range tests { + got := rs.Replace(tt.key, tt.rec) + if !reflect.DeepEqual(tt.expected, got) { + t.Fatalf("Expected %v, got %v", tt.expected, got) + } + } +} diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index e8f3984f65a..2e8530d0ca8 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,22 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "k8s.io/helm/pkg/storage/driver" +package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( - "fmt" + "context" "strconv" "strings" "time" + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kblabels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/validation" - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) var _ Driver = (*Secrets)(nil) @@ -40,13 +41,13 @@ const SecretsDriverName = "Secret" // Secrets is a wrapper around an implementation of a kubernetes // SecretsInterface. type Secrets struct { - impl internalversion.SecretInterface + impl corev1.SecretInterface Log func(string, ...interface{}) } -// NewSecrets initializes a new Secrets wrapping an implmenetation of +// NewSecrets initializes a new Secrets wrapping an implementation of // the kubernetes SecretsInterface. -func NewSecrets(impl internalversion.SecretInterface) *Secrets { +func NewSecrets(impl corev1.SecretInterface) *Secrets { return &Secrets{ impl: impl, Log: func(_ string, _ ...interface{}) {}, @@ -62,36 +63,28 @@ func (secrets *Secrets) Name() string { // or error if not found. func (secrets *Secrets) Get(key string) (*rspb.Release, error) { // fetch the secret holding the release named by key - obj, err := secrets.impl.Get(key, metav1.GetOptions{}) + obj, err := secrets.impl.Get(context.Background(), key, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { - return nil, ErrReleaseNotFound(key) + return nil, ErrReleaseNotFound } - - secrets.Log("get: failed to get %q: %s", key, err) - return nil, err + return nil, errors.Wrapf(err, "get: failed to get %q", key) } // found the secret, decode the base64 data string r, err := decodeRelease(string(obj.Data["release"])) - if err != nil { - secrets.Log("get: failed to decode data %q: %s", key, err) - return nil, err - } - // return the release object - return r, nil + return r, errors.Wrapf(err, "get: failed to decode data %q", key) } // List fetches all releases and returns the list releases such // that filter(release) == true. An error is returned if the // secret fails to retrieve the releases. func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { - lsel := kblabels.Set{"OWNER": "TILLER"}.AsSelector() + lsel := kblabels.Set{"owner": "helm"}.AsSelector() opts := metav1.ListOptions{LabelSelector: lsel.String()} - list, err := secrets.impl.List(opts) + list, err := secrets.impl.List(context.Background(), opts) if err != nil { - secrets.Log("list: failed to list: %s", err) - return nil, err + return nil, errors.Wrap(err, "list: failed to list") } var results []*rspb.Release @@ -104,6 +97,9 @@ func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, secrets.Log("list: failed to decode release: %v: %s", item, err) continue } + + rls.Labels = item.ObjectMeta.Labels + if filter(rls) { results = append(results, rls) } @@ -117,21 +113,20 @@ func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) ls := kblabels.Set{} for k, v := range labels { if errs := validation.IsValidLabelValue(v); len(errs) != 0 { - return nil, fmt.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; ")) + return nil, errors.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; ")) } ls[k] = v } opts := metav1.ListOptions{LabelSelector: ls.AsSelector().String()} - list, err := secrets.impl.List(opts) + list, err := secrets.impl.List(context.Background(), opts) if err != nil { - secrets.Log("query: failed to query with labels: %s", err) - return nil, err + return nil, errors.Wrap(err, "query: failed to query with labels") } if len(list.Items) == 0 { - return nil, ErrReleaseNotFound(labels["NAME"]) + return nil, ErrReleaseNotFound } var results []*rspb.Release @@ -153,22 +148,20 @@ func (secrets *Secrets) Create(key string, rls *rspb.Release) error { var lbs labels lbs.init() - lbs.set("CREATED_AT", strconv.Itoa(int(time.Now().Unix()))) + lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix()))) // create a new secret to hold the release obj, err := newSecretsObject(key, rls, lbs) if err != nil { - secrets.Log("create: failed to encode release %q: %s", rls.Name, err) - return err + return errors.Wrapf(err, "create: failed to encode release %q", rls.Name) } // push the secret object out into the kubiverse - if _, err := secrets.impl.Create(obj); err != nil { + if _, err := secrets.impl.Create(context.Background(), obj, metav1.CreateOptions{}); err != nil { if apierrors.IsAlreadyExists(err) { - return ErrReleaseExists(rls.Name) + return ErrReleaseExists } - secrets.Log("create: failed to create: %s", err) - return err + return errors.Wrap(err, "create: failed to create") } return nil } @@ -180,56 +173,44 @@ func (secrets *Secrets) Update(key string, rls *rspb.Release) error { var lbs labels lbs.init() - lbs.set("MODIFIED_AT", strconv.Itoa(int(time.Now().Unix()))) + lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix()))) // create a new secret object to hold the release obj, err := newSecretsObject(key, rls, lbs) if err != nil { - secrets.Log("update: failed to encode release %q: %s", rls.Name, err) - return err + return errors.Wrapf(err, "update: failed to encode release %q", rls.Name) } // push the secret object out into the kubiverse - _, err = secrets.impl.Update(obj) - if err != nil { - secrets.Log("update: failed to update: %s", err) - return err - } - return nil + _, err = secrets.impl.Update(context.Background(), obj, metav1.UpdateOptions{}) + return errors.Wrap(err, "update: failed to update") } // Delete deletes the Secret holding the release named by key. func (secrets *Secrets) Delete(key string) (rls *rspb.Release, err error) { // fetch the release to check existence if rls, err = secrets.Get(key); err != nil { - if apierrors.IsNotFound(err) { - return nil, ErrReleaseExists(rls.Name) - } - - secrets.Log("delete: failed to get release %q: %s", key, err) return nil, err } // delete the release - if err = secrets.impl.Delete(key, &metav1.DeleteOptions{}); err != nil { - return rls, err - } - return rls, nil + err = secrets.impl.Delete(context.Background(), key, metav1.DeleteOptions{}) + return rls, err } // newSecretsObject constructs a kubernetes Secret object // to store a release. Each secret data entry is the base64 -// encoded string of a release's binary protobuf encoding. +// encoded gzipped string of a release. // // The following labels are used within each secret: // -// "MODIFIED_AT" - timestamp indicating when this secret was last modified. (set in Update) -// "CREATED_AT" - timestamp indicating when this secret was created. (set in Create) -// "VERSION" - version of the release. -// "STATUS" - status of the release (see proto/hapi/release.status.pb.go for variants) -// "OWNER" - owner of the secret, currently "TILLER". -// "NAME" - name of the release. +// "modifiedAt" - timestamp indicating when this secret was last modified. (set in Update) +// "createdAt" - timestamp indicating when this secret was created. (set in Create) +// "version" - version of the release. +// "status" - status of the release (see pkg/release/status.go for variants) +// "owner" - owner of the secret, currently "helm". +// "name" - name of the release. // -func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*core.Secret, error) { - const owner = "TILLER" +func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*v1.Secret, error) { + const owner = "helm" // encode the release s, err := encodeRelease(rls) @@ -242,17 +223,28 @@ func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*core.Secret, } // apply labels - lbs.set("NAME", rls.Name) - lbs.set("OWNER", owner) - lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) - lbs.set("VERSION", strconv.Itoa(int(rls.Version))) - - // create and return secret object - return &core.Secret{ + lbs.set("name", rls.Name) + lbs.set("owner", owner) + lbs.set("status", rls.Info.Status.String()) + lbs.set("version", strconv.Itoa(rls.Version)) + + // create and return secret object. + // Helm 3 introduced setting the 'Type' field + // in the Kubernetes storage object. + // Helm defines the field content as follows: + // /.v + // Type field for Helm 3: helm.sh/release.v1 + // Note: Version starts at 'v1' for Helm 3 and + // should be incremented if the release object + // metadata is modified. + // This would potentially be a breaking change + // and should only happen between major versions. + return &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: key, Labels: lbs.toMap(), }, + Type: "helm.sh/release.v1", Data: map[string][]byte{"release": []byte(s)}, }, nil } diff --git a/pkg/storage/driver/secrets_test.go b/pkg/storage/driver/secrets_test.go index e6f62e70201..d509c7b3a04 100644 --- a/pkg/storage/driver/secrets_test.go +++ b/pkg/storage/driver/secrets_test.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -15,13 +15,13 @@ package driver import ( "encoding/base64" + "encoding/json" "reflect" "testing" - "github.com/gogo/protobuf/proto" - "k8s.io/kubernetes/pkg/apis/core" + v1 "k8s.io/api/core/v1" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) func TestSecretName(t *testing.T) { @@ -32,11 +32,11 @@ func TestSecretName(t *testing.T) { } func TestSecretGet(t *testing.T) { - vers := int32(1) + vers := 1 name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) @@ -47,29 +47,29 @@ func TestSecretGet(t *testing.T) { } // compare fetched release with original if !reflect.DeepEqual(rel, got) { - t.Errorf("Expected {%q}, got {%q}", rel, got) + t.Errorf("Expected {%v}, got {%v}", rel, got) } } func TestUNcompressedSecretGet(t *testing.T) { - vers := int32(1) + vers := 1 name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) // Create a test fixture which contains an uncompressed release secret, err := newSecretsObject(key, rel, nil) if err != nil { t.Fatalf("Failed to create secret: %s", err) } - b, err := proto.Marshal(rel) + b, err := json.Marshal(rel) if err != nil { t.Fatalf("Failed to marshal release: %s", err) } secret.Data["release"] = []byte(base64.StdEncoding.EncodeToString(b)) var mock MockSecretsInterface - mock.objects = map[string]*core.Secret{key: secret} + mock.objects = map[string]*v1.Secret{key: secret} secrets := NewSecrets(&mock) // get release with key @@ -79,23 +79,23 @@ func TestUNcompressedSecretGet(t *testing.T) { } // compare fetched release with original if !reflect.DeepEqual(rel, got) { - t.Errorf("Expected {%q}, got {%q}", rel, got) + t.Errorf("Expected {%v}, got {%v}", rel, got) } } func TestSecretList(t *testing.T) { secrets := newTestFixtureSecrets(t, []*rspb.Release{ - releaseStub("key-1", 1, "default", rspb.Status_DELETED), - releaseStub("key-2", 1, "default", rspb.Status_DELETED), - releaseStub("key-3", 1, "default", rspb.Status_DEPLOYED), - releaseStub("key-4", 1, "default", rspb.Status_DEPLOYED), - releaseStub("key-5", 1, "default", rspb.Status_SUPERSEDED), - releaseStub("key-6", 1, "default", rspb.Status_SUPERSEDED), + releaseStub("key-1", 1, "default", rspb.StatusUninstalled), + releaseStub("key-2", 1, "default", rspb.StatusUninstalled), + releaseStub("key-3", 1, "default", rspb.StatusDeployed), + releaseStub("key-4", 1, "default", rspb.StatusDeployed), + releaseStub("key-5", 1, "default", rspb.StatusSuperseded), + releaseStub("key-6", 1, "default", rspb.StatusSuperseded), }...) // list all deleted releases del, err := secrets.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_DELETED + return rel.Info.Status == rspb.StatusUninstalled }) // check if err != nil { @@ -107,7 +107,7 @@ func TestSecretList(t *testing.T) { // list all deployed releases dpl, err := secrets.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_DEPLOYED + return rel.Info.Status == rspb.StatusDeployed }) // check if err != nil { @@ -119,7 +119,7 @@ func TestSecretList(t *testing.T) { // list all superseded releases ssd, err := secrets.List(func(rel *rspb.Release) bool { - return rel.Info.Status.Code == rspb.Status_SUPERSEDED + return rel.Info.Status == rspb.StatusSuperseded }) // check if err != nil { @@ -130,14 +130,38 @@ func TestSecretList(t *testing.T) { } } +func TestSecretQuery(t *testing.T) { + secrets := newTestFixtureSecrets(t, []*rspb.Release{ + releaseStub("key-1", 1, "default", rspb.StatusUninstalled), + releaseStub("key-2", 1, "default", rspb.StatusUninstalled), + releaseStub("key-3", 1, "default", rspb.StatusDeployed), + releaseStub("key-4", 1, "default", rspb.StatusDeployed), + releaseStub("key-5", 1, "default", rspb.StatusSuperseded), + releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + }...) + + rls, err := secrets.Query(map[string]string{"status": "deployed"}) + if err != nil { + t.Fatalf("Failed to query: %s", err) + } + if len(rls) != 2 { + t.Fatalf("Expected 2 results, actual %d", len(rls)) + } + + _, err = secrets.Query(map[string]string{"name": "notExist"}) + if err != ErrReleaseNotFound { + t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) + } +} + func TestSecretCreate(t *testing.T) { secrets := newTestFixtureSecrets(t) - vers := int32(1) + vers := 1 name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) // store the release in a secret if err := secrets.Create(key, rel); err != nil { @@ -152,21 +176,21 @@ func TestSecretCreate(t *testing.T) { // compare created release with original if !reflect.DeepEqual(rel, got) { - t.Errorf("Expected {%q}, got {%q}", rel, got) + t.Errorf("Expected {%v}, got {%v}", rel, got) } } func TestSecretUpdate(t *testing.T) { - vers := int32(1) + vers := 1 name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) // modify release status code - rel.Info.Status.Code = rspb.Status_SUPERSEDED + rel.Info.Status = rspb.StatusSuperseded // perform the update if err := secrets.Update(key, rel); err != nil { @@ -180,7 +204,38 @@ func TestSecretUpdate(t *testing.T) { } // check release has actually been updated by comparing modified fields - if rel.Info.Status.Code != got.Info.Status.Code { - t.Errorf("Expected status %s, got status %s", rel.Info.Status.Code, got.Info.Status.Code) + if rel.Info.Status != got.Info.Status { + t.Errorf("Expected status %s, got status %s", rel.Info.Status.String(), got.Info.Status.String()) + } +} + +func TestSecretDelete(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) + + // perform the delete on a non-existing release + _, err := secrets.Delete("nonexistent") + if err != ErrReleaseNotFound { + t.Fatalf("Expected ErrReleaseNotFound, got: {%v}", err) + } + + // perform the delete + rls, err := secrets.Delete(key) + if err != nil { + t.Fatalf("Failed to delete release with key %q: %s", key, err) + } + if !reflect.DeepEqual(rel, rls) { + t.Errorf("Expected {%v}, got {%v}", rel, rls) + } + + // fetch the deleted release + _, err = secrets.Get(key) + if !reflect.DeepEqual(ErrReleaseNotFound, err) { + t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) } } diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go new file mode 100644 index 00000000000..2e68b5f361b --- /dev/null +++ b/pkg/storage/driver/sql.go @@ -0,0 +1,492 @@ +/* +Copyright The Helm Authors. + +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 driver // import "helm.sh/helm/v3/pkg/storage/driver" + +import ( + "fmt" + "sort" + "time" + + "github.com/jmoiron/sqlx" + migrate "github.com/rubenv/sql-migrate" + + sq "github.com/Masterminds/squirrel" + + // Import pq for postgres dialect + _ "github.com/lib/pq" + + rspb "helm.sh/helm/v3/pkg/release" +) + +var _ Driver = (*SQL)(nil) + +var labelMap = map[string]struct{}{ + "modifiedAt": {}, + "createdAt": {}, + "version": {}, + "status": {}, + "owner": {}, + "name": {}, +} + +const postgreSQLDialect = "postgres" + +// SQLDriverName is the string name of this driver. +const SQLDriverName = "SQL" + +const sqlReleaseTableName = "releases_v1" + +const ( + sqlReleaseTableKeyColumn = "key" + sqlReleaseTableTypeColumn = "type" + sqlReleaseTableBodyColumn = "body" + sqlReleaseTableNameColumn = "name" + sqlReleaseTableNamespaceColumn = "namespace" + sqlReleaseTableVersionColumn = "version" + sqlReleaseTableStatusColumn = "status" + sqlReleaseTableOwnerColumn = "owner" + sqlReleaseTableCreatedAtColumn = "createdAt" + sqlReleaseTableModifiedAtColumn = "modifiedAt" +) + +const ( + sqlReleaseDefaultOwner = "helm" + sqlReleaseDefaultType = "helm.sh/release.v1" +) + +// SQL is the sql storage driver implementation. +type SQL struct { + db *sqlx.DB + namespace string + statementBuilder sq.StatementBuilderType + + Log func(string, ...interface{}) +} + +// Name returns the name of the driver. +func (s *SQL) Name() string { + return SQLDriverName +} + +func (s *SQL) ensureDBSetup() error { + // Populate the database with the relations we need if they don't exist yet + migrations := &migrate.MemoryMigrationSource{ + Migrations: []*migrate.Migration{ + { + Id: "init", + Up: []string{ + fmt.Sprintf(` + CREATE TABLE %s ( + %s VARCHAR(67), + %s VARCHAR(64) NOT NULL, + %s TEXT NOT NULL, + %s VARCHAR(64) NOT NULL, + %s VARCHAR(64) NOT NULL, + %s INTEGER NOT NULL, + %s TEXT NOT NULL, + %s TEXT NOT NULL, + %s INTEGER NOT NULL, + %s INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY(%s, %s) + ); + CREATE INDEX ON %s (%s, %s); + CREATE INDEX ON %s (%s); + CREATE INDEX ON %s (%s); + CREATE INDEX ON %s (%s); + CREATE INDEX ON %s (%s); + CREATE INDEX ON %s (%s); + + GRANT ALL ON %s TO PUBLIC; + + ALTER TABLE %s ENABLE ROW LEVEL SECURITY; + `, + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableTypeColumn, + sqlReleaseTableBodyColumn, + sqlReleaseTableNameColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableVersionColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableCreatedAtColumn, + sqlReleaseTableModifiedAtColumn, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableName, + sqlReleaseTableVersionColumn, + sqlReleaseTableName, + sqlReleaseTableStatusColumn, + sqlReleaseTableName, + sqlReleaseTableOwnerColumn, + sqlReleaseTableName, + sqlReleaseTableCreatedAtColumn, + sqlReleaseTableName, + sqlReleaseTableModifiedAtColumn, + sqlReleaseTableName, + sqlReleaseTableName, + ), + }, + Down: []string{ + fmt.Sprintf(` + DROP TABLE %s; + `, sqlReleaseTableName), + }, + }, + }, + } + + _, err := migrate.Exec(s.db.DB, postgreSQLDialect, migrations, migrate.Up) + return err +} + +// SQLReleaseWrapper describes how Helm releases are stored in an SQL database +type SQLReleaseWrapper struct { + // The primary key, made of {release-name}.{release-version} + Key string `db:"key"` + + // See https://github.com/helm/helm/blob/master/pkg/storage/driver/secrets.go#L236 + Type string `db:"type"` + + // The rspb.Release body, as a base64-encoded string + Body string `db:"body"` + + // Release "labels" that can be used as filters in the storage.Query(labels map[string]string) + // we implemented. Note that allowing Helm users to filter against new dimensions will require a + // new migration to be added, and the Create and/or update functions to be updated accordingly. + Name string `db:"name"` + Namespace string `db:"namespace"` + Version int `db:"version"` + Status string `db:"status"` + Owner string `db:"owner"` + CreatedAt int `db:"createdAt"` + ModifiedAt int `db:"modifiedAt"` +} + +// NewSQL initializes a new sql driver. +func NewSQL(connectionString string, logger func(string, ...interface{}), namespace string) (*SQL, error) { + db, err := sqlx.Connect(postgreSQLDialect, connectionString) + if err != nil { + return nil, err + } + + driver := &SQL{ + db: db, + Log: logger, + statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar), + } + + if err := driver.ensureDBSetup(); err != nil { + return nil, err + } + + driver.namespace = namespace + + return driver, nil +} + +// Get returns the release named by key. +func (s *SQL) Get(key string) (*rspb.Release, error) { + var record SQLReleaseWrapper + + qb := s.statementBuilder. + Select(sqlReleaseTableBodyColumn). + From(sqlReleaseTableName). + Where(sq.Eq{sqlReleaseTableKeyColumn: key}). + Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}) + + query, args, err := qb.ToSql() + if err != nil { + s.Log("failed to build query: %v", err) + return nil, err + } + + // Get will return an error if the result is empty + if err := s.db.Get(&record, query, args...); err != nil { + s.Log("got SQL error when getting release %s: %v", key, err) + return nil, ErrReleaseNotFound + } + + release, err := decodeRelease(record.Body) + if err != nil { + s.Log("get: failed to decode data %q: %v", key, err) + return nil, err + } + + return release, nil +} + +// List returns the list of all releases such that filter(release) == true +func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { + sb := s.statementBuilder. + Select(sqlReleaseTableBodyColumn). + From(sqlReleaseTableName). + Where(sq.Eq{sqlReleaseTableOwnerColumn: sqlReleaseDefaultOwner}) + + // If a namespace was specified, we only list releases from that namespace + if s.namespace != "" { + sb = sb.Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}) + } + + query, args, err := sb.ToSql() + if err != nil { + s.Log("failed to build query: %v", err) + return nil, err + } + + var records = []SQLReleaseWrapper{} + if err := s.db.Select(&records, query, args...); err != nil { + s.Log("list: failed to list: %v", err) + return nil, err + } + + var releases []*rspb.Release + for _, record := range records { + release, err := decodeRelease(record.Body) + if err != nil { + s.Log("list: failed to decode release: %v: %v", record, err) + continue + } + if filter(release) { + releases = append(releases, release) + } + } + + return releases, nil +} + +// Query returns the set of releases that match the provided set of labels. +func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { + sb := s.statementBuilder. + Select(sqlReleaseTableBodyColumn). + From(sqlReleaseTableName) + + keys := make([]string, 0, len(labels)) + for key := range labels { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + if _, ok := labelMap[key]; ok { + sb = sb.Where(sq.Eq{key: labels[key]}) + } else { + s.Log("unknown label %s", key) + return nil, fmt.Errorf("unknown label %s", key) + } + } + + // If a namespace was specified, we only list releases from that namespace + if s.namespace != "" { + sb = sb.Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}) + } + + // Build our query + query, args, err := sb.ToSql() + if err != nil { + s.Log("failed to build query: %v", err) + return nil, err + } + + var records = []SQLReleaseWrapper{} + if err := s.db.Select(&records, query, args...); err != nil { + s.Log("list: failed to query with labels: %v", err) + return nil, err + } + + var releases []*rspb.Release + for _, record := range records { + release, err := decodeRelease(record.Body) + if err != nil { + s.Log("list: failed to decode release: %v: %v", record, err) + continue + } + releases = append(releases, release) + } + + if len(releases) == 0 { + return nil, ErrReleaseNotFound + } + + return releases, nil +} + +// Create creates a new release. +func (s *SQL) Create(key string, rls *rspb.Release) error { + namespace := rls.Namespace + if namespace == "" { + namespace = defaultNamespace + } + s.namespace = namespace + + body, err := encodeRelease(rls) + if err != nil { + s.Log("failed to encode release: %v", err) + return err + } + + transaction, err := s.db.Beginx() + if err != nil { + s.Log("failed to start SQL transaction: %v", err) + return fmt.Errorf("error beginning transaction: %v", err) + } + + insertQuery, args, err := s.statementBuilder. + Insert(sqlReleaseTableName). + Columns( + sqlReleaseTableKeyColumn, + sqlReleaseTableTypeColumn, + sqlReleaseTableBodyColumn, + sqlReleaseTableNameColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableVersionColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableCreatedAtColumn, + ). + Values( + key, + sqlReleaseDefaultType, + body, + rls.Name, + namespace, + int(rls.Version), + rls.Info.Status.String(), + sqlReleaseDefaultOwner, + int(time.Now().Unix()), + ).ToSql() + if err != nil { + s.Log("failed to build insert query: %v", err) + return err + } + + if _, err := transaction.Exec(insertQuery, args...); err != nil { + defer transaction.Rollback() + + selectQuery, args, buildErr := s.statementBuilder. + Select(sqlReleaseTableKeyColumn). + From(sqlReleaseTableName). + Where(sq.Eq{sqlReleaseTableKeyColumn: key}). + Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). + ToSql() + if buildErr != nil { + s.Log("failed to build select query: %v", buildErr) + return err + } + + var record SQLReleaseWrapper + if err := transaction.Get(&record, selectQuery, args...); err == nil { + s.Log("release %s already exists", key) + return ErrReleaseExists + } + + s.Log("failed to store release %s in SQL database: %v", key, err) + return err + } + defer transaction.Commit() + + return nil +} + +// Update updates a release. +func (s *SQL) Update(key string, rls *rspb.Release) error { + namespace := rls.Namespace + if namespace == "" { + namespace = defaultNamespace + } + s.namespace = namespace + + body, err := encodeRelease(rls) + if err != nil { + s.Log("failed to encode release: %v", err) + return err + } + + query, args, err := s.statementBuilder. + Update(sqlReleaseTableName). + Set(sqlReleaseTableBodyColumn, body). + Set(sqlReleaseTableNameColumn, rls.Name). + Set(sqlReleaseTableVersionColumn, int(rls.Version)). + Set(sqlReleaseTableStatusColumn, rls.Info.Status.String()). + Set(sqlReleaseTableOwnerColumn, sqlReleaseDefaultOwner). + Set(sqlReleaseTableModifiedAtColumn, int(time.Now().Unix())). + Where(sq.Eq{sqlReleaseTableKeyColumn: key}). + Where(sq.Eq{sqlReleaseTableNamespaceColumn: namespace}). + ToSql() + + if err != nil { + s.Log("failed to build update query: %v", err) + return err + } + + if _, err := s.db.Exec(query, args...); err != nil { + s.Log("failed to update release %s in SQL database: %v", key, err) + return err + } + + return nil +} + +// Delete deletes a release or returns ErrReleaseNotFound. +func (s *SQL) Delete(key string) (*rspb.Release, error) { + transaction, err := s.db.Beginx() + if err != nil { + s.Log("failed to start SQL transaction: %v", err) + return nil, fmt.Errorf("error beginning transaction: %v", err) + } + + selectQuery, args, err := s.statementBuilder. + Select(sqlReleaseTableBodyColumn). + From(sqlReleaseTableName). + Where(sq.Eq{sqlReleaseTableKeyColumn: key}). + Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). + ToSql() + if err != nil { + s.Log("failed to build select query: %v", err) + return nil, err + } + + var record SQLReleaseWrapper + err = transaction.Get(&record, selectQuery, args...) + if err != nil { + s.Log("release %s not found: %v", key, err) + return nil, ErrReleaseNotFound + } + + release, err := decodeRelease(record.Body) + if err != nil { + s.Log("failed to decode release %s: %v", key, err) + transaction.Rollback() + return nil, err + } + defer transaction.Commit() + + deleteQuery, args, err := s.statementBuilder. + Delete(sqlReleaseTableName). + Where(sq.Eq{sqlReleaseTableKeyColumn: key}). + Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). + ToSql() + if err != nil { + s.Log("failed to build select query: %v", err) + return nil, err + } + + _, err = transaction.Exec(deleteQuery, args...) + return release, err +} diff --git a/pkg/storage/driver/sql_test.go b/pkg/storage/driver/sql_test.go new file mode 100644 index 00000000000..1562a90aa4d --- /dev/null +++ b/pkg/storage/driver/sql_test.go @@ -0,0 +1,442 @@ +/* +Copyright The Helm Authors. +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 driver + +import ( + "fmt" + "reflect" + "regexp" + "testing" + "time" + + sqlmock "github.com/DATA-DOG/go-sqlmock" + + rspb "helm.sh/helm/v3/pkg/release" +) + +func TestSQLName(t *testing.T) { + sqlDriver, _ := newTestFixtureSQL(t) + if sqlDriver.Name() != SQLDriverName { + t.Errorf("Expected name to be %s, got %s", SQLDriverName, sqlDriver.Name()) + } +} + +func TestSQLGet(t *testing.T) { + vers := int(1) + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + body, _ := encodeRelease(rel) + + sqlDriver, mock := newTestFixtureSQL(t) + + query := fmt.Sprintf( + regexp.QuoteMeta("SELECT %s FROM %s WHERE %s = $1 AND %s = $2"), + sqlReleaseTableBodyColumn, + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectQuery(query). + WithArgs(key, namespace). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }).AddRow( + body, + ), + ).RowsWillBeClosed() + + got, err := sqlDriver.Get(key) + if err != nil { + t.Fatalf("Failed to get release: %v", err) + } + + if !reflect.DeepEqual(rel, got) { + t.Errorf("Expected release {%v}, got {%v}", rel, got) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSQLList(t *testing.T) { + body1, _ := encodeRelease(releaseStub("key-1", 1, "default", rspb.StatusUninstalled)) + body2, _ := encodeRelease(releaseStub("key-2", 1, "default", rspb.StatusUninstalled)) + body3, _ := encodeRelease(releaseStub("key-3", 1, "default", rspb.StatusDeployed)) + body4, _ := encodeRelease(releaseStub("key-4", 1, "default", rspb.StatusDeployed)) + body5, _ := encodeRelease(releaseStub("key-5", 1, "default", rspb.StatusSuperseded)) + body6, _ := encodeRelease(releaseStub("key-6", 1, "default", rspb.StatusSuperseded)) + + sqlDriver, mock := newTestFixtureSQL(t) + + for i := 0; i < 3; i++ { + query := fmt.Sprintf( + "SELECT %s FROM %s WHERE %s = $1 AND %s = $2", + sqlReleaseTableBodyColumn, + sqlReleaseTableName, + sqlReleaseTableOwnerColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectQuery(regexp.QuoteMeta(query)). + WithArgs(sqlReleaseDefaultOwner, sqlDriver.namespace). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }). + AddRow(body1). + AddRow(body2). + AddRow(body3). + AddRow(body4). + AddRow(body5). + AddRow(body6), + ).RowsWillBeClosed() + } + + // list all deleted releases + del, err := sqlDriver.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusUninstalled + }) + // check + if err != nil { + t.Errorf("Failed to list deleted: %v", err) + } + if len(del) != 2 { + t.Errorf("Expected 2 deleted, got %d:\n%v\n", len(del), del) + } + + // list all deployed releases + dpl, err := sqlDriver.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusDeployed + }) + // check + if err != nil { + t.Errorf("Failed to list deployed: %v", err) + } + if len(dpl) != 2 { + t.Errorf("Expected 2 deployed, got %d:\n%v\n", len(dpl), dpl) + } + + // list all superseded releases + ssd, err := sqlDriver.List(func(rel *rspb.Release) bool { + return rel.Info.Status == rspb.StatusSuperseded + }) + // check + if err != nil { + t.Errorf("Failed to list superseded: %v", err) + } + if len(ssd) != 2 { + t.Errorf("Expected 2 superseded, got %d:\n%v\n", len(ssd), ssd) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSqlCreate(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + sqlDriver, mock := newTestFixtureSQL(t) + body, _ := encodeRelease(rel) + + query := fmt.Sprintf( + "INSERT INTO %s (%s,%s,%s,%s,%s,%s,%s,%s,%s) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)", + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableTypeColumn, + sqlReleaseTableBodyColumn, + sqlReleaseTableNameColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableVersionColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableCreatedAtColumn, + ) + + mock.ExpectBegin() + mock. + ExpectExec(regexp.QuoteMeta(query)). + WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix())). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + if err := sqlDriver.Create(key, rel); err != nil { + t.Fatalf("failed to create release with key %s: %v", key, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSqlCreateAlreadyExists(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + sqlDriver, mock := newTestFixtureSQL(t) + body, _ := encodeRelease(rel) + + insertQuery := fmt.Sprintf( + "INSERT INTO %s (%s,%s,%s,%s,%s,%s,%s,%s,%s) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)", + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableTypeColumn, + sqlReleaseTableBodyColumn, + sqlReleaseTableNameColumn, + sqlReleaseTableNamespaceColumn, + sqlReleaseTableVersionColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableCreatedAtColumn, + ) + + // Insert fails (primary key already exists) + mock.ExpectBegin() + mock. + ExpectExec(regexp.QuoteMeta(insertQuery)). + WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix())). + WillReturnError(fmt.Errorf("dialect dependent SQL error")) + + selectQuery := fmt.Sprintf( + regexp.QuoteMeta("SELECT %s FROM %s WHERE %s = $1 AND %s = $2"), + sqlReleaseTableKeyColumn, + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + ) + + // Let's check that we do make sure the error is due to a release already existing + mock. + ExpectQuery(selectQuery). + WithArgs(key, namespace). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableKeyColumn, + }).AddRow( + key, + ), + ).RowsWillBeClosed() + mock.ExpectRollback() + + if err := sqlDriver.Create(key, rel); err == nil { + t.Fatalf("failed to create release with key %s: %v", key, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSqlUpdate(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + sqlDriver, mock := newTestFixtureSQL(t) + body, _ := encodeRelease(rel) + + query := fmt.Sprintf( + "UPDATE %s SET %s = $1, %s = $2, %s = $3, %s = $4, %s = $5, %s = $6 WHERE %s = $7 AND %s = $8", + sqlReleaseTableName, + sqlReleaseTableBodyColumn, + sqlReleaseTableNameColumn, + sqlReleaseTableVersionColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableModifiedAtColumn, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectExec(regexp.QuoteMeta(query)). + WithArgs(body, rel.Name, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix()), key, namespace). + WillReturnResult(sqlmock.NewResult(0, 1)) + + if err := sqlDriver.Update(key, rel); err != nil { + t.Fatalf("failed to update release with key %s: %v", key, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSqlQuery(t *testing.T) { + // Reflect actual use cases in ../storage.go + labelSetDeployed := map[string]string{ + "name": "smug-pigeon", + "owner": sqlReleaseDefaultOwner, + "status": "deployed", + } + labelSetAll := map[string]string{ + "name": "smug-pigeon", + "owner": sqlReleaseDefaultOwner, + } + + supersededRelease := releaseStub("smug-pigeon", 1, "default", rspb.StatusSuperseded) + supersededReleaseBody, _ := encodeRelease(supersededRelease) + deployedRelease := releaseStub("smug-pigeon", 2, "default", rspb.StatusDeployed) + deployedReleaseBody, _ := encodeRelease(deployedRelease) + + // Let's actually start our test + sqlDriver, mock := newTestFixtureSQL(t) + + query := fmt.Sprintf( + "SELECT %s FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3 AND %s = $4", + sqlReleaseTableBodyColumn, + sqlReleaseTableName, + sqlReleaseTableNameColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableStatusColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectQuery(regexp.QuoteMeta(query)). + WithArgs("smug-pigeon", sqlReleaseDefaultOwner, "deployed", "default"). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }).AddRow( + deployedReleaseBody, + ), + ).RowsWillBeClosed() + + query = fmt.Sprintf( + "SELECT %s FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3", + sqlReleaseTableBodyColumn, + sqlReleaseTableName, + sqlReleaseTableNameColumn, + sqlReleaseTableOwnerColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectQuery(regexp.QuoteMeta(query)). + WithArgs("smug-pigeon", sqlReleaseDefaultOwner, "default"). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }).AddRow( + supersededReleaseBody, + ).AddRow( + deployedReleaseBody, + ), + ).RowsWillBeClosed() + + results, err := sqlDriver.Query(labelSetDeployed) + if err != nil { + t.Fatalf("failed to query for deployed smug-pigeon release: %v", err) + } + + for _, res := range results { + if !reflect.DeepEqual(res, deployedRelease) { + t.Errorf("Expected release {%v}, got {%v}", deployedRelease, res) + } + } + + results, err = sqlDriver.Query(labelSetAll) + if err != nil { + t.Fatalf("failed to query release history for smug-pigeon: %v", err) + } + + if len(results) != 2 { + t.Errorf("expected a resultset of size 2, got %d", len(results)) + } + + for _, res := range results { + if !reflect.DeepEqual(res, deployedRelease) && !reflect.DeepEqual(res, supersededRelease) { + t.Errorf("Expected release {%v} or {%v}, got {%v}", deployedRelease, supersededRelease, res) + } + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } +} + +func TestSqlDelete(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + body, _ := encodeRelease(rel) + + sqlDriver, mock := newTestFixtureSQL(t) + + selectQuery := fmt.Sprintf( + "SELECT %s FROM %s WHERE %s = $1 AND %s = $2", + sqlReleaseTableBodyColumn, + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock.ExpectBegin() + mock. + ExpectQuery(regexp.QuoteMeta(selectQuery)). + WithArgs(key, namespace). + WillReturnRows( + mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }).AddRow( + body, + ), + ).RowsWillBeClosed() + + deleteQuery := fmt.Sprintf( + "DELETE FROM %s WHERE %s = $1 AND %s = $2", + sqlReleaseTableName, + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, + ) + + mock. + ExpectExec(regexp.QuoteMeta(deleteQuery)). + WithArgs(key, namespace). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + deletedRelease, err := sqlDriver.Delete(key) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("sql expectations weren't met: %v", err) + } + if err != nil { + t.Fatalf("failed to delete release with key %q: %v", key, err) + } + + if !reflect.DeepEqual(rel, deletedRelease) { + t.Errorf("Expected release {%v}, got {%v}", rel, deletedRelease) + } +} diff --git a/pkg/storage/driver/util.go b/pkg/storage/driver/util.go index 65fb17e7c5b..e5b846163cc 100644 --- a/pkg/storage/driver/util.go +++ b/pkg/storage/driver/util.go @@ -1,5 +1,5 @@ /* -Copyright 2017 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "k8s.io/helm/pkg/storage/driver" +package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( "bytes" "compress/gzip" "encoding/base64" + "encoding/json" "io/ioutil" - "github.com/golang/protobuf/proto" - rspb "k8s.io/helm/pkg/proto/hapi/release" + rspb "helm.sh/helm/v3/pkg/release" ) var b64 = base64.StdEncoding @@ -31,9 +31,9 @@ var b64 = base64.StdEncoding var magicGzip = []byte{0x1f, 0x8b, 0x08} // encodeRelease encodes a release returning a base64 encoded -// gzipped binary protobuf encoding representation, or error. +// gzipped string representation, or error. func encodeRelease(rls *rspb.Release) (string, error) { - b, err := proto.Marshal(rls) + b, err := json.Marshal(rls) if err != nil { return "", err } @@ -50,10 +50,9 @@ func encodeRelease(rls *rspb.Release) (string, error) { return b64.EncodeToString(buf.Bytes()), nil } -// decodeRelease decodes the bytes in data into a release -// type. Data must contain a base64 encoded string of a -// valid protobuf encoding of a release, otherwise -// an error is returned. +// decodeRelease decodes the bytes of data into a release +// type. Data must contain a base64 encoded gzipped string of a +// valid release, otherwise an error is returned. func decodeRelease(data string) (*rspb.Release, error) { // base64 decode string b, err := b64.DecodeString(data) @@ -69,6 +68,7 @@ func decodeRelease(data string) (*rspb.Release, error) { if err != nil { return nil, err } + defer r.Close() b2, err := ioutil.ReadAll(r) if err != nil { return nil, err @@ -77,8 +77,8 @@ func decodeRelease(data string) (*rspb.Release, error) { } var rls rspb.Release - // unmarshal protobuf bytes - if err := proto.Unmarshal(b, &rls); err != nil { + // unmarshal release object bytes + if err := json.Unmarshal(b, &rls); err != nil { return nil, err } return &rls, nil diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 4b39e0bb237..d836a412ae7 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,17 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -package storage // import "k8s.io/helm/pkg/storage" +package storage // import "helm.sh/helm/v3/pkg/storage" import ( "fmt" "strings" - rspb "k8s.io/helm/pkg/proto/hapi/release" - relutil "k8s.io/helm/pkg/releaseutil" - "k8s.io/helm/pkg/storage/driver" + "github.com/pkg/errors" + + rspb "helm.sh/helm/v3/pkg/release" + relutil "helm.sh/helm/v3/pkg/releaseutil" + "helm.sh/helm/v3/pkg/storage/driver" ) +// HelmStorageType is the type field of the Kubernetes storage object which stores the Helm release +// version. It is modified slightly replacing the '/': sh.helm/release.v1 +// Note: The version 'v1' is incremented if the release object metadata is +// modified between major releases. +// This constant is used as a prefix for the Kubernetes storage object name. +const HelmStorageType = "sh.helm.release.v1" + // Storage represents a storage engine for a Release. type Storage struct { driver.Driver @@ -40,7 +49,7 @@ type Storage struct { // Get retrieves the release from storage. An error is returned // if the storage driver failed to fetch the release, or the // release identified by the key, version pair does not exist. -func (s *Storage) Get(name string, version int32) (*rspb.Release, error) { +func (s *Storage) Get(name string, version int) (*rspb.Release, error) { s.Log("getting release %q", makeKey(name, version)) return s.Driver.Get(makeKey(name, version)) } @@ -52,12 +61,15 @@ func (s *Storage) Create(rls *rspb.Release) error { s.Log("creating release %q", makeKey(rls.Name, rls.Version)) if s.MaxHistory > 0 { // Want to make space for one more release. - s.removeLeastRecent(rls.Name, s.MaxHistory-1) + if err := s.removeLeastRecent(rls.Name, s.MaxHistory-1); err != nil && + !errors.Is(err, driver.ErrReleaseNotFound) { + return err + } } return s.Driver.Create(makeKey(rls.Name, rls.Version), rls) } -// Update update the release in storage. An error is returned if the +// Update updates the release in storage. An error is returned if the // storage backend fails to update the release or if the release // does not exist. func (s *Storage) Update(rls *rspb.Release) error { @@ -68,7 +80,7 @@ func (s *Storage) Update(rls *rspb.Release) error { // Delete deletes the release from storage. An error is returned if // the storage backend fails to delete the release or if the release // does not exist. -func (s *Storage) Delete(name string, version int32) (*rspb.Release, error) { +func (s *Storage) Delete(name string, version int) (*rspb.Release, error) { s.Log("deleting release %q", makeKey(name, version)) return s.Driver.Delete(makeKey(name, version)) } @@ -80,12 +92,12 @@ func (s *Storage) ListReleases() ([]*rspb.Release, error) { return s.Driver.List(func(_ *rspb.Release) bool { return true }) } -// ListDeleted returns all releases with Status == DELETED. An error is returned +// ListUninstalled returns all releases with Status == UNINSTALLED. An error is returned // if the storage backend fails to retrieve the releases. -func (s *Storage) ListDeleted() ([]*rspb.Release, error) { - s.Log("listing deleted releases in storage") +func (s *Storage) ListUninstalled() ([]*rspb.Release, error) { + s.Log("listing uninstalled releases in storage") return s.Driver.List(func(rls *rspb.Release) bool { - return relutil.StatusFilter(rspb.Status_DELETED).Check(rls) + return relutil.StatusFilter(rspb.StatusUninstalled).Check(rls) }) } @@ -94,27 +106,7 @@ func (s *Storage) ListDeleted() ([]*rspb.Release, error) { func (s *Storage) ListDeployed() ([]*rspb.Release, error) { s.Log("listing all deployed releases in storage") return s.Driver.List(func(rls *rspb.Release) bool { - return relutil.StatusFilter(rspb.Status_DEPLOYED).Check(rls) - }) -} - -// ListFilterAll returns the set of releases satisfying the predicate -// (filter0 && filter1 && ... && filterN), i.e. a Release is included in the results -// if and only if all filters return true. -func (s *Storage) ListFilterAll(fns ...relutil.FilterFunc) ([]*rspb.Release, error) { - s.Log("listing all releases with filter") - return s.Driver.List(func(rls *rspb.Release) bool { - return relutil.All(fns...).Check(rls) - }) -} - -// ListFilterAny returns the set of releases satisfying the predicate -// (filter0 || filter1 || ... || filterN), i.e. a Release is included in the results -// if at least one of the filters returns true. -func (s *Storage) ListFilterAny(fns ...relutil.FilterFunc) ([]*rspb.Release, error) { - s.Log("listing any releases with filter") - return s.Driver.List(func(rls *rspb.Release) bool { - return relutil.Any(fns...).Check(rls) + return relutil.StatusFilter(rspb.StatusDeployed).Check(rls) }) } @@ -123,17 +115,18 @@ func (s *Storage) ListFilterAny(fns ...relutil.FilterFunc) ([]*rspb.Release, err func (s *Storage) Deployed(name string) (*rspb.Release, error) { ls, err := s.DeployedAll(name) if err != nil { - if strings.Contains(err.Error(), "not found") { - return nil, fmt.Errorf("%q has no deployed releases", name) - } return nil, err } if len(ls) == 0 { - return nil, fmt.Errorf("%q has no deployed releases", name) + return nil, driver.NewErrNoDeployedReleases(name) } - return ls[0], err + // If executed concurrently, Helm's database gets corrupted + // and multiple releases are DEPLOYED. Take the latest. + relutil.Reverse(ls, relutil.SortByRevision) + + return ls[0], nil } // DeployedAll returns all deployed releases with the provided name, or @@ -142,15 +135,15 @@ func (s *Storage) DeployedAll(name string) ([]*rspb.Release, error) { s.Log("getting deployed releases from %q history", name) ls, err := s.Driver.Query(map[string]string{ - "NAME": name, - "OWNER": "TILLER", - "STATUS": "DEPLOYED", + "name": name, + "owner": "helm", + "status": "deployed", }) if err == nil { return ls, nil } if strings.Contains(err.Error(), "not found") { - return nil, fmt.Errorf("%q has no deployed releases", name) + return nil, driver.NewErrNoDeployedReleases(name) } return nil, err } @@ -160,10 +153,10 @@ func (s *Storage) DeployedAll(name string) ([]*rspb.Release, error) { func (s *Storage) History(name string) ([]*rspb.Release, error) { s.Log("getting release history for %q", name) - return s.Driver.Query(map[string]string{"NAME": name, "OWNER": "TILLER"}) + return s.Driver.Query(map[string]string{"name": name, "owner": "helm"}) } -// removeLeastRecent removes items from history until the lengh number of releases +// removeLeastRecent removes items from history until the length number of releases // does not exceed max. // // We allow max to be set explicitly so that calling functions can "make space" @@ -179,33 +172,59 @@ func (s *Storage) removeLeastRecent(name string, max int) error { if len(h) <= max { return nil } - overage := len(h) - max // We want oldest to newest relutil.SortByRevision(h) + lastDeployed, err := s.Deployed(name) + if err != nil { + return err + } + + var toDelete []*rspb.Release + for _, rel := range h { + // once we have enough releases to delete to reach the max, stop + if len(h)-len(toDelete) == max { + break + } + if lastDeployed != nil { + if rel.Version != lastDeployed.Version { + toDelete = append(toDelete, rel) + } + } else { + toDelete = append(toDelete, rel) + } + } + // Delete as many as possible. In the case of API throughput limitations, // multiple invocations of this function will eventually delete them all. - toDelete := h[0:overage] - errors := []error{} + errs := []error{} for _, rel := range toDelete { - key := makeKey(name, rel.Version) - _, innerErr := s.Delete(name, rel.Version) - if innerErr != nil { - s.Log("error pruning %s from release history: %s", key, innerErr) - errors = append(errors, innerErr) + err = s.deleteReleaseVersion(name, rel.Version) + if err != nil { + errs = append(errs, err) } } - s.Log("Pruned %d record(s) from %s with %d error(s)", len(toDelete), name, len(errors)) - switch c := len(errors); c { + s.Log("Pruned %d record(s) from %s with %d error(s)", len(toDelete), name, len(errs)) + switch c := len(errs); c { case 0: return nil case 1: - return errors[0] + return errs[0] default: - return fmt.Errorf("encountered %d deletion errors. First is: %s", c, errors[0]) + return errors.Errorf("encountered %d deletion errors. First is: %s", c, errs[0]) + } +} + +func (s *Storage) deleteReleaseVersion(name string, version int) error { + key := makeKey(name, version) + _, err := s.Delete(name, version) + if err != nil { + s.Log("error pruning %s from release history: %s", key, err) + return err } + return nil } // Last fetches the last revision of the named release. @@ -216,18 +235,21 @@ func (s *Storage) Last(name string) (*rspb.Release, error) { return nil, err } if len(h) == 0 { - return nil, fmt.Errorf("no revision for release %q", name) + return nil, errors.Errorf("no revision for release %q", name) } relutil.Reverse(h, relutil.SortByRevision) return h[0], nil } -// makeKey concatenates a release name and version into -// a string with format ```#v```. +// makeKey concatenates the Kubernetes storage object type, a release name and version +// into a string with format:```..v```. +// The storage type is prepended to keep name uniqueness between different +// release storage types. An example of clash when not using the type: +// https://github.com/helm/helm/issues/6435. // This key is used to uniquely identify storage objects. -func makeKey(rlsname string, version int32) string { - return fmt.Sprintf("%s.v%d", rlsname, version) +func makeKey(rlsname string, version int) string { + return fmt.Sprintf("%s.%s.v%d", HelmStorageType, rlsname, version) } // Init initializes a new storage backend with the driver d. diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index fb2824de7b3..ac79ca2fdef 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,15 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -package storage // import "k8s.io/helm/pkg/storage" +package storage // import "helm.sh/helm/v3/pkg/storage" import ( "fmt" "reflect" "testing" - rspb "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/storage/driver" + "github.com/pkg/errors" + + rspb "helm.sh/helm/v3/pkg/release" + "helm.sh/helm/v3/pkg/storage/driver" ) func TestStorageCreate(t *testing.T) { @@ -43,7 +45,7 @@ func TestStorageCreate(t *testing.T) { // verify the fetched and created release are the same if !reflect.DeepEqual(rls, res) { - t.Fatalf("Expected %q, got %q", rls, res) + t.Fatalf("Expected %v, got %v", rls, res) } } @@ -55,13 +57,13 @@ func TestStorageUpdate(t *testing.T) { rls := ReleaseTestData{ Name: "angry-beaver", Version: 1, - Status: rspb.Status_DEPLOYED, + Status: rspb.StatusDeployed, }.ToRelease() assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") // modify the release - rls.Info.Status.Code = rspb.Status_DELETED + rls.Info.Status = rspb.StatusUninstalled assertErrNil(t.Fatal, storage.Update(rls), "UpdateRelease") // retrieve the updated release @@ -70,7 +72,7 @@ func TestStorageUpdate(t *testing.T) { // verify updated and fetched releases are the same. if !reflect.DeepEqual(rls, res) { - t.Fatalf("Expected %q, got %q", rls, res) + t.Fatalf("Expected %v, got %v", rls, res) } } @@ -97,7 +99,7 @@ func TestStorageDelete(t *testing.T) { // verify updated and fetched releases are the same. if !reflect.DeepEqual(rls, res) { - t.Fatalf("Expected %q, got %q", rls, res) + t.Fatalf("Expected %v, got %v", rls, res) } hist, err := storage.History(rls.Name) @@ -122,13 +124,13 @@ func TestStorageList(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: "happy-catdog", Status: rspb.Status_SUPERSEDED}.ToRelease() - rls1 := ReleaseTestData{Name: "livid-human", Status: rspb.Status_SUPERSEDED}.ToRelease() - rls2 := ReleaseTestData{Name: "relaxed-cat", Status: rspb.Status_SUPERSEDED}.ToRelease() - rls3 := ReleaseTestData{Name: "hungry-hippo", Status: rspb.Status_DEPLOYED}.ToRelease() - rls4 := ReleaseTestData{Name: "angry-beaver", Status: rspb.Status_DEPLOYED}.ToRelease() - rls5 := ReleaseTestData{Name: "opulent-frog", Status: rspb.Status_DELETED}.ToRelease() - rls6 := ReleaseTestData{Name: "happy-liger", Status: rspb.Status_DELETED}.ToRelease() + rls0 := ReleaseTestData{Name: "happy-catdog", Status: rspb.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: "livid-human", Status: rspb.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: "relaxed-cat", Status: rspb.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: "hungry-hippo", Status: rspb.StatusDeployed}.ToRelease() + rls4 := ReleaseTestData{Name: "angry-beaver", Status: rspb.StatusDeployed}.ToRelease() + rls5 := ReleaseTestData{Name: "opulent-frog", Status: rspb.StatusUninstalled}.ToRelease() + rls6 := ReleaseTestData{Name: "happy-liger", Status: rspb.StatusUninstalled}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'rls0'") @@ -145,9 +147,9 @@ func TestStorageList(t *testing.T) { NumExpected int ListFunc func() ([]*rspb.Release, error) }{ - {"ListDeleted", 2, storage.ListDeleted}, {"ListDeployed", 2, storage.ListDeployed}, {"ListReleases", 7, storage.ListReleases}, + {"ListUninstalled", 2, storage.ListUninstalled}, } setup() @@ -169,15 +171,15 @@ func TestStorageDeployed(t *testing.T) { storage := Init(driver.NewMemory()) const name = "angry-bird" - const vers = int32(4) + const vers = 4 // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.Status_DEPLOYED}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -200,8 +202,48 @@ func TestStorageDeployed(t *testing.T) { t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) case rls.Version != vers: t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) - case rls.Info.Status.Code != rspb.Status_DEPLOYED: - t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.Code) + case rls.Info.Status != rspb.StatusDeployed: + t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.String()) + } +} + +func TestStorageDeployedWithCorruption(t *testing.T) { + storage := Init(driver.NewMemory()) + + const name = "angry-bird" + const vers = int(4) + + // setup storage with test releases + setup := func() { + // release records (notice odd order and corruption) + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusDeployed}.ToRelease() + + // create the release records in the storage + assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") + assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'angry-bird' (v2)") + assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'angry-bird' (v3)") + assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'angry-bird' (v4)") + } + + setup() + + rls, err := storage.Deployed(name) + if err != nil { + t.Fatalf("Failed to query for deployed release: %s\n", err) + } + + switch { + case rls == nil: + t.Fatalf("Release is nil") + case rls.Name != name: + t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) + case rls.Version != vers: + t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) + case rls.Info.Status != rspb.StatusDeployed: + t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.String()) } } @@ -213,10 +255,10 @@ func TestStorageHistory(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.Status_DEPLOYED}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -236,6 +278,32 @@ func TestStorageHistory(t *testing.T) { } } +func TestStorageRemoveLeastRecentWithError(t *testing.T) { + storage := Init(driver.NewMemory()) + storage.Log = t.Logf + + storage.MaxHistory = 1 + + const name = "angry-bird" + + // setup storage with test releases + setup := func() { + // release records + rls1 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() + + // create the release records in the storage + assertErrNil(t.Fatal, storage.Driver.Create(makeKey(rls1.Name, rls1.Version), rls1), "Storing release 'angry-bird' (v1)") + } + setup() + + rls2 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() + wantErr := driver.ErrNoDeployedReleases + gotErr := storage.Create(rls2) + if !errors.Is(gotErr, wantErr) { + t.Fatalf("Storing release 'angry-bird' (v2) should return the error %#v, but returned %#v", wantErr, gotErr) + } +} + func TestStorageRemoveLeastRecent(t *testing.T) { storage := Init(driver.NewMemory()) storage.Log = t.Logf @@ -248,10 +316,10 @@ func TestStorageRemoveLeastRecent(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.Status_DEPLOYED}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -270,7 +338,7 @@ func TestStorageRemoveLeastRecent(t *testing.T) { } storage.MaxHistory = 3 - rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.Status_DEPLOYED}.ToRelease() + rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusDeployed}.ToRelease() assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)") // On inserting the 5th record, we expect two records to be pruned from history. @@ -286,13 +354,64 @@ func TestStorageRemoveLeastRecent(t *testing.T) { // We expect the existing records to be 3, 4, and 5. for i, item := range hist { - v := int(item.Version) + v := item.Version if expect := i + 3; v != expect { t.Errorf("Expected release %d, got %d", expect, v) } } } +func TestStorageDoNotDeleteDeployed(t *testing.T) { + storage := Init(driver.NewMemory()) + storage.Log = t.Logf + storage.MaxHistory = 3 + + const name = "angry-bird" + + // setup storage with test releases + setup := func() { + // release records + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusDeployed}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusFailed}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() + + // create the release records in the storage + assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") + assertErrNil(t.Fatal, storage.Create(rls1), "Storing release 'angry-bird' (v2)") + assertErrNil(t.Fatal, storage.Create(rls2), "Storing release 'angry-bird' (v3)") + assertErrNil(t.Fatal, storage.Create(rls3), "Storing release 'angry-bird' (v4)") + } + setup() + + rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusFailed}.ToRelease() + assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)") + + // On inserting the 5th record, we expect a total of 3 releases, but we expect version 2 + // (the only deployed release), to still exist + hist, err := storage.History(name) + if err != nil { + t.Fatal(err) + } else if len(hist) != storage.MaxHistory { + for _, item := range hist { + t.Logf("%s %v", item.Name, item.Version) + } + t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(hist)) + } + + expectedVersions := map[int]bool{ + 2: true, + 4: true, + 5: true, + } + + for _, item := range hist { + if !expectedVersions[item.Version] { + t.Errorf("Release version %d, found when not expected", item.Version) + } + } +} + func TestStorageLast(t *testing.T) { storage := Init(driver.NewMemory()) @@ -301,10 +420,10 @@ func TestStorageLast(t *testing.T) { // Set up storage with test releases. setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.Status_SUPERSEDED}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.Status_FAILED}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -327,10 +446,10 @@ func TestStorageLast(t *testing.T) { type ReleaseTestData struct { Name string - Version int32 + Version int Manifest string Namespace string - Status rspb.Status_Code + Status rspb.Status } func (test ReleaseTestData) ToRelease() *rspb.Release { @@ -339,7 +458,7 @@ func (test ReleaseTestData) ToRelease() *rspb.Release { Version: test.Version, Manifest: test.Manifest, Namespace: test.Namespace, - Info: &rspb.Info{Status: &rspb.Status{Code: test.Status}}, + Info: &rspb.Info{Status: test.Status}, } } diff --git a/pkg/strvals/doc.go b/pkg/strvals/doc.go index d2b859e67d5..f1729058710 100644 --- a/pkg/strvals/doc.go +++ b/pkg/strvals/doc.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 diff --git a/pkg/strvals/parser.go b/pkg/strvals/parser.go index aa3d15904d5..457b99f94a7 100644 --- a/pkg/strvals/parser.go +++ b/pkg/strvals/parser.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -17,13 +17,13 @@ package strvals import ( "bytes" - "errors" "fmt" "io" "strconv" "strings" - "github.com/ghodss/yaml" + "github.com/pkg/errors" + "sigs.k8s.io/yaml" ) // ErrNotList indicates that a non-list was treated as a list. @@ -71,7 +71,21 @@ func ParseInto(s string, dest map[string]interface{}) error { return t.parse() } -// ParseIntoString parses a strvals line nad merges the result into dest. +// ParseFile parses a set line, but its final value is loaded from the file at the path specified by the original value. +// +// A set line is of the form name1=path1,name2=path2 +// +// When the files at path1 and path2 contained "val1" and "val2" respectively, the set line is consumed as +// name1=val1,name2=val2 +func ParseFile(s string, reader RunesValueReader) (map[string]interface{}, error) { + vals := map[string]interface{}{} + scanner := bytes.NewBufferString(s) + t := newFileParser(scanner, vals, reader) + err := t.parse() + return vals, err +} + +// ParseIntoString parses a strvals line and merges the result into dest. // // This method always returns a string as the value. func ParseIntoString(s string, dest map[string]interface{}) error { @@ -80,16 +94,39 @@ func ParseIntoString(s string, dest map[string]interface{}) error { return t.parse() } +// ParseIntoFile parses a filevals line and merges the result into dest. +// +// This method always returns a string as the value. +func ParseIntoFile(s string, dest map[string]interface{}, reader RunesValueReader) error { + scanner := bytes.NewBufferString(s) + t := newFileParser(scanner, dest, reader) + return t.parse() +} + +// RunesValueReader is a function that takes the given value (a slice of runes) +// and returns the parsed value +type RunesValueReader func([]rune) (interface{}, error) + // parser is a simple parser that takes a strvals line and parses it into a // map representation. +// +// where sc is the source of the original data being parsed +// where data is the final parsed data from the parses with correct types type parser struct { - sc *bytes.Buffer - data map[string]interface{} - st bool + sc *bytes.Buffer + data map[string]interface{} + reader RunesValueReader } func newParser(sc *bytes.Buffer, data map[string]interface{}, stringBool bool) *parser { - return &parser{sc: sc, data: data, st: stringBool} + stringConverter := func(rs []rune) (interface{}, error) { + return typedVal(rs, stringBool), nil + } + return &parser{sc: sc, data: data, reader: stringConverter} +} + +func newFileParser(sc *bytes.Buffer, data map[string]interface{}, reader RunesValueReader) *parser { + return &parser{sc: sc, data: data, reader: reader} } func (t *parser) parse() error { @@ -113,7 +150,12 @@ func runeSet(r []rune) map[rune]bool { return s } -func (t *parser) key(data map[string]interface{}) error { +func (t *parser) key(data map[string]interface{}) (reterr error) { + defer func() { + if r := recover(); r != nil { + reterr = fmt.Errorf("unable to parse key: %s", r) + } + }() stop := runeSet([]rune{'=', '[', ',', '.'}) for { switch k, last, err := runesUntil(t.sc, stop); { @@ -121,14 +163,14 @@ func (t *parser) key(data map[string]interface{}) error { if len(k) == 0 { return err } - return fmt.Errorf("key %q has no value", string(k)) + return errors.Errorf("key %q has no value", string(k)) //set(data, string(k), "") //return err case last == '[': // We are in a list index context, so we need to set an index. i, err := t.keyIndex() if err != nil { - return fmt.Errorf("error parsing index: %s", err) + return errors.Wrap(err, "error parsing index") } kk := string(k) // Find or create target list @@ -153,8 +195,12 @@ func (t *parser) key(data map[string]interface{}) error { set(data, string(k), "") return e case ErrNotList: - v, e := t.val() - set(data, string(k), typedVal(v, t.st)) + rs, e := t.val() + if e != nil && e != io.EOF { + return e + } + v, e := t.reader(rs) + set(data, string(k), v) return e default: return e @@ -163,7 +209,7 @@ func (t *parser) key(data map[string]interface{}) error { case last == ',': // No value given. Set the value to empty string. Return error. set(data, string(k), "") - return fmt.Errorf("key %q has no value (cannot end with ,)", string(k)) + return errors.Errorf("key %q has no value (cannot end with ,)", string(k)) case last == '.': // First, create or find the target map. inner := map[string]interface{}{} @@ -174,7 +220,7 @@ func (t *parser) key(data map[string]interface{}) error { // Recurse e := t.key(inner) if len(inner) == 0 { - return fmt.Errorf("key map %q has no value", string(k)) + return errors.Errorf("key map %q has no value", string(k)) } set(data, string(k), inner) return e @@ -190,14 +236,26 @@ func set(data map[string]interface{}, key string, val interface{}) { data[key] = val } -func setIndex(list []interface{}, index int, val interface{}) []interface{} { +func setIndex(list []interface{}, index int, val interface{}) (l2 []interface{}, err error) { + // There are possible index values that are out of range on a target system + // causing a panic. This will catch the panic and return an error instead. + // The value of the index that causes a panic varies from system to system. + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("error processing index %d: %s", index, r) + } + }() + + if index < 0 { + return list, fmt.Errorf("negative %d index not allowed", index) + } if len(list) <= index { newlist := make([]interface{}, index+1) copy(newlist, list) list = newlist } list[index] = val - return list + return list, nil } func (t *parser) keyIndex() (int, error) { @@ -212,46 +270,76 @@ func (t *parser) keyIndex() (int, error) { } func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) { + if i < 0 { + return list, fmt.Errorf("negative %d index not allowed", i) + } stop := runeSet([]rune{'[', '.', '='}) switch k, last, err := runesUntil(t.sc, stop); { case len(k) > 0: - return list, fmt.Errorf("unexpected data at end of array index: %q", k) + return list, errors.Errorf("unexpected data at end of array index: %q", k) case err != nil: return list, err case last == '=': vl, e := t.valList() switch e { case nil: - return setIndex(list, i, vl), nil + return setIndex(list, i, vl) case io.EOF: - return setIndex(list, i, ""), err + return setIndex(list, i, "") case ErrNotList: - v, e := t.val() - return setIndex(list, i, typedVal(v, t.st)), e + rs, e := t.val() + if e != nil && e != io.EOF { + return list, e + } + v, e := t.reader(rs) + if e != nil { + return list, e + } + return setIndex(list, i, v) default: return list, e } case last == '[': // now we have a nested list. Read the index and handle. - i, err := t.keyIndex() + nextI, err := t.keyIndex() if err != nil { - return list, fmt.Errorf("error parsing index: %s", err) + return list, errors.Wrap(err, "error parsing index") + } + var crtList []interface{} + if len(list) > i { + // If nested list already exists, take the value of list to next cycle. + existed := list[i] + if existed != nil { + crtList = list[i].([]interface{}) + } } // Now we need to get the value after the ]. - list2, err := t.listItem(list, i) - return setIndex(list, i, list2), err + list2, err := t.listItem(crtList, nextI) + if err != nil { + return list, err + } + return setIndex(list, i, list2) case last == '.': // We have a nested object. Send to t.key inner := map[string]interface{}{} if len(list) > i { - inner = list[i].(map[string]interface{}) + var ok bool + inner, ok = list[i].(map[string]interface{}) + if !ok { + // We have indices out of order. Initialize empty value. + list[i] = map[string]interface{}{} + inner = list[i].(map[string]interface{}) + } } // Recurse e := t.key(inner) - return setIndex(list, i, inner), e + if e != nil { + return list, e + } + return setIndex(list, i, inner) default: - return nil, fmt.Errorf("parse error: unexpected token %v", last) + return nil, errors.Errorf("parse error: unexpected token %v", last) } } @@ -275,7 +363,7 @@ func (t *parser) valList() ([]interface{}, error) { list := []interface{}{} stop := runeSet([]rune{',', '}'}) for { - switch v, last, err := runesUntil(t.sc, stop); { + switch rs, last, err := runesUntil(t.sc, stop); { case err != nil: if err == io.EOF { err = errors.New("list must terminate with '}'") @@ -286,10 +374,15 @@ func (t *parser) valList() ([]interface{}, error) { if r, _, e := t.sc.ReadRune(); e == nil && r != ',' { t.sc.UnreadRune() } - list = append(list, typedVal(v, t.st)) - return list, nil + v, e := t.reader(rs) + list = append(list, v) + return list, e case last == ',': - list = append(list, typedVal(v, t.st)) + v, e := t.reader(rs) + if e != nil { + return list, e + } + list = append(list, v) } } } @@ -321,6 +414,11 @@ func inMap(k rune, m map[rune]bool) bool { func typedVal(v []rune, st bool) interface{} { val := string(v) + + if st { + return val + } + if strings.EqualFold(val, "true") { return true } @@ -329,8 +427,16 @@ func typedVal(v []rune, st bool) interface{} { return false } - // If this value does not start with zero, and not returnString, try parsing it to an int - if !st && len(val) != 0 && val[0] != '0' { + if strings.EqualFold(val, "null") { + return nil + } + + if strings.EqualFold(val, "0") { + return int64(0) + } + + // If this value does not start with zero, try parsing it to an int + if len(val) != 0 && val[0] != '0' { if iv, err := strconv.ParseInt(val, 10, 64); err == nil { return iv } diff --git a/pkg/strvals/parser_test.go b/pkg/strvals/parser_test.go index fd287bf8a43..cef98ba0a15 100644 --- a/pkg/strvals/parser_test.go +++ b/pkg/strvals/parser_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright The Helm Authors. 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 @@ -18,7 +18,7 @@ package strvals import ( "testing" - "github.com/ghodss/yaml" + "sigs.k8s.io/yaml" ) func TestSetIndex(t *testing.T) { @@ -28,6 +28,7 @@ func TestSetIndex(t *testing.T) { expect []interface{} add int val int + err bool }{ { name: "short", @@ -35,6 +36,7 @@ func TestSetIndex(t *testing.T) { expect: []interface{}{0, 1, 2}, add: 2, val: 2, + err: false, }, { name: "equal", @@ -42,6 +44,7 @@ func TestSetIndex(t *testing.T) { expect: []interface{}{0, 2}, add: 1, val: 2, + err: false, }, { name: "long", @@ -49,17 +52,41 @@ func TestSetIndex(t *testing.T) { expect: []interface{}{0, 1, 2, 4, 4, 5}, add: 3, val: 4, + err: false, + }, + { + name: "negative", + initial: []interface{}{0, 1, 2, 3, 4, 5}, + expect: []interface{}{0, 1, 2, 3, 4, 5}, + add: -1, + val: 4, + err: true, }, } for _, tt := range tests { - got := setIndex(tt.initial, tt.add, tt.val) + got, err := setIndex(tt.initial, tt.add, tt.val) + + if err != nil && tt.err == false { + t.Fatalf("%s: Expected no error but error returned", tt.name) + } else if err == nil && tt.err == true { + t.Fatalf("%s: Expected error but no error returned", tt.name) + } + if len(got) != len(tt.expect) { t.Fatalf("%s: Expected length %d, got %d", tt.name, len(tt.expect), len(got)) } - if gg := got[tt.add].(int); gg != tt.val { - t.Errorf("%s, Expected value %d, got %d", tt.name, tt.val, gg) + if !tt.err { + if gg := got[tt.add].(int); gg != tt.val { + t.Errorf("%s, Expected value %d, got %d", tt.name, tt.val, gg) + } + } + + for k, v := range got { + if v != tt.expect[k] { + t.Errorf("%s, Expected value %d, got %d", tt.name, tt.expect[k], v) + } } } } @@ -75,12 +102,32 @@ func TestParseSet(t *testing.T) { expect: map[string]interface{}{"long_int_string": "1234567890"}, err: false, }, + { + str: "boolean=true", + expect: map[string]interface{}{"boolean": "true"}, + err: false, + }, + { + str: "is_null=null", + expect: map[string]interface{}{"is_null": "null"}, + err: false, + }, + { + str: "zero=0", + expect: map[string]interface{}{"zero": "0"}, + err: false, + }, } tests := []struct { str string expect map[string]interface{} err bool }{ + { + "name1=null,f=false,t=true", + map[string]interface{}{"name1": nil, "f": false, "t": true}, + false, + }, { "name1=value1", map[string]interface{}{"name1": "value1"}, @@ -108,10 +155,23 @@ func TestParseSet(t *testing.T) { str: "leading_zeros=00009", expect: map[string]interface{}{"leading_zeros": "00009"}, }, + { + str: "zero_int=0", + expect: map[string]interface{}{"zero_int": 0}, + }, { str: "long_int=1234567890", expect: map[string]interface{}{"long_int": 1234567890}, }, + { + str: "boolean=true", + expect: map[string]interface{}{"boolean": true}, + }, + { + str: "is_null=null", + expect: map[string]interface{}{"is_null": nil}, + err: false, + }, { str: "name1,name2=", err: true, @@ -238,6 +298,10 @@ func TestParseSet(t *testing.T) { }, }, }, + { + str: "list[0].foo=bar,list[-30].hello=world", + err: true, + }, { str: "list[0]=foo,list[1]=bar", expect: map[string]interface{}{"list": []string{"foo", "bar"}}, @@ -250,6 +314,10 @@ func TestParseSet(t *testing.T) { str: "list[0]=foo,list[3]=bar", expect: map[string]interface{}{"list": []interface{}{"foo", nil, nil, "bar"}}, }, + { + str: "list[0]=foo,list[-20]=bar", + err: true, + }, { str: "illegal[0]name.foo=bar", err: true, @@ -270,6 +338,34 @@ func TestParseSet(t *testing.T) { str: "nested[1][1]=1", expect: map[string]interface{}{"nested": []interface{}{nil, []interface{}{nil, 1}}}, }, + { + str: "name1.name2[0].foo=bar,name1.name2[1].foo=bar", + expect: map[string]interface{}{ + "name1": map[string]interface{}{ + "name2": []map[string]interface{}{{"foo": "bar"}, {"foo": "bar"}}, + }, + }, + }, + { + str: "name1.name2[1].foo=bar,name1.name2[0].foo=bar", + expect: map[string]interface{}{ + "name1": map[string]interface{}{ + "name2": []map[string]interface{}{{"foo": "bar"}, {"foo": "bar"}}, + }, + }, + }, + { + str: "name1.name2[1].foo=bar", + expect: map[string]interface{}{ + "name1": map[string]interface{}{ + "name2": []map[string]interface{}{nil, {"foo": "bar"}}, + }, + }, + }, + { + str: "]={}].", + err: true, + }, } for _, tt := range tests { @@ -325,22 +421,201 @@ func TestParseSet(t *testing.T) { } func TestParseInto(t *testing.T) { + tests := []struct { + input string + input2 string + got map[string]interface{} + expect map[string]interface{} + err bool + }{ + { + input: "outer.inner1=value1,outer.inner3=value3,outer.inner4=4", + got: map[string]interface{}{ + "outer": map[string]interface{}{ + "inner1": "overwrite", + "inner2": "value2", + }, + }, + expect: map[string]interface{}{ + "outer": map[string]interface{}{ + "inner1": "value1", + "inner2": "value2", + "inner3": "value3", + "inner4": 4, + }}, + err: false, + }, + { + input: "listOuter[0][0].type=listValue", + input2: "listOuter[0][0].status=alive", + got: map[string]interface{}{}, + expect: map[string]interface{}{ + "listOuter": [][]interface{}{{map[string]string{ + "type": "listValue", + "status": "alive", + }}}, + }, + err: false, + }, + { + input: "listOuter[0][0].type=listValue", + input2: "listOuter[1][0].status=alive", + got: map[string]interface{}{}, + expect: map[string]interface{}{ + "listOuter": [][]interface{}{ + { + map[string]string{"type": "listValue"}, + }, + { + map[string]string{"status": "alive"}, + }, + }, + }, + err: false, + }, + { + input: "listOuter[0][1][0].type=listValue", + input2: "listOuter[0][0][1].status=alive", + got: map[string]interface{}{ + "listOuter": []interface{}{ + []interface{}{ + []interface{}{ + map[string]string{"exited": "old"}, + }, + }, + }, + }, + expect: map[string]interface{}{ + "listOuter": [][][]interface{}{ + { + { + map[string]string{"exited": "old"}, + map[string]string{"status": "alive"}, + }, + { + map[string]string{"type": "listValue"}, + }, + }, + }, + }, + err: false, + }, + } + for _, tt := range tests { + if err := ParseInto(tt.input, tt.got); err != nil { + t.Fatal(err) + } + if tt.err { + t.Errorf("%s: Expected error. Got nil", tt.input) + } + + if tt.input2 != "" { + if err := ParseInto(tt.input2, tt.got); err != nil { + t.Fatal(err) + } + if tt.err { + t.Errorf("%s: Expected error. Got nil", tt.input2) + } + } + + y1, err := yaml.Marshal(tt.expect) + if err != nil { + t.Fatal(err) + } + y2, err := yaml.Marshal(tt.got) + if err != nil { + t.Fatalf("Error serializing parsed value: %s", err) + } + + if string(y1) != string(y2) { + t.Errorf("%s: Expected:\n%s\nGot:\n%s", tt.input, y1, y2) + } + } +} + +func TestParseIntoString(t *testing.T) { got := map[string]interface{}{ "outer": map[string]interface{}{ "inner1": "overwrite", "inner2": "value2", }, } - input := "outer.inner1=value1,outer.inner3=value3" + input := "outer.inner1=1,outer.inner3=3" expect := map[string]interface{}{ "outer": map[string]interface{}{ - "inner1": "value1", + "inner1": "1", "inner2": "value2", - "inner3": "value3", + "inner3": "3", }, } - if err := ParseInto(input, got); err != nil { + if err := ParseIntoString(input, got); err != nil { + t.Fatal(err) + } + + y1, err := yaml.Marshal(expect) + if err != nil { + t.Fatal(err) + } + y2, err := yaml.Marshal(got) + if err != nil { + t.Fatalf("Error serializing parsed value: %s", err) + } + + if string(y1) != string(y2) { + t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2) + } +} + +func TestParseFile(t *testing.T) { + input := "name1=path1" + expect := map[string]interface{}{ + "name1": "value1", + } + rs2v := func(rs []rune) (interface{}, error) { + v := string(rs) + if v != "path1" { + t.Errorf("%s: runesToVal: Expected value path1, got %s", input, v) + return "", nil + } + return "value1", nil + } + + got, err := ParseFile(input, rs2v) + if err != nil { + t.Fatal(err) + } + + y1, err := yaml.Marshal(expect) + if err != nil { + t.Fatal(err) + } + y2, err := yaml.Marshal(got) + if err != nil { + t.Fatalf("Error serializing parsed value: %s", err) + } + + if string(y1) != string(y2) { + t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2) + } +} + +func TestParseIntoFile(t *testing.T) { + got := map[string]interface{}{} + input := "name1=path1" + expect := map[string]interface{}{ + "name1": "value1", + } + rs2v := func(rs []rune) (interface{}, error) { + v := string(rs) + if v != "path1" { + t.Errorf("%s: runesToVal: Expected value path1, got %s", input, v) + return "", nil + } + return "value1", nil + } + + if err := ParseIntoFile(input, got, rs2v); err != nil { t.Fatal(err) } diff --git a/pkg/sympath/walk_test.go b/pkg/sympath/walk_test.go deleted file mode 100644 index d86d8dabd4f..00000000000 --- a/pkg/sympath/walk_test.go +++ /dev/null @@ -1,134 +0,0 @@ -/* -Copyright (c) for portions of walk_test.go are held by The Go Authors, 2009 and are provided under -the BSD license. - -https://github.com/golang/go/blob/master/LICENSE - -Copyright 2017 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 sympath - -import ( - "os" - "path/filepath" - "testing" -) - -type Node struct { - name string - entries []*Node // nil if the entry is a file - mark int -} - -var tree = &Node{ - "testdata", - []*Node{ - {"a", nil, 0}, - {"b", []*Node{}, 0}, - {"c", nil, 0}, - { - "d", - []*Node{ - {"x", nil, 0}, - {"y", []*Node{}, 0}, - { - "z", - []*Node{ - {"u", nil, 0}, - {"v", nil, 0}, - {"w", nil, 0}, - }, - 0, - }, - }, - 0, - }, - }, - 0, -} - -func walkTree(n *Node, path string, f func(path string, n *Node)) { - f(path, n) - for _, e := range n.entries { - walkTree(e, filepath.Join(path, e.name), f) - } -} - -func makeTree(t *testing.T) { - walkTree(tree, tree.name, func(path string, n *Node) { - if n.entries == nil { - fd, err := os.Create(path) - if err != nil { - t.Errorf("makeTree: %v", err) - return - } - fd.Close() - } else { - os.Mkdir(path, 0770) - } - }) -} - -func checkMarks(t *testing.T, report bool) { - walkTree(tree, tree.name, func(path string, n *Node) { - if n.mark != 1 && report { - t.Errorf("node %s mark = %d; expected 1", path, n.mark) - } - n.mark = 0 - }) -} - -// Assumes that each node name is unique. Good enough for a test. -// If clear is true, any incoming error is cleared before return. The errors -// are always accumulated, though. -func mark(info os.FileInfo, err error, errors *[]error, clear bool) error { - if err != nil { - *errors = append(*errors, err) - if clear { - return nil - } - return err - } - name := info.Name() - walkTree(tree, tree.name, func(path string, n *Node) { - if n.name == name { - n.mark++ - } - }) - return nil -} - -func TestWalk(t *testing.T) { - makeTree(t) - errors := make([]error, 0, 10) - clear := true - markFn := func(path string, info os.FileInfo, err error) error { - return mark(info, err, &errors, clear) - } - // Expect no errors. - err := Walk(tree.name, markFn) - if err != nil { - t.Fatalf("no error expected, found: %s", err) - } - if len(errors) != 0 { - t.Fatalf("unexpected errors: %s", errors) - } - checkMarks(t, true) - - // cleanup - if err := os.RemoveAll(tree.name); err != nil { - t.Errorf("removeTree: %v", err) - } -} diff --git a/pkg/tiller/environment/environment.go b/pkg/tiller/environment/environment.go deleted file mode 100644 index 366fdf522af..00000000000 --- a/pkg/tiller/environment/environment.go +++ /dev/null @@ -1,227 +0,0 @@ -/* -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 environment describes the operating environment for Tiller. - -Tiller's environment encapsulates all of the service dependencies Tiller has. -These dependencies are expressed as interfaces so that alternate implementations -(mocks, etc.) can be easily generated. -*/ -package environment - -import ( - "io" - "time" - - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/kubectl/resource" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/engine" - "k8s.io/helm/pkg/kube" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/storage" - "k8s.io/helm/pkg/storage/driver" -) - -// DefaultTillerNamespace is the default namespace for Tiller. -const DefaultTillerNamespace = "kube-system" - -// GoTplEngine is the name of the Go template engine, as registered in the EngineYard. -const GoTplEngine = "gotpl" - -// DefaultEngine points to the engine that the EngineYard should treat as the -// default. A chart that does not specify an engine may be run through the -// default engine. -var DefaultEngine = GoTplEngine - -// EngineYard maps engine names to engine implementations. -type EngineYard map[string]Engine - -// Get retrieves a template engine by name. -// -// If no matching template engine is found, the second return value will -// be false. -func (y EngineYard) Get(k string) (Engine, bool) { - e, ok := y[k] - return e, ok -} - -// Default returns the default template engine. -// -// The default is specified by DefaultEngine. -// -// If the default template engine cannot be found, this panics. -func (y EngineYard) Default() Engine { - d, ok := y[DefaultEngine] - if !ok { - // This is a developer error! - panic("Default template engine does not exist") - } - return d -} - -// Engine represents a template engine that can render templates. -// -// For some engines, "rendering" includes both compiling and executing. (Other -// engines do not distinguish between phases.) -// -// The engine returns a map where the key is the named output entity (usually -// a file name) and the value is the rendered content of the template. -// -// An Engine must be capable of executing multiple concurrent requests, but -// without tainting one request's environment with data from another request. -type Engine interface { - // Render renders a chart. - // - // It receives a chart, a config, and a map of overrides to the config. - // Overrides are assumed to be passed from the system, not the user. - Render(*chart.Chart, chartutil.Values) (map[string]string, error) -} - -// KubeClient represents a client capable of communicating with the Kubernetes API. -// -// A KubeClient must be concurrency safe. -type KubeClient interface { - // Create creates one or more resources. - // - // namespace must contain a valid existing namespace. - // - // reader must contain a YAML stream (one or more YAML documents separated - // by "\n---\n"). - Create(namespace string, reader io.Reader, timeout int64, shouldWait bool) error - - // Get gets one or more resources. Returned string hsa the format like kubectl - // provides with the column headers separating the resource types. - // - // namespace must contain a valid existing namespace. - // - // reader must contain a YAML stream (one or more YAML documents separated - // by "\n---\n"). - Get(namespace string, reader io.Reader) (string, error) - - // Delete destroys one or more resources. - // - // namespace must contain a valid existing namespace. - // - // 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, timeout int64, shouldWait bool) error - - // Update updates one or more resources or creates the resource - // if it doesn't exist. - // - // namespace must contain a valid existing namespace. - // - // reader must contain a YAML stream (one or more YAML documents separated - // by "\n---\n"). - Update(namespace string, originalReader, modifiedReader io.Reader, force bool, recreate bool, timeout int64, shouldWait bool) error - - Build(namespace string, reader io.Reader) (kube.Result, error) - BuildUnstructured(namespace string, reader io.Reader) (kube.Result, error) - - // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase - // and returns said phase (PodSucceeded or PodFailed qualify). - WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (core.PodPhase, error) -} - -// PrintingKubeClient implements KubeClient, but simply prints the reader to -// the given output. -type PrintingKubeClient struct { - Out io.Writer -} - -// Create prints the values of what would be created with a real KubeClient. -func (p *PrintingKubeClient) Create(ns string, r io.Reader, timeout int64, shouldWait bool) error { - _, err := io.Copy(p.Out, r) - return err -} - -// Get prints the values of what would be created with a real KubeClient. -func (p *PrintingKubeClient) Get(ns string, r io.Reader) (string, error) { - _, err := io.Copy(p.Out, r) - return "", err -} - -// Delete implements KubeClient delete. -// -// It only prints out the content to be deleted. -func (p *PrintingKubeClient) Delete(ns string, r io.Reader) error { - _, err := io.Copy(p.Out, r) - return err -} - -// WatchUntilReady implements KubeClient WatchUntilReady. -func (p *PrintingKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error { - _, err := io.Copy(p.Out, r) - return err -} - -// Update implements KubeClient Update. -func (p *PrintingKubeClient) Update(ns string, currentReader, modifiedReader io.Reader, force bool, recreate bool, timeout int64, shouldWait bool) error { - _, err := io.Copy(p.Out, modifiedReader) - return err -} - -// Build implements KubeClient Build. -func (p *PrintingKubeClient) Build(ns string, reader io.Reader) (kube.Result, error) { - return []*resource.Info{}, nil -} - -// BuildUnstructured implements KubeClient BuildUnstructured. -func (p *PrintingKubeClient) BuildUnstructured(ns string, reader io.Reader) (kube.Result, error) { - return []*resource.Info{}, nil -} - -// WaitAndGetCompletedPodPhase implements KubeClient WaitAndGetCompletedPodPhase. -func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (core.PodPhase, error) { - _, err := io.Copy(p.Out, reader) - return core.PodUnknown, err -} - -// Environment provides the context for executing a client request. -// -// All services in a context are concurrency safe. -type Environment struct { - // EngineYard provides access to the known template engines. - EngineYard EngineYard - // Releases stores records of releases. - Releases *storage.Storage - // KubeClient is a Kubernetes API client. - KubeClient KubeClient -} - -// New returns an environment initialized with the defaults. -func New() *Environment { - e := engine.New() - var ey EngineYard = map[string]Engine{ - // Currently, the only template engine we support is the GoTpl one. But - // we can easily add some here. - GoTplEngine: e, - } - - return &Environment{ - EngineYard: ey, - Releases: storage.Init(driver.NewMemory()), - } -} diff --git a/pkg/tiller/environment/environment_test.go b/pkg/tiller/environment/environment_test.go deleted file mode 100644 index d8c82b90110..00000000000 --- a/pkg/tiller/environment/environment_test.go +++ /dev/null @@ -1,110 +0,0 @@ -/* -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 environment - -import ( - "bytes" - "io" - "testing" - "time" - - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/kubectl/resource" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/kube" - "k8s.io/helm/pkg/proto/hapi/chart" -) - -type mockEngine struct { - out map[string]string -} - -func (e *mockEngine) Render(chrt *chart.Chart, v chartutil.Values) (map[string]string, error) { - return e.out, nil -} - -type mockKubeClient struct{} - -func (k *mockKubeClient) Create(ns string, r io.Reader, timeout int64, shouldWait bool) error { - return nil -} -func (k *mockKubeClient) Get(ns string, r io.Reader) (string, error) { - return "", nil -} -func (k *mockKubeClient) Delete(ns string, r io.Reader) error { - return nil -} -func (k *mockKubeClient) Update(ns string, currentReader, modifiedReader io.Reader, force bool, recreate bool, timeout int64, shouldWait bool) error { - return nil -} -func (k *mockKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error { - return nil -} -func (k *mockKubeClient) Build(ns string, reader io.Reader) (kube.Result, error) { - return []*resource.Info{}, nil -} -func (k *mockKubeClient) BuildUnstructured(ns string, reader io.Reader) (kube.Result, error) { - return []*resource.Info{}, nil -} -func (k *mockKubeClient) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (core.PodPhase, error) { - return core.PodUnknown, nil -} - -func (k *mockKubeClient) WaitAndGetCompletedPodStatus(namespace string, reader io.Reader, timeout time.Duration) (core.PodPhase, error) { - return "", nil -} - -var _ Engine = &mockEngine{} -var _ KubeClient = &mockKubeClient{} -var _ KubeClient = &PrintingKubeClient{} - -func TestEngine(t *testing.T) { - eng := &mockEngine{out: map[string]string{"albatross": "test"}} - - env := New() - env.EngineYard = EngineYard(map[string]Engine{"test": eng}) - - if engine, ok := env.EngineYard.Get("test"); !ok { - t.Errorf("failed to get engine from EngineYard") - } else if out, err := engine.Render(&chart.Chart{}, map[string]interface{}{}); err != nil { - t.Errorf("unexpected template error: %s", err) - } else if out["albatross"] != "test" { - t.Errorf("expected 'test', got %q", out["albatross"]) - } -} - -func TestKubeClient(t *testing.T) { - kc := &mockKubeClient{} - env := New() - env.KubeClient = kc - - manifests := map[string]string{ - "foo": "name: value\n", - "bar": "name: value\n", - } - - b := bytes.NewBuffer(nil) - for _, content := range manifests { - b.WriteString("\n---\n") - b.WriteString(content) - } - - if err := env.KubeClient.Create("sharry-bobbins", b, 300, false); err != nil { - t.Errorf("Kubeclient failed: %s", err) - } -} diff --git a/pkg/tiller/hook_sorter.go b/pkg/tiller/hook_sorter.go deleted file mode 100644 index 42d546620d3..00000000000 --- a/pkg/tiller/hook_sorter.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -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 tiller - -import ( - "sort" - - "k8s.io/helm/pkg/proto/hapi/release" -) - -// sortByHookWeight does an in-place sort of hooks by their supplied weight. -func sortByHookWeight(hooks []*release.Hook) []*release.Hook { - hs := newHookWeightSorter(hooks) - sort.Sort(hs) - return hs.hooks -} - -type hookWeightSorter struct { - hooks []*release.Hook -} - -func newHookWeightSorter(h []*release.Hook) *hookWeightSorter { - return &hookWeightSorter{ - hooks: h, - } -} - -func (hs *hookWeightSorter) Len() int { return len(hs.hooks) } - -func (hs *hookWeightSorter) Swap(i, j int) { - hs.hooks[i], hs.hooks[j] = hs.hooks[j], hs.hooks[i] -} - -func (hs *hookWeightSorter) Less(i, j int) bool { - if hs.hooks[i].Weight == hs.hooks[j].Weight { - return hs.hooks[i].Name < hs.hooks[j].Name - } - return hs.hooks[i].Weight < hs.hooks[j].Weight -} diff --git a/pkg/tiller/hook_sorter_test.go b/pkg/tiller/hook_sorter_test.go deleted file mode 100644 index ac5b9bf8daf..00000000000 --- a/pkg/tiller/hook_sorter_test.go +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2017 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 tiller - -import ( - "testing" - - "k8s.io/helm/pkg/proto/hapi/release" -) - -func TestHookSorter(t *testing.T) { - hooks := []*release.Hook{ - { - Name: "g", - Kind: "pre-install", - Weight: 99, - }, - { - Name: "f", - Kind: "pre-install", - Weight: 3, - }, - { - Name: "b", - Kind: "pre-install", - Weight: -3, - }, - { - Name: "e", - Kind: "pre-install", - Weight: 3, - }, - { - Name: "a", - Kind: "pre-install", - Weight: -10, - }, - { - Name: "c", - Kind: "pre-install", - Weight: 0, - }, - { - Name: "d", - Kind: "pre-install", - Weight: 3, - }, - } - - res := sortByHookWeight(hooks) - got := "" - expect := "abcdefg" - for _, r := range res { - got += r.Name - } - if got != expect { - t.Errorf("Expected %q, got %q", expect, got) - } -} diff --git a/pkg/tiller/hooks.go b/pkg/tiller/hooks.go deleted file mode 100644 index e1e965d0857..00000000000 --- a/pkg/tiller/hooks.go +++ /dev/null @@ -1,233 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - "log" - "path" - "strconv" - "strings" - - "github.com/ghodss/yaml" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hooks" - "k8s.io/helm/pkg/proto/hapi/release" - util "k8s.io/helm/pkg/releaseutil" -) - -var events = map[string]release.Hook_Event{ - hooks.PreInstall: release.Hook_PRE_INSTALL, - hooks.PostInstall: release.Hook_POST_INSTALL, - hooks.PreDelete: release.Hook_PRE_DELETE, - hooks.PostDelete: release.Hook_POST_DELETE, - hooks.PreUpgrade: release.Hook_PRE_UPGRADE, - hooks.PostUpgrade: release.Hook_POST_UPGRADE, - hooks.PreRollback: release.Hook_PRE_ROLLBACK, - hooks.PostRollback: release.Hook_POST_ROLLBACK, - hooks.ReleaseTestSuccess: release.Hook_RELEASE_TEST_SUCCESS, - hooks.ReleaseTestFailure: release.Hook_RELEASE_TEST_FAILURE, -} - -// 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, - hooks.BeforeHookCreation: release.Hook_BEFORE_HOOK_CREATION, -} - -// 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 -} - -type manifestFile struct { - entries map[string]string - path string - apis chartutil.VersionSet -} - -// sortManifests takes a map of filename/YAML contents, splits the file -// by manifest entries, and sorts the entries 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. -// -// 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) { - result := &result{} - - for filePath, c := range files { - - // Skip partials. We could return these as a separate map, but there doesn't - // seem to be any need for that at this time. - if strings.HasPrefix(path.Base(filePath), "_") { - continue - } - // Skip empty files and log this. - if len(strings.TrimSpace(c)) == 0 { - log.Printf("info: manifest %q is empty. Skipping.", filePath) - continue - } - - manifestFile := &manifestFile{ - entries: util.SplitManifests(c), - path: filePath, - apis: apis, - } - - if err := manifestFile.sort(result); err != nil { - return result.hooks, result.generic, err - } - } - - return result.hooks, sortByKind(result.generic, sort), nil -} - -// sort takes a manifestFile object which may contain multiple resource definition -// entries and sorts each entry by hook types, and saves the resulting hooks and -// generic manifests (or non-hooks) to the result struct. -// -// To determine hook type, it looks for a YAML structure like this: -// -// kind: SomeKind -// apiVersion: v1 -// metadata: -// 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 - err := yaml.Unmarshal([]byte(m), &entry) - - if err != nil { - e := fmt.Errorf("YAML parse error on %s: %s", file.path, err) - return e - } - - if entry.Version != "" && !file.apis.Has(entry.Version) { - return fmt.Errorf("apiVersion %q in %s is not available", entry.Version, file.path) - } - - if !hasAnyAnnotation(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, - }) - continue - } - - hw := calculateHookWeight(entry) - - h := &release.Hook{ - Name: entry.Metadata.Name, - Kind: entry.Kind, - Path: file.path, - Manifest: m, - Events: []release.Hook_Event{}, - Weight: hw, - DeletePolicies: []release.Hook_DeletePolicy{}, - } - - isUnknownHook := false - for _, hookType := range strings.Split(hookTypes, ",") { - hookType = strings.ToLower(strings.TrimSpace(hookType)) - e, ok := events[hookType] - if !ok { - isUnknownHook = true - break - } - h.Events = append(h.Events, e) - } - - if isUnknownHook { - log.Printf("info: skipping unknown hook: %q", hookTypes) - continue - } - - result.hooks = append(result.hooks, h) - - operateAnnotationValues(entry, hooks.HookDeleteAnno, func(value string) { - policy, exist := deletePolices[value] - if exist { - h.DeletePolicies = append(h.DeletePolicies, policy) - } else { - log.Printf("info: skipping unknown hook delete policy: %q", value) - } - }) - } - - return nil -} - -func hasAnyAnnotation(entry util.SimpleHead) bool { - if entry.Metadata == nil || - entry.Metadata.Annotations == nil || - len(entry.Metadata.Annotations) == 0 { - return false - } - - return true -} - -func calculateHookWeight(entry util.SimpleHead) int32 { - hws := entry.Metadata.Annotations[hooks.HookWeightAnno] - hw, err := strconv.Atoi(hws) - if err != nil { - hw = 0 - } - - return int32(hw) -} - -func operateAnnotationValues(entry util.SimpleHead, annotation string, operate func(p string)) { - if dps, ok := entry.Metadata.Annotations[annotation]; ok { - for _, dp := range strings.Split(dps, ",") { - dp = strings.ToLower(strings.TrimSpace(dp)) - operate(dp) - } - } -} diff --git a/pkg/tiller/hooks_test.go b/pkg/tiller/hooks_test.go deleted file mode 100644 index 658f859f4e6..00000000000 --- a/pkg/tiller/hooks_test.go +++ /dev/null @@ -1,246 +0,0 @@ -/* -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 tiller - -import ( - "reflect" - "testing" - - "github.com/ghodss/yaml" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/proto/hapi/release" - util "k8s.io/helm/pkg/releaseutil" -) - -func TestSortManifests(t *testing.T) { - - data := []struct { - name []string - path string - kind []string - hooks map[string][]release.Hook_Event - manifest string - }{ - { - name: []string{"first"}, - path: "one", - kind: []string{"Job"}, - hooks: map[string][]release.Hook_Event{"first": {release.Hook_PRE_INSTALL}}, - manifest: `apiVersion: v1 -kind: Job -metadata: - name: first - labels: - doesnot: matter - annotations: - "helm.sh/hook": pre-install -`, - }, - { - name: []string{"second"}, - path: "two", - kind: []string{"ReplicaSet"}, - hooks: map[string][]release.Hook_Event{"second": {release.Hook_POST_INSTALL}}, - manifest: `kind: ReplicaSet -apiVersion: v1beta1 -metadata: - name: second - annotations: - "helm.sh/hook": post-install -`, - }, { - name: []string{"third"}, - path: "three", - kind: []string{"ReplicaSet"}, - hooks: map[string][]release.Hook_Event{"third": nil}, - manifest: `kind: ReplicaSet -apiVersion: v1beta1 -metadata: - name: third - annotations: - "helm.sh/hook": no-such-hook -`, - }, { - name: []string{"fourth"}, - path: "four", - kind: []string{"Pod"}, - hooks: map[string][]release.Hook_Event{"fourth": nil}, - manifest: `kind: Pod -apiVersion: v1 -metadata: - name: fourth - annotations: - nothing: here`, - }, { - name: []string{"fifth"}, - path: "five", - kind: []string{"ReplicaSet"}, - hooks: map[string][]release.Hook_Event{"fifth": {release.Hook_POST_DELETE, release.Hook_POST_INSTALL}}, - manifest: `kind: ReplicaSet -apiVersion: v1beta1 -metadata: - name: fifth - annotations: - "helm.sh/hook": post-delete, post-install -`, - }, { - // Regression test: files with an underscore in the base name should be skipped. - name: []string{"sixth"}, - path: "six/_six", - kind: []string{"ReplicaSet"}, - hooks: map[string][]release.Hook_Event{"sixth": nil}, - manifest: `invalid manifest`, // This will fail if partial is not skipped. - }, { - // Regression test: files with no content should be skipped. - name: []string{"seventh"}, - path: "seven", - kind: []string{"ReplicaSet"}, - hooks: map[string][]release.Hook_Event{"seventh": nil}, - manifest: "", - }, - { - name: []string{"eighth", "example-test"}, - path: "eight", - kind: []string{"ConfigMap", "Pod"}, - hooks: map[string][]release.Hook_Event{"eighth": nil, "example-test": {release.Hook_RELEASE_TEST_SUCCESS}}, - manifest: `kind: ConfigMap -apiVersion: v1 -metadata: - name: eighth -data: - name: value ---- -apiVersion: v1 -kind: Pod -metadata: - name: example-test - annotations: - "helm.sh/hook": test-success -`, - }, - } - - manifests := make(map[string]string, len(data)) - for _, o := range data { - manifests[o.path] = o.manifest - } - - hs, generic, err := sortManifests(manifests, chartutil.NewVersionSet("v1", "v1beta1"), InstallOrder) - if err != nil { - t.Fatalf("Unexpected error: %s", err) - } - - // This test will fail if 'six' or 'seven' was added. - if len(generic) != 2 { - t.Errorf("Expected 2 generic manifests, got %d", len(generic)) - } - - if len(hs) != 4 { - t.Errorf("Expected 4 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) - } - nameFound := false - for _, expectedName := range expect.name { - if out.Name == expectedName { - nameFound = true - } - } - if !nameFound { - t.Errorf("Got unexpected name %s", out.Name) - } - kindFound := false - for _, expectedKind := range expect.kind { - if out.Kind == expectedKind { - kindFound = true - } - } - if !kindFound { - t.Errorf("Got unexpected kind %s", out.Kind) - } - - expectedHooks := expect.hooks[out.Name] - if !reflect.DeepEqual(expectedHooks, out.Events) { - t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events) - } - - } - } - if !found { - t.Errorf("Result not found: %v", out) - } - } - - // Verify the sort order - sorted := []Manifest{} - for _, s := range data { - manifests := util.SplitManifests(s.manifest) - - for _, m := range manifests { - var sh util.SimpleHead - err := yaml.Unmarshal([]byte(m), &sh) - if err != nil { - // This is expected for manifests that are corrupt or empty. - t.Log(err) - continue - } - - name := sh.Metadata.Name - - //only keep track of non-hook manifests - if err == nil && s.hooks[name] == nil { - another := Manifest{ - Content: m, - Name: name, - Head: &sh, - } - sorted = append(sorted, another) - } - } - } - - 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) - } - } -} - -func TestVersionSet(t *testing.T) { - vs := chartutil.NewVersionSet("v1", "v1beta1", "extensions/alpha5", "batch/v1") - - if l := len(vs); l != 4 { - t.Errorf("Expected 4, got %d", l) - } - - if !vs.Has("extensions/alpha5") { - t.Error("No match for alpha5") - } - - if vs.Has("nosuch/extension") { - t.Error("Found nonexistent extension") - } -} diff --git a/pkg/tiller/kind_sorter.go b/pkg/tiller/kind_sorter.go deleted file mode 100644 index f367e65c872..00000000000 --- a/pkg/tiller/kind_sorter.go +++ /dev/null @@ -1,148 +0,0 @@ -/* -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 tiller - -import ( - "sort" -) - -// SortOrder is an ordering of Kinds. -type SortOrder []string - -// InstallOrder is the order in which manifests should be installed (by Kind). -// -// Those occurring earlier in the list get installed before those occurring later in the list. -var InstallOrder SortOrder = []string{ - "Namespace", - "ResourceQuota", - "LimitRange", - "Secret", - "ConfigMap", - "StorageClass", - "PersistentVolume", - "PersistentVolumeClaim", - "ServiceAccount", - "CustomResourceDefinition", - "ClusterRole", - "ClusterRoleBinding", - "Role", - "RoleBinding", - "Service", - "DaemonSet", - "Pod", - "ReplicationController", - "ReplicaSet", - "Deployment", - "StatefulSet", - "Job", - "CronJob", - "Ingress", - "APIService", -} - -// UninstallOrder is the order in which manifests should be uninstalled (by Kind). -// -// Those occurring earlier in the list get uninstalled before those occurring later in the list. -var UninstallOrder SortOrder = []string{ - "APIService", - "Ingress", - "Service", - "CronJob", - "Job", - "StatefulSet", - "Deployment", - "ReplicaSet", - "ReplicationController", - "Pod", - "DaemonSet", - "RoleBinding", - "Role", - "ClusterRoleBinding", - "ClusterRole", - "CustomResourceDefinition", - "ServiceAccount", - "PersistentVolumeClaim", - "PersistentVolume", - "StorageClass", - "ConfigMap", - "Secret", - "LimitRange", - "ResourceQuota", - "Namespace", -} - -// sortByKind does an in-place sort of manifests by Kind. -// -// Results are sorted by 'ordering' -func sortByKind(manifests []Manifest, ordering SortOrder) []Manifest { - ks := newKindSorter(manifests, ordering) - sort.Sort(ks) - return ks.manifests -} - -type kindSorter struct { - ordering map[string]int - manifests []Manifest -} - -func newKindSorter(m []Manifest, s SortOrder) *kindSorter { - o := make(map[string]int, len(s)) - for v, k := range s { - o[k] = v - } - - return &kindSorter{ - manifests: m, - ordering: o, - } -} - -func (k *kindSorter) Len() int { return len(k.manifests) } - -func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifests[j], k.manifests[i] } - -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] - // if same kind (including unknown) sub sort alphanumeric - if first == second { - // if both are unknown and of different kind sort by kind alphabetically - if !aok && !bok && a.Head.Kind != b.Head.Kind { - return a.Head.Kind < b.Head.Kind - } - return a.Name < b.Name - } - // unknown kind is last - if !aok { - return false - } - if !bok { - return true - } - // 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 deleted file mode 100644 index ef7296e890d..00000000000 --- a/pkg/tiller/kind_sorter_test.go +++ /dev/null @@ -1,217 +0,0 @@ -/* -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 tiller - -import ( - "bytes" - "testing" - - util "k8s.io/helm/pkg/releaseutil" -) - -func TestKindSorter(t *testing.T) { - manifests := []Manifest{ - { - Name: "i", - Head: &util.SimpleHead{Kind: "ClusterRole"}, - }, - { - Name: "j", - Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, - }, - { - Name: "e", - Head: &util.SimpleHead{Kind: "ConfigMap"}, - }, - { - Name: "u", - Head: &util.SimpleHead{Kind: "CronJob"}, - }, - { - Name: "2", - Head: &util.SimpleHead{Kind: "CustomResourceDefinition"}, - }, - { - Name: "n", - Head: &util.SimpleHead{Kind: "DaemonSet"}, - }, - { - Name: "r", - Head: &util.SimpleHead{Kind: "Deployment"}, - }, - { - Name: "!", - Head: &util.SimpleHead{Kind: "HonkyTonkSet"}, - }, - { - Name: "v", - Head: &util.SimpleHead{Kind: "Ingress"}, - }, - { - Name: "t", - Head: &util.SimpleHead{Kind: "Job"}, - }, - { - Name: "c", - Head: &util.SimpleHead{Kind: "LimitRange"}, - }, - { - Name: "a", - Head: &util.SimpleHead{Kind: "Namespace"}, - }, - { - Name: "f", - Head: &util.SimpleHead{Kind: "PersistentVolume"}, - }, - { - Name: "g", - Head: &util.SimpleHead{Kind: "PersistentVolumeClaim"}, - }, - { - Name: "o", - Head: &util.SimpleHead{Kind: "Pod"}, - }, - { - Name: "q", - Head: &util.SimpleHead{Kind: "ReplicaSet"}, - }, - { - Name: "p", - Head: &util.SimpleHead{Kind: "ReplicationController"}, - }, - { - Name: "b", - Head: &util.SimpleHead{Kind: "ResourceQuota"}, - }, - { - Name: "k", - Head: &util.SimpleHead{Kind: "Role"}, - }, - { - Name: "l", - Head: &util.SimpleHead{Kind: "RoleBinding"}, - }, - { - Name: "d", - Head: &util.SimpleHead{Kind: "Secret"}, - }, - { - Name: "m", - Head: &util.SimpleHead{Kind: "Service"}, - }, - { - Name: "h", - Head: &util.SimpleHead{Kind: "ServiceAccount"}, - }, - { - Name: "s", - Head: &util.SimpleHead{Kind: "StatefulSet"}, - }, - { - Name: "1", - Head: &util.SimpleHead{Kind: "StorageClass"}, - }, - { - Name: "w", - Head: &util.SimpleHead{Kind: "APIService"}, - }, - } - - for _, test := range []struct { - description string - order SortOrder - expected string - }{ - {"install", InstallOrder, "abcde1fgh2ijklmnopqrstuvw!"}, - {"uninstall", UninstallOrder, "wvmutsrqponlkji2hgf1edcba!"}, - } { - var buf bytes.Buffer - t.Run(test.description, func(t *testing.T) { - if got, want := len(test.expected), len(manifests); got != want { - t.Fatalf("Expected %d names in order, got %d", want, got) - } - defer buf.Reset() - for _, r := range sortByKind(manifests, test.order) { - buf.WriteString(r.Name) - } - if got := buf.String(); got != test.expected { - t.Errorf("Expected %q, got %q", test.expected, got) - } - }) - } -} - -// TestKindSorterSubSort verifies manifests of same kind are also sorted alphanumeric -func TestKindSorterSubSort(t *testing.T) { - manifests := []Manifest{ - { - Name: "a", - Head: &util.SimpleHead{Kind: "ClusterRole"}, - }, - { - Name: "A", - Head: &util.SimpleHead{Kind: "ClusterRole"}, - }, - { - Name: "0", - Head: &util.SimpleHead{Kind: "ConfigMap"}, - }, - { - Name: "1", - Head: &util.SimpleHead{Kind: "ConfigMap"}, - }, - { - Name: "z", - Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, - }, - { - Name: "!", - Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, - }, - { - Name: "u2", - Head: &util.SimpleHead{Kind: "Unknown"}, - }, - { - Name: "u1", - Head: &util.SimpleHead{Kind: "Unknown"}, - }, - { - Name: "t3", - Head: &util.SimpleHead{Kind: "Unknown2"}, - }, - } - for _, test := range []struct { - description string - order SortOrder - expected string - }{ - // expectation is sorted by kind (unknown is last) and then sub sorted alphabetically within each group - {"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", InstallOrder, "01Aa!zu1u2t3"}, - } { - var buf bytes.Buffer - t.Run(test.description, func(t *testing.T) { - defer buf.Reset() - for _, r := range sortByKind(manifests, test.order) { - 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_content.go b/pkg/tiller/release_content.go deleted file mode 100644 index fd783d6b68f..00000000000 --- a/pkg/tiller/release_content.go +++ /dev/null @@ -1,39 +0,0 @@ -/* -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 tiller - -import ( - ctx "golang.org/x/net/context" - - "k8s.io/helm/pkg/proto/hapi/services" -) - -// GetReleaseContent gets all of the stored information for the given release. -func (s *ReleaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleaseContentRequest) (*services.GetReleaseContentResponse, error) { - if err := validateReleaseName(req.Name); err != nil { - s.Log("releaseContent: Release name is invalid: %s", req.Name) - return nil, err - } - - if req.Version <= 0 { - rel, err := s.env.Releases.Last(req.Name) - return &services.GetReleaseContentResponse{Release: rel}, err - } - - rel, err := s.env.Releases.Get(req.Name, req.Version) - return &services.GetReleaseContentResponse{Release: rel}, err -} diff --git a/pkg/tiller/release_content_test.go b/pkg/tiller/release_content_test.go deleted file mode 100644 index 7c003f70911..00000000000 --- a/pkg/tiller/release_content_test.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -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 tiller - -import ( - "testing" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/services" -) - -func TestGetReleaseContent(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - if err := rs.env.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - - res, err := rs.GetReleaseContent(c, &services.GetReleaseContentRequest{Name: rel.Name, Version: 1}) - if err != nil { - t.Errorf("Error getting release content: %s", err) - } - - if res.Release.Chart.Metadata.Name != rel.Chart.Metadata.Name { - t.Errorf("Expected %q, got %q", rel.Chart.Metadata.Name, res.Release.Chart.Metadata.Name) - } -} diff --git a/pkg/tiller/release_history.go b/pkg/tiller/release_history.go deleted file mode 100644 index 0dd52597851..00000000000 --- a/pkg/tiller/release_history.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -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 tiller - -import ( - "golang.org/x/net/context" - - tpb "k8s.io/helm/pkg/proto/hapi/services" - relutil "k8s.io/helm/pkg/releaseutil" -) - -// GetHistory gets the history for a given release. -func (s *ReleaseServer) GetHistory(ctx context.Context, req *tpb.GetHistoryRequest) (*tpb.GetHistoryResponse, error) { - if err := validateReleaseName(req.Name); err != nil { - s.Log("getHistory: Release name is invalid: %s", req.Name) - return nil, err - } - - s.Log("getting history for release %s", req.Name) - h, err := s.env.Releases.History(req.Name) - if err != nil { - return nil, err - } - - relutil.Reverse(h, relutil.SortByRevision) - - var resp tpb.GetHistoryResponse - for i := 0; i < min(len(h), int(req.Max)); i++ { - resp.Releases = append(resp.Releases, h[i]) - } - - return &resp, nil -} - -func min(x, y int) int { - if x < y { - return x - } - return y -} diff --git a/pkg/tiller/release_history_test.go b/pkg/tiller/release_history_test.go deleted file mode 100644 index 5df98410f5e..00000000000 --- a/pkg/tiller/release_history_test.go +++ /dev/null @@ -1,115 +0,0 @@ -/* -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 tiller - -import ( - "reflect" - "testing" - - "k8s.io/helm/pkg/helm" - rpb "k8s.io/helm/pkg/proto/hapi/release" - tpb "k8s.io/helm/pkg/proto/hapi/services" -) - -func TestGetHistory_WithRevisions(t *testing.T) { - mk := func(name string, vers int32, code rpb.Status_Code) *rpb.Release { - return &rpb.Release{ - Name: name, - Version: vers, - Info: &rpb.Info{Status: &rpb.Status{Code: code}}, - } - } - - // GetReleaseHistoryTests - tests := []struct { - desc string - req *tpb.GetHistoryRequest - res *tpb.GetHistoryResponse - }{ - { - desc: "get release with history and default limit (max=256)", - req: &tpb.GetHistoryRequest{Name: "angry-bird", Max: 256}, - res: &tpb.GetHistoryResponse{Releases: []*rpb.Release{ - mk("angry-bird", 4, rpb.Status_DEPLOYED), - mk("angry-bird", 3, rpb.Status_SUPERSEDED), - mk("angry-bird", 2, rpb.Status_SUPERSEDED), - mk("angry-bird", 1, rpb.Status_SUPERSEDED), - }}, - }, - { - desc: "get release with history using result limit (max=2)", - req: &tpb.GetHistoryRequest{Name: "angry-bird", Max: 2}, - res: &tpb.GetHistoryResponse{Releases: []*rpb.Release{ - mk("angry-bird", 4, rpb.Status_DEPLOYED), - mk("angry-bird", 3, rpb.Status_SUPERSEDED), - }}, - }, - } - - // test release history for release 'angry-bird' - hist := []*rpb.Release{ - mk("angry-bird", 4, rpb.Status_DEPLOYED), - mk("angry-bird", 3, rpb.Status_SUPERSEDED), - mk("angry-bird", 2, rpb.Status_SUPERSEDED), - mk("angry-bird", 1, rpb.Status_SUPERSEDED), - } - - srv := rsFixture() - for _, rls := range hist { - if err := srv.env.Releases.Create(rls); err != nil { - t.Fatalf("Failed to create release: %s", err) - } - } - - // run tests - for _, tt := range tests { - res, err := srv.GetHistory(helm.NewContext(), tt.req) - if err != nil { - t.Fatalf("%s:\nFailed to get History of %q: %s", tt.desc, tt.req.Name, err) - } - if !reflect.DeepEqual(res, tt.res) { - t.Fatalf("%s:\nExpected:\n\t%+v\nActual\n\t%+v", tt.desc, tt.res, res) - } - } -} - -func TestGetHistory_WithNoRevisions(t *testing.T) { - tests := []struct { - desc string - req *tpb.GetHistoryRequest - }{ - { - desc: "get release with no history", - req: &tpb.GetHistoryRequest{Name: "sad-panda", Max: 256}, - }, - } - - // create release 'sad-panda' with no revision history - rls := namedReleaseStub("sad-panda", rpb.Status_DEPLOYED) - srv := rsFixture() - srv.env.Releases.Create(rls) - - for _, tt := range tests { - res, err := srv.GetHistory(helm.NewContext(), tt.req) - if err != nil { - t.Fatalf("%s:\nFailed to get History of %q: %s", tt.desc, tt.req.Name, err) - } - if len(res.Releases) > 1 { - t.Fatalf("%s:\nExpected zero items, got %d", tt.desc, len(res.Releases)) - } - } -} diff --git a/pkg/tiller/release_install.go b/pkg/tiller/release_install.go deleted file mode 100644 index 8e7fd3acdb6..00000000000 --- a/pkg/tiller/release_install.go +++ /dev/null @@ -1,225 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - "strings" - - ctx "golang.org/x/net/context" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hooks" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - relutil "k8s.io/helm/pkg/releaseutil" - "k8s.io/helm/pkg/timeconv" -) - -// InstallRelease installs a release and stores the release record. -func (s *ReleaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { - s.Log("preparing install for %s", req.Name) - rel, err := s.prepareRelease(req) - if err != nil { - s.Log("failed install prepare step: %s", err) - res := &services.InstallReleaseResponse{Release: rel} - - // On dry run, append the manifest contents to a failed release. This is - // a stop-gap until we can revisit an error backchannel post-2.0. - if req.DryRun && strings.HasPrefix(err.Error(), "YAML parse error") { - err = fmt.Errorf("%s\n%s", err, rel.Manifest) - } - return res, err - } - - s.Log("performing install for %s", req.Name) - res, err := s.performRelease(rel, req) - if err != nil { - s.Log("failed install perform step: %s", err) - } - return res, err -} - -// 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 - } - - name, err := s.uniqName(req.Name, req.ReuseName) - if err != nil { - return nil, err - } - - caps, err := capabilities(s.clientset.Discovery()) - if err != nil { - return nil, err - } - - revision := 1 - ts := timeconv.Now() - options := chartutil.ReleaseOptions{ - Name: name, - Time: ts, - Namespace: req.Namespace, - Revision: revision, - IsInstall: true, - } - valuesToRender, err := chartutil.ToRenderValuesCaps(req.Chart, req.Values, options, caps) - if err != nil { - return nil, err - } - - hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender, caps.APIVersions) - if err != nil { - // Return a release with partial data so that client can show debugging - // information. - rel := &release.Release{ - Name: name, - Namespace: req.Namespace, - Chart: req.Chart, - Config: req.Values, - Info: &release.Info{ - FirstDeployed: ts, - LastDeployed: ts, - Status: &release.Status{Code: release.Status_UNKNOWN}, - Description: fmt.Sprintf("Install failed: %s", err), - }, - Version: 0, - } - if manifestDoc != nil { - rel.Manifest = manifestDoc.String() - } - return rel, err - } - - // Store a release. - rel := &release.Release{ - Name: name, - Namespace: req.Namespace, - Chart: req.Chart, - Config: req.Values, - Info: &release.Info{ - FirstDeployed: ts, - LastDeployed: ts, - Status: &release.Status{Code: release.Status_PENDING_INSTALL}, - Description: "Initial install underway", // Will be overwritten. - }, - Manifest: manifestDoc.String(), - Hooks: hooks, - Version: int32(revision), - } - if len(notesTxt) > 0 { - rel.Info.Status.Notes = notesTxt - } - - err = validateManifest(s.env.KubeClient, req.Namespace, manifestDoc.Bytes()) - return rel, err -} - -// 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 { - s.Log("dry run for %s", r.Name) - res.Release.Info.Description = "Dry run complete" - return res, nil - } - - // pre-install hooks - if !req.DisableHooks { - if err := s.execHook(r.Hooks, r.Name, r.Namespace, hooks.PreInstall, req.Timeout); err != nil { - return res, err - } - } else { - s.Log("install hooks disabled for %s", req.Name) - } - - switch h, err := s.env.Releases.History(req.Name); { - // if this is a replace operation, append to the release history - case req.ReuseName && err == nil && len(h) >= 1: - s.Log("name reuse for %s requested, replacing release", req.Name) - // get latest release revision - relutil.Reverse(h, relutil.SortByRevision) - - // old release - old := h[0] - - // update old release status - old.Info.Status.Code = release.Status_SUPERSEDED - s.recordRelease(old, true) - - // update new release with next revision number - // so as to append to the old release's history - r.Version = old.Version + 1 - updateReq := &services.UpdateReleaseRequest{ - Wait: req.Wait, - Recreate: false, - Timeout: req.Timeout, - } - s.recordRelease(r, false) - if err := s.ReleaseModule.Update(old, r, updateReq, s.env); err != nil { - msg := fmt.Sprintf("Release replace %q failed: %s", r.Name, err) - s.Log("warning: %s", msg) - old.Info.Status.Code = release.Status_SUPERSEDED - r.Info.Status.Code = release.Status_FAILED - r.Info.Description = msg - s.recordRelease(old, true) - s.recordRelease(r, true) - return res, err - } - - default: - // nothing to replace, create as normal - // regular manifests - s.recordRelease(r, false) - if err := s.ReleaseModule.Create(r, req, s.env); err != nil { - msg := fmt.Sprintf("Release %q failed: %s", r.Name, err) - s.Log("warning: %s", msg) - r.Info.Status.Code = release.Status_FAILED - r.Info.Description = msg - s.recordRelease(r, true) - return res, fmt.Errorf("release %s failed: %s", r.Name, err) - } - } - - // post-install hooks - if !req.DisableHooks { - if err := s.execHook(r.Hooks, r.Name, r.Namespace, hooks.PostInstall, req.Timeout); err != nil { - msg := fmt.Sprintf("Release %q failed post-install: %s", r.Name, err) - s.Log("warning: %s", msg) - r.Info.Status.Code = release.Status_FAILED - r.Info.Description = msg - s.recordRelease(r, true) - return res, err - } - } - - r.Info.Status.Code = release.Status_DEPLOYED - r.Info.Description = "Install complete" - // This is a tricky case. The release has been created, but the result - // cannot be recorded. The truest thing to tell the user is that the - // release was created. However, the user will not be able to do anything - // further with this release. - // - // One possible strategy would be to do a timed retry to see if we can get - // this stored in the future. - s.recordRelease(r, true) - - return res, nil -} diff --git a/pkg/tiller/release_install_test.go b/pkg/tiller/release_install_test.go deleted file mode 100644 index 2f21dc46b52..00000000000 --- a/pkg/tiller/release_install_test.go +++ /dev/null @@ -1,415 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - "strings" - "testing" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/version" -) - -func TestInstallRelease(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - - req := installRequest() - res, err := rs.InstallRelease(c, req) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - if res.Release.Name == "" { - t.Errorf("Expected release name.") - } - if res.Release.Namespace != "spaced" { - t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Release.Namespace) - } - - rel, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) - } - - 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) - } - - if len(rel.Manifest) == 0 { - t.Errorf("Expected manifest in %v", res) - } - - if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { - t.Errorf("unexpected output: %s", rel.Manifest) - } - - if rel.Info.Description != "Install complete" { - t.Errorf("unexpected description: %s", rel.Info.Description) - } -} - -func TestInstallRelease_WithNotes(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - - req := installRequest( - withChart(withNotes(notesText)), - ) - res, err := rs.InstallRelease(c, req) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - if res.Release.Name == "" { - t.Errorf("Expected release name.") - } - if res.Release.Namespace != "spaced" { - t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Release.Namespace) - } - - rel, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) - } - - 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.Info.Status.Notes != notesText { - t.Fatalf("Expected '%s', got '%s'", notesText, rel.Info.Status.Notes) - } - - 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) - } - - if len(rel.Manifest) == 0 { - t.Errorf("Expected manifest in %v", res) - } - - if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { - t.Errorf("unexpected output: %s", rel.Manifest) - } - - if rel.Info.Description != "Install complete" { - t.Errorf("unexpected description: %s", rel.Info.Description) - } -} - -func TestInstallRelease_WithNotesRendered(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - - req := installRequest( - withChart(withNotes(notesText + " {{.Release.Name}}")), - ) - res, err := rs.InstallRelease(c, req) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - if res.Release.Name == "" { - t.Errorf("Expected release name.") - } - if res.Release.Namespace != "spaced" { - t.Errorf("Expected release namespace 'spaced', got '%s'.", res.Release.Namespace) - } - - rel, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) - } - - 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) - } - - expectedNotes := fmt.Sprintf("%s %s", notesText, res.Release.Name) - if rel.Info.Status.Notes != expectedNotes { - t.Fatalf("Expected '%s', got '%s'", expectedNotes, rel.Info.Status.Notes) - } - - 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) - } - - if len(rel.Manifest) == 0 { - t.Errorf("Expected manifest in %v", res) - } - - if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { - t.Errorf("unexpected output: %s", rel.Manifest) - } - - if rel.Info.Description != "Install complete" { - t.Errorf("unexpected description: %s", rel.Info.Description) - } -} - -func TestInstallRelease_TillerVersion(t *testing.T) { - version.Version = "2.2.0" - c := helm.NewContext() - rs := rsFixture() - - req := installRequest( - withChart(withTiller(">=2.2.0")), - ) - _, err := rs.InstallRelease(c, req) - if err != nil { - t.Fatalf("Expected valid range. Got %q", err) - } -} - -func TestInstallRelease_WrongTillerVersion(t *testing.T) { - version.Version = "2.2.0" - c := helm.NewContext() - rs := rsFixture() - - req := installRequest( - withChart(withTiller("<2.0.0")), - ) - _, err := rs.InstallRelease(c, req) - if err == nil { - t.Fatalf("Expected to fail because of wrong version") - } - - expect := "Chart incompatible with Tiller" - if !strings.Contains(err.Error(), expect) { - t.Errorf("Expected %q to contain %q", err.Error(), expect) - } -} - -func TestInstallRelease_WithChartAndDependencyNotes(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - - req := installRequest(withChart( - withNotes(notesText), - withDependency(withNotes(notesText+" child")), - )) - res, err := rs.InstallRelease(c, req) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - if res.Release.Name == "" { - t.Errorf("Expected release name.") - } - - rel, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) - } - - t.Logf("rel: %v", rel) - - if rel.Info.Status.Notes != notesText { - t.Fatalf("Expected '%s', got '%s'", notesText, rel.Info.Status.Notes) - } - - if rel.Info.Description != "Install complete" { - t.Errorf("unexpected description: %s", rel.Info.Description) - } -} - -func TestInstallRelease_DryRun(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - - req := installRequest(withDryRun(), - withChart(withSampleTemplates()), - ) - res, err := rs.InstallRelease(c, req) - if err != nil { - t.Errorf("Failed install: %s", err) - } - if res.Release.Name == "" { - t.Errorf("Expected release name.") - } - - if !strings.Contains(res.Release.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { - t.Errorf("unexpected output: %s", res.Release.Manifest) - } - - if !strings.Contains(res.Release.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world") { - t.Errorf("unexpected output: %s", res.Release.Manifest) - } - - if !strings.Contains(res.Release.Manifest, "hello: Earth") { - t.Errorf("Should contain partial content. %s", res.Release.Manifest) - } - - if strings.Contains(res.Release.Manifest, "hello: {{ template \"_planet\" . }}") { - t.Errorf("Should not contain partial templates itself. %s", res.Release.Manifest) - } - - if strings.Contains(res.Release.Manifest, "empty") { - t.Errorf("Should not contain template data for an empty file. %s", res.Release.Manifest) - } - - if _, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version); 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.") - } - - if res.Release.Info.Description != "Dry run complete" { - t.Errorf("unexpected description: %s", res.Release.Info.Description) - } -} - -func TestInstallRelease_NoHooks(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rs.env.Releases.Create(releaseStub()) - - req := installRequest(withDisabledHooks()) - res, err := rs.InstallRelease(c, req) - if err != nil { - t.Errorf("Failed install: %s", err) - } - - if hl := res.Release.Hooks[0].LastRun; hl != nil { - t.Errorf("Expected that no hooks were run. Got %d", hl) - } -} - -func TestInstallRelease_FailedHooks(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rs.env.Releases.Create(releaseStub()) - rs.env.KubeClient = newHookFailingKubeClient() - - req := installRequest() - res, err := rs.InstallRelease(c, req) - if err == nil { - t.Error("Expected failed install") - } - - if hl := res.Release.Info.Status.Code; hl != release.Status_FAILED { - t.Errorf("Expected FAILED release. Got %d", hl) - } -} - -func TestInstallRelease_ReuseName(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rel.Info.Status.Code = release.Status_DELETED - rs.env.Releases.Create(rel) - - req := installRequest( - withReuseName(), - withName(rel.Name), - ) - res, err := rs.InstallRelease(c, req) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - if res.Release.Name != rel.Name { - t.Errorf("expected %q, got %q", rel.Name, res.Release.Name) - } - - getreq := &services.GetReleaseStatusRequest{Name: rel.Name, Version: 0} - getres, err := rs.GetReleaseStatus(c, getreq) - if err != nil { - t.Errorf("Failed to retrieve release: %s", err) - } - if getres.Info.Status.Code != release.Status_DEPLOYED { - t.Errorf("Release status is %q", getres.Info.Status.Code) - } -} - -func TestInstallRelease_KubeVersion(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - - req := installRequest( - withChart(withKube(">=0.0.0")), - ) - _, err := rs.InstallRelease(c, req) - if err != nil { - t.Fatalf("Expected valid range. Got %q", err) - } -} - -func TestInstallRelease_WrongKubeVersion(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - - req := installRequest( - withChart(withKube(">=5.0.0")), - ) - - _, err := rs.InstallRelease(c, req) - if err == nil { - t.Fatalf("Expected to fail because of wrong version") - } - - expect := "Chart requires kubernetesVersion" - if !strings.Contains(err.Error(), expect) { - t.Errorf("Expected %q to contain %q", err.Error(), expect) - } -} diff --git a/pkg/tiller/release_list.go b/pkg/tiller/release_list.go deleted file mode 100644 index 72c21d97cc1..00000000000 --- a/pkg/tiller/release_list.go +++ /dev/null @@ -1,178 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - "github.com/golang/protobuf/proto" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - relutil "k8s.io/helm/pkg/releaseutil" - "regexp" -) - -// ListReleases lists the releases found by the server. -func (s *ReleaseServer) ListReleases(req *services.ListReleasesRequest, stream services.ReleaseService_ListReleasesServer) error { - if len(req.StatusCodes) == 0 { - req.StatusCodes = []release.Status_Code{release.Status_DEPLOYED} - } - - //rels, err := s.env.Releases.ListDeployed() - rels, err := s.env.Releases.ListFilterAll(func(r *release.Release) bool { - for _, sc := range req.StatusCodes { - if sc == r.Info.Status.Code { - return true - } - } - return false - }) - if err != nil { - return err - } - - if req.Namespace != "" { - rels, err = filterByNamespace(req.Namespace, rels) - if err != nil { - return err - } - } - - if len(req.Filter) != 0 { - rels, err = filterReleases(req.Filter, rels) - if err != nil { - return err - } - } - - total := int64(len(rels)) - - switch req.SortBy { - case services.ListSort_NAME: - relutil.SortByName(rels) - case services.ListSort_LAST_RELEASED: - relutil.SortByDate(rels) - } - - if req.SortOrder == services.ListSort_DESC { - ll := len(rels) - rr := make([]*release.Release, ll) - for i, item := range rels { - rr[ll-i-1] = item - } - rels = rr - } - - l := int64(len(rels)) - if req.Offset != "" { - - i := -1 - for ii, cur := range rels { - if cur.Name == req.Offset { - i = ii - } - } - if i == -1 { - return fmt.Errorf("offset %q not found", req.Offset) - } - - if len(rels) < i { - return fmt.Errorf("no items after %q", req.Offset) - } - - rels = rels[i:] - l = int64(len(rels)) - } - - if req.Limit == 0 { - req.Limit = ListDefaultLimit - } - - next := "" - if l > req.Limit { - next = rels[req.Limit].Name - rels = rels[0:req.Limit] - l = int64(len(rels)) - } - res := &services.ListReleasesResponse{ - Next: next, - Count: l, - Total: total, - } - chunks := s.partition(rels[:min(len(rels), int(req.Limit))], maxMsgSize-proto.Size(res)) - for res.Releases = range chunks { - if err := stream.Send(res); err != nil { - for range chunks { // drain - } - return err - } - } - return nil -} - -// partition packs releases into slices upto the capacity cap in bytes. -func (s *ReleaseServer) partition(rels []*release.Release, cap int) <-chan []*release.Release { - chunks := make(chan []*release.Release, 1) - go func() { - var ( - fill = 0 // fill is space available to fill - size int // size is size of a release - ) - var chunk []*release.Release - for _, rls := range rels { - if size = proto.Size(rls); size+fill > cap { - // Over-cap, push chunk onto channel to send over gRPC stream - s.Log("partitioned at %d with %d releases (cap=%d)", fill, len(chunk), cap) - chunks <- chunk - // reset paritioning state - chunk = chunk[:0] - fill = 0 - } - chunk = append(chunk, rls) - fill += size - } - if len(chunk) > 0 { - // send remaining if any - chunks <- chunk - } - close(chunks) - }() - return chunks -} - -func filterByNamespace(namespace string, rels []*release.Release) ([]*release.Release, error) { - matches := []*release.Release{} - for _, r := range rels { - if namespace == r.Namespace { - matches = append(matches, r) - } - } - return matches, nil -} - -func filterReleases(filter string, rels []*release.Release) ([]*release.Release, error) { - preg, err := regexp.Compile(filter) - if err != nil { - return rels, err - } - matches := []*release.Release{} - for _, r := range rels { - if preg.MatchString(r.Name) { - matches = append(matches, r) - } - } - return matches, nil -} diff --git a/pkg/tiller/release_list_test.go b/pkg/tiller/release_list_test.go deleted file mode 100644 index 64877422a94..00000000000 --- a/pkg/tiller/release_list_test.go +++ /dev/null @@ -1,233 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - "testing" - - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" -) - -func TestListReleases(t *testing.T) { - rs := rsFixture() - num := 7 - for i := 0; i < num; i++ { - rel := releaseStub() - rel.Name = fmt.Sprintf("rel-%d", i) - if err := rs.env.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - } - - mrs := &mockListServer{} - if err := rs.ListReleases(&services.ListReleasesRequest{Offset: "", Limit: 64}, mrs); err != nil { - t.Fatalf("Failed listing: %s", err) - } - - if len(mrs.val.Releases) != num { - t.Errorf("Expected %d releases, got %d", num, len(mrs.val.Releases)) - } -} - -func TestListReleasesByStatus(t *testing.T) { - rs := rsFixture() - stubs := []*release.Release{ - namedReleaseStub("kamal", release.Status_DEPLOYED), - namedReleaseStub("astrolabe", release.Status_DELETED), - namedReleaseStub("octant", release.Status_FAILED), - namedReleaseStub("sextant", release.Status_UNKNOWN), - } - for _, stub := range stubs { - if err := rs.env.Releases.Create(stub); err != nil { - t.Fatalf("Could not create stub: %s", err) - } - } - - tests := []struct { - statusCodes []release.Status_Code - names []string - }{ - { - names: []string{"kamal"}, - statusCodes: []release.Status_Code{release.Status_DEPLOYED}, - }, - { - names: []string{"astrolabe"}, - statusCodes: []release.Status_Code{release.Status_DELETED}, - }, - { - names: []string{"kamal", "octant"}, - statusCodes: []release.Status_Code{release.Status_DEPLOYED, release.Status_FAILED}, - }, - { - names: []string{"kamal", "astrolabe", "octant", "sextant"}, - statusCodes: []release.Status_Code{ - release.Status_DEPLOYED, - release.Status_DELETED, - release.Status_FAILED, - release.Status_UNKNOWN, - }, - }, - } - - for i, tt := range tests { - mrs := &mockListServer{} - if err := rs.ListReleases(&services.ListReleasesRequest{StatusCodes: tt.statusCodes, Offset: "", Limit: 64}, mrs); err != nil { - t.Fatalf("Failed listing %d: %s", i, err) - } - - if len(tt.names) != len(mrs.val.Releases) { - t.Fatalf("Expected %d releases, got %d", len(tt.names), len(mrs.val.Releases)) - } - - for _, name := range tt.names { - found := false - for _, rel := range mrs.val.Releases { - if rel.Name == name { - found = true - } - } - if !found { - t.Errorf("%d: Did not find name %q", i, name) - } - } - } -} - -func TestListReleasesSort(t *testing.T) { - rs := rsFixture() - - // Put them in by reverse order so that the mock doesn't "accidentally" - // sort. - num := 7 - for i := num; i > 0; i-- { - rel := releaseStub() - rel.Name = fmt.Sprintf("rel-%d", i) - if err := rs.env.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - } - - limit := 6 - mrs := &mockListServer{} - req := &services.ListReleasesRequest{ - Offset: "", - Limit: int64(limit), - SortBy: services.ListSort_NAME, - } - if err := rs.ListReleases(req, mrs); err != nil { - t.Fatalf("Failed listing: %s", err) - } - - if len(mrs.val.Releases) != limit { - t.Errorf("Expected %d releases, got %d", limit, len(mrs.val.Releases)) - } - - for i := 0; i < limit; i++ { - n := fmt.Sprintf("rel-%d", i+1) - if mrs.val.Releases[i].Name != n { - t.Errorf("Expected %q, got %q", n, mrs.val.Releases[i].Name) - } - } -} - -func TestListReleasesFilter(t *testing.T) { - rs := rsFixture() - names := []string{ - "axon", - "dendrite", - "neuron", - "neuroglia", - "synapse", - "nucleus", - "organelles", - } - num := 7 - for i := 0; i < num; i++ { - rel := releaseStub() - rel.Name = names[i] - if err := rs.env.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - } - - mrs := &mockListServer{} - req := &services.ListReleasesRequest{ - Offset: "", - Limit: 64, - Filter: "neuro[a-z]+", - SortBy: services.ListSort_NAME, - } - if err := rs.ListReleases(req, mrs); err != nil { - t.Fatalf("Failed listing: %s", err) - } - - if len(mrs.val.Releases) != 2 { - t.Errorf("Expected 2 releases, got %d", len(mrs.val.Releases)) - } - - if mrs.val.Releases[0].Name != "neuroglia" { - t.Errorf("Unexpected sort order: %v.", mrs.val.Releases) - } - if mrs.val.Releases[1].Name != "neuron" { - t.Errorf("Unexpected sort order: %v.", mrs.val.Releases) - } -} - -func TestReleasesNamespace(t *testing.T) { - rs := rsFixture() - - names := []string{ - "axon", - "dendrite", - "neuron", - "ribosome", - } - - namespaces := []string{ - "default", - "test123", - "test123", - "cerebellum", - } - num := 4 - for i := 0; i < num; i++ { - rel := releaseStub() - rel.Name = names[i] - rel.Namespace = namespaces[i] - if err := rs.env.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - } - - mrs := &mockListServer{} - req := &services.ListReleasesRequest{ - Offset: "", - Limit: 64, - Namespace: "test123", - } - - if err := rs.ListReleases(req, mrs); err != nil { - t.Fatalf("Failed listing: %s", err) - } - - if len(mrs.val.Releases) != 2 { - t.Errorf("Expected 2 releases, got %d", len(mrs.val.Releases)) - } -} diff --git a/pkg/tiller/release_modules.go b/pkg/tiller/release_modules.go deleted file mode 100644 index 876e1ba37a8..00000000000 --- a/pkg/tiller/release_modules.go +++ /dev/null @@ -1,183 +0,0 @@ -/* -Copyright 2017 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 tiller - -import ( - "bytes" - "errors" - "fmt" - "log" - "strings" - - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/kube" - "k8s.io/helm/pkg/proto/hapi/release" - rudderAPI "k8s.io/helm/pkg/proto/hapi/rudder" - "k8s.io/helm/pkg/proto/hapi/services" - relutil "k8s.io/helm/pkg/releaseutil" - "k8s.io/helm/pkg/rudder" - "k8s.io/helm/pkg/tiller/environment" -) - -// ReleaseModule is an interface that allows ReleaseServer to run operations on release via either local implementation or Rudder service -type ReleaseModule interface { - Create(r *release.Release, req *services.InstallReleaseRequest, env *environment.Environment) error - Update(current, target *release.Release, req *services.UpdateReleaseRequest, env *environment.Environment) error - Rollback(current, target *release.Release, req *services.RollbackReleaseRequest, env *environment.Environment) error - Status(r *release.Release, req *services.GetReleaseStatusRequest, env *environment.Environment) (string, error) - Delete(r *release.Release, req *services.UninstallReleaseRequest, env *environment.Environment) (string, []error) -} - -// LocalReleaseModule is a local implementation of ReleaseModule -type LocalReleaseModule struct { - clientset internalclientset.Interface -} - -// Create creates a release via kubeclient from provided environment -func (m *LocalReleaseModule) Create(r *release.Release, req *services.InstallReleaseRequest, env *environment.Environment) error { - b := bytes.NewBufferString(r.Manifest) - return env.KubeClient.Create(r.Namespace, b, req.Timeout, req.Wait) -} - -// Update performs an update from current to target release -func (m *LocalReleaseModule) Update(current, target *release.Release, req *services.UpdateReleaseRequest, env *environment.Environment) error { - c := bytes.NewBufferString(current.Manifest) - t := bytes.NewBufferString(target.Manifest) - return env.KubeClient.Update(target.Namespace, c, t, req.Force, req.Recreate, req.Timeout, req.Wait) -} - -// Rollback performs a rollback from current to target release -func (m *LocalReleaseModule) Rollback(current, target *release.Release, req *services.RollbackReleaseRequest, env *environment.Environment) error { - c := bytes.NewBufferString(current.Manifest) - t := bytes.NewBufferString(target.Manifest) - return env.KubeClient.Update(target.Namespace, c, t, req.Force, req.Recreate, req.Timeout, req.Wait) -} - -// Status returns kubectl-like formatted status of release objects -func (m *LocalReleaseModule) Status(r *release.Release, req *services.GetReleaseStatusRequest, env *environment.Environment) (string, error) { - return env.KubeClient.Get(r.Namespace, bytes.NewBufferString(r.Manifest)) -} - -// Delete deletes the release and returns manifests that were kept in the deletion process -func (m *LocalReleaseModule) Delete(rel *release.Release, req *services.UninstallReleaseRequest, env *environment.Environment) (kept string, errs []error) { - vs, err := GetVersionSet(m.clientset.Discovery()) - if err != nil { - return rel.Manifest, []error{fmt.Errorf("Could not get apiVersions from Kubernetes: %v", err)} - } - return DeleteRelease(rel, vs, env.KubeClient) -} - -// RemoteReleaseModule is a ReleaseModule which calls Rudder service to operate on a release -type RemoteReleaseModule struct{} - -// Create calls rudder.InstallRelease -func (m *RemoteReleaseModule) Create(r *release.Release, req *services.InstallReleaseRequest, env *environment.Environment) error { - request := &rudderAPI.InstallReleaseRequest{Release: r} - _, err := rudder.InstallRelease(request) - return err -} - -// Update calls rudder.UpgradeRelease -func (m *RemoteReleaseModule) Update(current, target *release.Release, req *services.UpdateReleaseRequest, env *environment.Environment) error { - upgrade := &rudderAPI.UpgradeReleaseRequest{ - Current: current, - Target: target, - Recreate: req.Recreate, - Timeout: req.Timeout, - Wait: req.Wait, - Force: req.Force, - } - _, err := rudder.UpgradeRelease(upgrade) - return err -} - -// Rollback calls rudder.Rollback -func (m *RemoteReleaseModule) Rollback(current, target *release.Release, req *services.RollbackReleaseRequest, env *environment.Environment) error { - rollback := &rudderAPI.RollbackReleaseRequest{ - Current: current, - Target: target, - Recreate: req.Recreate, - Timeout: req.Timeout, - Wait: req.Wait, - } - _, err := rudder.RollbackRelease(rollback) - return err -} - -// Status returns status retrieved from rudder.ReleaseStatus -func (m *RemoteReleaseModule) Status(r *release.Release, req *services.GetReleaseStatusRequest, env *environment.Environment) (string, error) { - statusRequest := &rudderAPI.ReleaseStatusRequest{Release: r} - resp, err := rudder.ReleaseStatus(statusRequest) - if resp == nil { - return "", err - } - return resp.Info.Status.Resources, err -} - -// Delete calls rudder.DeleteRelease -func (m *RemoteReleaseModule) Delete(r *release.Release, req *services.UninstallReleaseRequest, env *environment.Environment) (string, []error) { - deleteRequest := &rudderAPI.DeleteReleaseRequest{Release: r} - resp, err := rudder.DeleteRelease(deleteRequest) - - errs := make([]error, 0) - result := "" - - if err != nil { - errs = append(errs, err) - } - if resp != nil { - result = resp.Release.Manifest - } - return result, errs -} - -// DeleteRelease is a helper that allows Rudder to delete a release without exposing most of Tiller inner functions -func DeleteRelease(rel *release.Release, vs chartutil.VersionSet, kubeClient environment.KubeClient) (kept string, errs []error) { - manifests := relutil.SplitManifests(rel.Manifest) - _, files, err := sortManifests(manifests, vs, UninstallOrder) - if err != nil { - // We could instead just delete everything in no particular order. - // FIXME: One way to delete at this point would be to try a label-based - // deletion. The problem with this is that we could get a false positive - // and delete something that was not legitimately part of this release. - return rel.Manifest, []error{fmt.Errorf("corrupted release record. You must manually delete the resources: %s", err)} - } - - filesToKeep, filesToDelete := filterManifestsToKeep(files) - if len(filesToKeep) > 0 { - kept = summarizeKeptManifests(filesToKeep, kubeClient, rel.Namespace) - } - - errs = []error{} - for _, file := range filesToDelete { - b := bytes.NewBufferString(strings.TrimSpace(file.Content)) - if b.Len() == 0 { - continue - } - if err := kubeClient.Delete(rel.Namespace, b); err != nil { - log.Printf("uninstall: Failed deletion of %q: %s", rel.Name, err) - if err == kube.ErrNoObjectsVisited { - // Rewrite the message from "no objects visited" - err = errors.New("object not found, skipping delete") - } - errs = append(errs, err) - } - } - return kept, errs -} diff --git a/pkg/tiller/release_rollback.go b/pkg/tiller/release_rollback.go deleted file mode 100644 index fa3d943f4f0..00000000000 --- a/pkg/tiller/release_rollback.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - - ctx "golang.org/x/net/context" - - "k8s.io/helm/pkg/hooks" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/timeconv" -) - -// RollbackRelease rolls back to a previous version of the given release. -func (s *ReleaseServer) RollbackRelease(c ctx.Context, req *services.RollbackReleaseRequest) (*services.RollbackReleaseResponse, error) { - s.Log("preparing rollback of %s", req.Name) - currentRelease, targetRelease, err := s.prepareRollback(req) - if err != nil { - return nil, err - } - - if !req.DryRun { - s.Log("creating rolled back release for %s", req.Name) - if err := s.env.Releases.Create(targetRelease); err != nil { - return nil, err - } - } - s.Log("performing rollback of %s", req.Name) - res, err := s.performRollback(currentRelease, targetRelease, req) - if err != nil { - return res, err - } - - if !req.DryRun { - s.Log("updating status for rolled back release for %s", req.Name) - if err := s.env.Releases.Update(targetRelease); err != nil { - return res, err - } - } - - return res, nil -} - -// prepareRollback finds the previous release and prepares a new release object with -// the previous release's configuration -func (s *ReleaseServer) prepareRollback(req *services.RollbackReleaseRequest) (*release.Release, *release.Release, error) { - if err := validateReleaseName(req.Name); err != nil { - s.Log("prepareRollback: Release name is invalid: %s", req.Name) - return nil, nil, err - } - - if req.Version < 0 { - return nil, nil, errInvalidRevision - } - - currentRelease, err := s.env.Releases.Last(req.Name) - if err != nil { - return nil, nil, err - } - - previousVersion := req.Version - if req.Version == 0 { - previousVersion = currentRelease.Version - 1 - } - - s.Log("rolling back %s (current: v%d, target: v%d)", req.Name, currentRelease.Version, previousVersion) - - previousRelease, err := s.env.Releases.Get(req.Name, previousVersion) - if err != nil { - return nil, nil, err - } - - // Store a new release object with previous release's configuration - targetRelease := &release.Release{ - Name: req.Name, - Namespace: currentRelease.Namespace, - Chart: previousRelease.Chart, - Config: previousRelease.Config, - Info: &release.Info{ - FirstDeployed: currentRelease.Info.FirstDeployed, - LastDeployed: timeconv.Now(), - Status: &release.Status{ - Code: release.Status_PENDING_ROLLBACK, - Notes: previousRelease.Info.Status.Notes, - }, - // Because we lose the reference to previous version elsewhere, we set the - // message here, and only override it later if we experience failure. - Description: fmt.Sprintf("Rollback to %d", previousVersion), - }, - Version: currentRelease.Version + 1, - Manifest: previousRelease.Manifest, - Hooks: previousRelease.Hooks, - } - - return currentRelease, targetRelease, nil -} - -func (s *ReleaseServer) performRollback(currentRelease, targetRelease *release.Release, req *services.RollbackReleaseRequest) (*services.RollbackReleaseResponse, error) { - res := &services.RollbackReleaseResponse{Release: targetRelease} - - if req.DryRun { - s.Log("dry run for %s", targetRelease.Name) - return res, nil - } - - // pre-rollback hooks - if !req.DisableHooks { - if err := s.execHook(targetRelease.Hooks, targetRelease.Name, targetRelease.Namespace, hooks.PreRollback, req.Timeout); err != nil { - return res, err - } - } else { - s.Log("rollback hooks disabled for %s", req.Name) - } - - if err := s.ReleaseModule.Rollback(currentRelease, targetRelease, req, s.env); err != nil { - msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err) - s.Log("warning: %s", msg) - currentRelease.Info.Status.Code = release.Status_SUPERSEDED - targetRelease.Info.Status.Code = release.Status_FAILED - targetRelease.Info.Description = msg - s.recordRelease(currentRelease, true) - s.recordRelease(targetRelease, true) - return res, err - } - - // post-rollback hooks - if !req.DisableHooks { - if err := s.execHook(targetRelease.Hooks, targetRelease.Name, targetRelease.Namespace, hooks.PostRollback, req.Timeout); err != nil { - return res, err - } - } - - deployed, err := s.env.Releases.DeployedAll(currentRelease.Name) - if err != nil { - return nil, err - } - // Supersede all previous deployments, see issue #2941. - for _, r := range deployed { - s.Log("superseding previous deployment %d", r.Version) - r.Info.Status.Code = release.Status_SUPERSEDED - s.recordRelease(r, true) - } - - targetRelease.Info.Status.Code = release.Status_DEPLOYED - - return res, nil -} diff --git a/pkg/tiller/release_rollback_test.go b/pkg/tiller/release_rollback_test.go deleted file mode 100644 index b73501a3685..00000000000 --- a/pkg/tiller/release_rollback_test.go +++ /dev/null @@ -1,254 +0,0 @@ -/* -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 tiller - -import ( - "strings" - "testing" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" -) - -func TestRollbackRelease(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rs.env.Releases.Create(rel) - upgradedRel := upgradeReleaseVersion(rel) - upgradedRel.Hooks = []*release.Hook{ - { - Name: "test-cm", - Kind: "ConfigMap", - Path: "test-cm", - Manifest: manifestWithRollbackHooks, - Events: []release.Hook_Event{ - release.Hook_PRE_ROLLBACK, - release.Hook_POST_ROLLBACK, - }, - }, - } - - upgradedRel.Manifest = "hello world" - rs.env.Releases.Update(rel) - rs.env.Releases.Create(upgradedRel) - - req := &services.RollbackReleaseRequest{ - Name: rel.Name, - } - res, err := rs.RollbackRelease(c, req) - if err != nil { - t.Fatalf("Failed rollback: %s", err) - } - - if res.Release.Name == "" { - t.Errorf("Expected release name.") - } - - if res.Release.Name != rel.Name { - t.Errorf("Updated release name does not match previous release name. Expected %s, got %s", rel.Name, res.Release.Name) - } - - if res.Release.Namespace != rel.Namespace { - t.Errorf("Expected release namespace '%s', got '%s'.", rel.Namespace, res.Release.Namespace) - } - - if res.Release.Version != 3 { - t.Errorf("Expected release version to be %v, got %v", 3, res.Release.Version) - } - - updated, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) - } - - if len(updated.Hooks) != 2 { - t.Fatalf("Expected 2 hooks, got %d", len(updated.Hooks)) - } - - if updated.Hooks[0].Manifest != manifestWithHook { - t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest) - } - - anotherUpgradedRelease := upgradeReleaseVersion(upgradedRel) - rs.env.Releases.Update(upgradedRel) - rs.env.Releases.Create(anotherUpgradedRelease) - - res, err = rs.RollbackRelease(c, req) - if err != nil { - t.Fatalf("Failed rollback: %s", err) - } - - updated, err = rs.env.Releases.Get(res.Release.Name, res.Release.Version) - if err != nil { - t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) - } - - if len(updated.Hooks) != 1 { - t.Fatalf("Expected 1 hook, got %d", len(updated.Hooks)) - } - - if updated.Hooks[0].Manifest != manifestWithRollbackHooks { - t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest) - } - - if res.Release.Version != 4 { - t.Errorf("Expected release version to be %v, got %v", 3, res.Release.Version) - } - - if updated.Hooks[0].Events[0] != release.Hook_PRE_ROLLBACK { - t.Errorf("Expected event 0 to be pre rollback") - } - - if updated.Hooks[0].Events[1] != release.Hook_POST_ROLLBACK { - t.Errorf("Expected event 1 to be post rollback") - } - - if len(res.Release.Manifest) == 0 { - t.Errorf("No manifest returned: %v", res.Release) - } - - if len(updated.Manifest) == 0 { - t.Errorf("Expected manifest in %v", res) - } - - if !strings.Contains(updated.Manifest, "hello world") { - t.Errorf("unexpected output: %s", rel.Manifest) - } - - if res.Release.Info.Description != "Rollback to 2" { - t.Errorf("Expected rollback to 2, got %q", res.Release.Info.Description) - } -} - -func TestRollbackWithReleaseVersion(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rs.Log = t.Logf - rs.env.Releases.Log = t.Logf - rel2 := releaseStub() - rel2.Name = "other" - rs.env.Releases.Create(rel2) - rel := releaseStub() - rs.env.Releases.Create(rel) - v2 := upgradeReleaseVersion(rel) - rs.env.Releases.Update(rel) - rs.env.Releases.Create(v2) - v3 := upgradeReleaseVersion(v2) - // retain the original release as DEPLOYED while the update should fail - v2.Info.Status.Code = release.Status_DEPLOYED - v3.Info.Status.Code = release.Status_FAILED - rs.env.Releases.Update(v2) - rs.env.Releases.Create(v3) - - req := &services.RollbackReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - Version: 1, - } - - _, err := rs.RollbackRelease(c, req) - if err != nil { - t.Fatalf("Failed rollback: %s", err) - } - // check that v2 is now in a SUPERSEDED state - oldRel, err := rs.env.Releases.Get(rel.Name, 2) - if err != nil { - t.Fatalf("Failed to retrieve v1: %s", err) - } - if oldRel.Info.Status.Code != release.Status_SUPERSEDED { - t.Errorf("Expected v2 to be in a SUPERSEDED state, got %q", oldRel.Info.Status.Code) - } - // make sure we didn't update some other deployments. - otherRel, err := rs.env.Releases.Get(rel2.Name, 1) - if err != nil { - t.Fatalf("Failed to retrieve other v1: %s", err) - } - if otherRel.Info.Status.Code != release.Status_DEPLOYED { - t.Errorf("Expected other deployed release to stay untouched, got %q", otherRel.Info.Status.Code) - } -} - -func TestRollbackReleaseNoHooks(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rel.Hooks = []*release.Hook{ - { - Name: "test-cm", - Kind: "ConfigMap", - Path: "test-cm", - Manifest: manifestWithRollbackHooks, - Events: []release.Hook_Event{ - release.Hook_PRE_ROLLBACK, - release.Hook_POST_ROLLBACK, - }, - }, - } - rs.env.Releases.Create(rel) - upgradedRel := upgradeReleaseVersion(rel) - rs.env.Releases.Update(rel) - rs.env.Releases.Create(upgradedRel) - - req := &services.RollbackReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - } - - res, err := rs.RollbackRelease(c, req) - if err != nil { - t.Fatalf("Failed rollback: %s", err) - } - - if hl := res.Release.Hooks[0].LastRun; hl != nil { - t.Errorf("Expected that no hooks were run. Got %d", hl) - } -} - -func TestRollbackReleaseFailure(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rs.env.Releases.Create(rel) - upgradedRel := upgradeReleaseVersion(rel) - rs.env.Releases.Update(rel) - rs.env.Releases.Create(upgradedRel) - - req := &services.RollbackReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - } - - rs.env.KubeClient = newUpdateFailingKubeClient() - res, err := rs.RollbackRelease(c, req) - if err == nil { - t.Error("Expected failed rollback") - } - - if targetStatus := res.Release.Info.Status.Code; targetStatus != release.Status_FAILED { - t.Errorf("Expected FAILED release. Got %v", targetStatus) - } - - oldRelease, err := rs.env.Releases.Get(rel.Name, rel.Version) - if err != nil { - t.Errorf("Expected to be able to get previous release") - } - if oldStatus := oldRelease.Info.Status.Code; oldStatus != release.Status_SUPERSEDED { - t.Errorf("Expected SUPERSEDED status on previous Release version. Got %v", oldStatus) - } -} diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go deleted file mode 100644 index 7c4bc62cf7f..00000000000 --- a/pkg/tiller/release_server.go +++ /dev/null @@ -1,444 +0,0 @@ -/* -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 tiller - -import ( - "bytes" - "errors" - "fmt" - "path" - "regexp" - "strings" - - "github.com/technosophos/moniker" - "gopkg.in/yaml.v2" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/discovery" - "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" - relutil "k8s.io/helm/pkg/releaseutil" - "k8s.io/helm/pkg/tiller/environment" - "k8s.io/helm/pkg/timeconv" - "k8s.io/helm/pkg/version" -) - -// releaseNameMaxLen is the maximum length of a release name. -// -// As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for -// charts to add data. Effectively, that gives us 53 chars. -// See https://github.com/kubernetes/helm/issues/1528 -const releaseNameMaxLen = 53 - -// NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine -// but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually -// wants to see this file after rendering in the status command. However, it must be a suffix -// since there can be filepath in front of it. -const notesFileSuffix = "NOTES.txt" - -var ( - // errMissingChart indicates that a chart was not provided. - errMissingChart = errors.New("no chart provided") - // errMissingRelease indicates that a release (name) was not provided. - errMissingRelease = errors.New("no release provided") - // errInvalidRevision indicates that an invalid release revision number was provided. - errInvalidRevision = errors.New("invalid release revision") - //errInvalidName indicates that an invalid release name was provided - errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53") -) - -// ListDefaultLimit is the default limit for number of items returned in a list. -var ListDefaultLimit int64 = 512 - -// ValidName is a regular expression for names. -// -// According to the Kubernetes help text, the regular expression it uses is: -// -// (([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])? -// -// We modified that. First, we added start and end delimiters. Second, we changed -// the final ? to + to require that the pattern match at least once. This modification -// prevents an empty string from matching. -var ValidName = regexp.MustCompile("^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$") - -// ReleaseServer implements the server-side gRPC endpoint for the HAPI services. -type ReleaseServer struct { - ReleaseModule - env *environment.Environment - clientset internalclientset.Interface - Log func(string, ...interface{}) -} - -// NewReleaseServer creates a new release server. -func NewReleaseServer(env *environment.Environment, clientset internalclientset.Interface, useRemote bool) *ReleaseServer { - var releaseModule ReleaseModule - if useRemote { - releaseModule = &RemoteReleaseModule{} - } else { - releaseModule = &LocalReleaseModule{ - clientset: clientset, - } - } - - return &ReleaseServer{ - env: env, - clientset: clientset, - ReleaseModule: releaseModule, - Log: func(_ string, _ ...interface{}) {}, - } -} - -// reuseValues copies values from the current release to a new release if the -// new release does not have any values. -// -// If the request already has values, or if there are no values in the current -// release, this does nothing. -// -// This is skipped if the req.ResetValues flag is set, in which case the -// request values are not altered. -func (s *ReleaseServer) reuseValues(req *services.UpdateReleaseRequest, current *release.Release) error { - if req.ResetValues { - // If ResetValues is set, we comletely ignore current.Config. - s.Log("resetting values to the chart's original version") - return nil - } - - // If the ReuseValues flag is set, we always copy the old values over the new config's values. - if req.ReuseValues { - s.Log("reusing the old release's values") - - // We have to regenerate the old coalesced values: - oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config) - if err != nil { - err := fmt.Errorf("failed to rebuild old values: %s", err) - s.Log("%s", err) - return err - } - nv, err := oldVals.YAML() - if err != nil { - return err - } - - // merge new values with current - req.Values.Raw = current.Config.Raw + "\n" + req.Values.Raw - req.Chart.Values = &chart.Config{Raw: nv} - - // yaml unmarshal and marshal to remove duplicate keys - y := map[string]interface{}{} - if err := yaml.Unmarshal([]byte(req.Values.Raw), &y); err != nil { - return err - } - data, err := yaml.Marshal(y) - if err != nil { - return err - } - - req.Values.Raw = string(data) - return nil - } - - // If req.Values is empty, but current.Config is not, copy current into the - // request. - if (req.Values == nil || req.Values.Raw == "" || req.Values.Raw == "{}\n") && - current.Config != nil && - current.Config.Raw != "" && - current.Config.Raw != "{}\n" { - s.Log("copying values from %s (v%d) to new release.", current.Name, current.Version) - req.Values = current.Config - } - return nil -} - -func (s *ReleaseServer) uniqName(start string, reuse bool) (string, error) { - - // If a name is supplied, we check to see if that name is taken. If not, it - // is granted. If reuse is true and a deleted release with that name exists, - // we re-grant it. Otherwise, an error is returned. - if start != "" { - - if len(start) > releaseNameMaxLen { - return "", fmt.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen) - } - - h, err := s.env.Releases.History(start) - if err != nil || len(h) < 1 { - return start, nil - } - relutil.Reverse(h, relutil.SortByRevision) - rel := h[0] - - if st := rel.Info.Status.Code; reuse && (st == release.Status_DELETED || st == release.Status_FAILED) { - // Allowe re-use of names if the previous release is marked deleted. - s.Log("name %s exists but is not in use, reusing name", start) - return start, nil - } else if reuse { - return "", errors.New("cannot re-use a name that is still in use") - } - - return "", fmt.Errorf("a release named %s already exists.\nRun: helm ls --all %s; to check the status of the release\nOr run: helm del --purge %s; to delete it", start, start, start) - } - - maxTries := 5 - for i := 0; i < maxTries; i++ { - namer := moniker.New() - name := namer.NameSep("-") - if len(name) > releaseNameMaxLen { - name = name[:releaseNameMaxLen] - } - if _, err := s.env.Releases.Get(name, 1); strings.Contains(err.Error(), "not found") { - return name, nil - } - s.Log("info: generated name %s is taken. Searching again.", name) - } - s.Log("warning: No available release names found after %d tries", maxTries) - return "ERROR", errors.New("no available release name found") -} - -func (s *ReleaseServer) engine(ch *chart.Chart) environment.Engine { - renderer := s.env.EngineYard.Default() - if ch.Metadata.Engine != "" { - if r, ok := s.env.EngineYard.Get(ch.Metadata.Engine); ok { - renderer = r - } else { - s.Log("warning: %s requested non-existent template engine %s", ch.Metadata.Name, ch.Metadata.Engine) - } - } - return renderer -} - -// capabilities builds a Capabilities from discovery information. -func capabilities(disc discovery.DiscoveryInterface) (*chartutil.Capabilities, error) { - sv, err := disc.ServerVersion() - if err != nil { - return nil, err - } - vs, err := GetVersionSet(disc) - if err != nil { - return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) - } - return &chartutil.Capabilities{ - APIVersions: vs, - KubeVersion: sv, - TillerVersion: version.GetVersionProto(), - }, nil -} - -// GetVersionSet retrieves a set of available k8s API versions -func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) { - groups, err := client.ServerGroups() - if err != nil { - return chartutil.DefaultVersionSet, err - } - - // FIXME: The Kubernetes test fixture for cli appears to always return nil - // for calls to Discovery().ServerGroups(). So in this case, we return - // the default API list. This is also a safe value to return in any other - // odd-ball case. - if groups.Size() == 0 { - return chartutil.DefaultVersionSet, nil - } - - versions := metav1.ExtractGroupVersions(groups) - return chartutil.NewVersionSet(versions...), nil -} - -func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) { - // Guard to make sure Tiller is at the right version to handle this chart. - sver := version.GetVersion() - if ch.Metadata.TillerVersion != "" && - !version.IsCompatibleRange(ch.Metadata.TillerVersion, sver) { - return nil, nil, "", fmt.Errorf("Chart incompatible with Tiller %s", sver) - } - - if ch.Metadata.KubeVersion != "" { - cap, _ := values["Capabilities"].(*chartutil.Capabilities) - gitVersion := cap.KubeVersion.String() - k8sVersion := strings.Split(gitVersion, "+")[0] - if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) { - return nil, nil, "", fmt.Errorf("Chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion) - } - } - - s.Log("rendering %s chart using values", ch.GetMetadata().Name) - renderer := s.engine(ch) - files, err := renderer.Render(ch, values) - if err != nil { - return nil, nil, "", err - } - - // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, - // pull it out of here into a separate file so that we can actually use the output of the rendered - // text file. We have to spin through this map because the file contains path information, so we - // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip - // it in the sortHooks. - notes := "" - for k, v := range files { - if strings.HasSuffix(k, notesFileSuffix) { - // Only apply the notes if it belongs to the parent chart - // Note: Do not use filePath.Join since it creates a path with \ which is not expected - if k == path.Join(ch.Metadata.Name, "templates", notesFileSuffix) { - notes = v - } - delete(files, k) - } - } - - // Sort hooks, manifests, and partials. Only hooks and manifests are returned, - // as partials are not used after renderer.Render. Empty manifests are also - // removed here. - hooks, manifests, err := sortManifests(files, vs, InstallOrder) - if err != nil { - // By catching parse errors here, we can prevent bogus releases from going - // to Kubernetes. - // - // We return the files as a big blob of data to help the user debug parser - // errors. - b := bytes.NewBuffer(nil) - for name, content := range files { - if len(strings.TrimSpace(content)) == 0 { - continue - } - b.WriteString("\n---\n# Source: " + name + "\n") - b.WriteString(content) - } - return nil, b, "", err - } - - // 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) - } - - return hooks, b, notes, nil -} - -// recordRelease with an update operation in case reuse has been set. -func (s *ReleaseServer) recordRelease(r *release.Release, reuse bool) { - if reuse { - if err := s.env.Releases.Update(r); err != nil { - s.Log("warning: Failed to update release %s: %s", r.Name, err) - } - } else if err := s.env.Releases.Create(r); err != nil { - s.Log("warning: Failed to record release %s: %s", r.Name, err) - } -} - -func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error { - kubeCli := s.env.KubeClient - code, ok := events[hook] - if !ok { - return fmt.Errorf("unknown hook %s", hook) - } - - s.Log("executing %d %s hooks for %s", len(hs), hook, name) - executingHooks := []*release.Hook{} - for _, h := range hs { - for _, e := range h.Events { - if e == code { - executingHooks = append(executingHooks, h) - } - } - } - - executingHooks = sortByHookWeight(executingHooks) - - for _, h := range executingHooks { - if err := s.deleteHookIfShouldBeDeletedByDeletePolicy(h, hooks.BeforeHookCreation, name, namespace, hook, kubeCli); err != nil { - return err - } - - b := bytes.NewBufferString(h.Manifest) - if err := kubeCli.Create(namespace, b, timeout, false); err != nil { - s.Log("warning: Release %s %s %s failed: %s", name, hook, h.Path, err) - return err - } - // No way to rewind a bytes.Buffer()? - b.Reset() - 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 err := s.deleteHookIfShouldBeDeletedByDeletePolicy(h, hooks.HookFailed, name, namespace, hook, kubeCli); err != nil { - return err - } - return err - } - } - - 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 { - if err := s.deleteHookIfShouldBeDeletedByDeletePolicy(h, hooks.HookSucceeded, name, namespace, hook, kubeCli); err != nil { - return err - } - h.LastRun = timeconv.Now() - } - - return nil -} - -func validateManifest(c environment.KubeClient, ns string, manifest []byte) error { - r := bytes.NewReader(manifest) - _, err := c.BuildUnstructured(ns, r) - return err -} - -func validateReleaseName(releaseName string) error { - if releaseName == "" { - return errMissingRelease - } - - if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) { - return errInvalidName - } - - return nil -} - -func (s *ReleaseServer) deleteHookIfShouldBeDeletedByDeletePolicy(h *release.Hook, policy string, name, namespace, hook string, kubeCli environment.KubeClient) error { - b := bytes.NewBufferString(h.Manifest) - if hookHasDeletePolicy(h, policy) { - s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, policy) - 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 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 hookHasDeletePolicy(h *release.Hook, policy string) bool { - if dp, ok := deletePolices[policy]; ok { - for _, v := range h.DeletePolicies { - if dp == v { - return true - } - } - } - return false -} diff --git a/pkg/tiller/release_server_test.go b/pkg/tiller/release_server_test.go deleted file mode 100644 index 73ba08f67ba..00000000000 --- a/pkg/tiller/release_server_test.go +++ /dev/null @@ -1,916 +0,0 @@ -/* -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 tiller - -import ( - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "regexp" - "testing" - "time" - - "github.com/ghodss/yaml" - "github.com/golang/protobuf/ptypes/timestamp" - "golang.org/x/net/context" - "google.golang.org/grpc/metadata" - "k8s.io/kubernetes/pkg/apis/core" - "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" - "k8s.io/kubernetes/pkg/kubectl/resource" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/hooks" - "k8s.io/helm/pkg/kube" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/storage" - "k8s.io/helm/pkg/storage/driver" - "k8s.io/helm/pkg/tiller/environment" -) - -const notesText = "my notes here" - -var manifestWithHook = `kind: ConfigMap -metadata: - name: test-cm - annotations: - "helm.sh/hook": post-install,pre-delete -data: - name: value` - -var manifestWithTestHook = `kind: Pod -metadata: - name: finding-nemo, - annotations: - "helm.sh/hook": test-success -spec: - containers: - - name: nemo-test - image: fake-image - cmd: fake-command -` - -var manifestWithKeep = `kind: ConfigMap -metadata: - name: test-cm-keep - annotations: - "helm.sh/resource-policy": keep -data: - name: value -` - -var manifestWithUpgradeHooks = `kind: ConfigMap -metadata: - name: test-cm - annotations: - "helm.sh/hook": post-upgrade,pre-upgrade -data: - name: value` - -var manifestWithRollbackHooks = `kind: ConfigMap -metadata: - name: test-cm - annotations: - "helm.sh/hook": post-rollback,pre-rollback -data: - name: value -` - -func rsFixture() *ReleaseServer { - clientset := fake.NewSimpleClientset() - return &ReleaseServer{ - ReleaseModule: &LocalReleaseModule{ - clientset: clientset, - }, - env: MockEnvironment(), - clientset: clientset, - Log: func(_ string, _ ...interface{}) {}, - } -} - -type chartOptions struct { - *chart.Chart -} - -type chartOption func(*chartOptions) - -func buildChart(opts ...chartOption) *chart.Chart { - c := &chartOptions{ - Chart: &chart.Chart{ - // TODO: This should be more complete. - Metadata: &chart.Metadata{ - Name: "hello", - }, - // This adds a basic template and hooks. - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithHook)}, - }, - }, - } - - for _, opt := range opts { - opt(c) - } - - return c.Chart -} - -func withKube(version string) chartOption { - return func(opts *chartOptions) { - opts.Metadata.KubeVersion = version - } -} - -func withTiller(version string) chartOption { - return func(opts *chartOptions) { - opts.Metadata.TillerVersion = version - } -} - -func withDependency(dependencyOpts ...chartOption) chartOption { - return func(opts *chartOptions) { - opts.Dependencies = append(opts.Dependencies, buildChart(dependencyOpts...)) - } -} - -func withNotes(notes string) chartOption { - return func(opts *chartOptions) { - opts.Templates = append(opts.Templates, &chart.Template{ - Name: "templates/NOTES.txt", - Data: []byte(notes), - }) - } -} - -func withSampleTemplates() chartOption { - return func(opts *chartOptions) { - sampleTemplates := []*chart.Template{ - // This adds basic templates and partials. - {Name: "templates/goodbye", Data: []byte("goodbye: world")}, - {Name: "templates/empty", Data: []byte("")}, - {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, - {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, - } - opts.Templates = append(opts.Templates, sampleTemplates...) - } -} - -type installOptions struct { - *services.InstallReleaseRequest -} - -type installOption func(*installOptions) - -func withName(name string) installOption { - return func(opts *installOptions) { - opts.Name = name - } -} - -func withDryRun() installOption { - return func(opts *installOptions) { - opts.DryRun = true - } -} - -func withDisabledHooks() installOption { - return func(opts *installOptions) { - opts.DisableHooks = true - } -} - -func withReuseName() installOption { - return func(opts *installOptions) { - opts.ReuseName = true - } -} - -func withChart(chartOpts ...chartOption) installOption { - return func(opts *installOptions) { - opts.Chart = buildChart(chartOpts...) - } -} - -func installRequest(opts ...installOption) *services.InstallReleaseRequest { - reqOpts := &installOptions{ - &services.InstallReleaseRequest{ - Namespace: "spaced", - Chart: buildChart(), - }, - } - - for _, opt := range opts { - opt(reqOpts) - } - - return reqOpts.InstallReleaseRequest -} - -// chartStub creates a fully stubbed out chart. -func chartStub() *chart.Chart { - return buildChart(withSampleTemplates()) -} - -// releaseStub creates a release stub, complete with the chartStub as its chart. -func releaseStub() *release.Release { - return namedReleaseStub("angry-panda", release.Status_DEPLOYED) -} - -func namedReleaseStub(name string, status release.Status_Code) *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: status}, - Description: "Named Release Stub", - }, - Chart: chartStub(), - Config: &chart.Config{Raw: `name: value`}, - Version: 1, - 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, - }, - }, - { - Name: "finding-nemo", - Kind: "Pod", - Path: "finding-nemo", - Manifest: manifestWithTestHook, - Events: []release.Hook_Event{ - release.Hook_RELEASE_TEST_SUCCESS, - }, - }, - }, - } -} - -func upgradeReleaseVersion(rel *release.Release) *release.Release { - date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0} - - rel.Info.Status.Code = release.Status_SUPERSEDED - return &release.Release{ - Name: rel.Name, - Info: &release.Info{ - FirstDeployed: rel.Info.FirstDeployed, - LastDeployed: &date, - Status: &release.Status{Code: release.Status_DEPLOYED}, - }, - Chart: rel.Chart, - Config: rel.Config, - Version: rel.Version + 1, - } -} - -func TestValidName(t *testing.T) { - for name, valid := range map[string]error{ - "nina pinta santa-maria": errInvalidName, - "nina-pinta-santa-maria": nil, - "-nina": errInvalidName, - "pinta-": errInvalidName, - "santa-maria": nil, - "niÃąa": errInvalidName, - "...": errInvalidName, - "pinta...": errInvalidName, - "santa...maria": nil, - "": errMissingRelease, - " ": errInvalidName, - ".nina.": errInvalidName, - "nina.pinta": nil, - "abcdefghi-abcdefghi-abcdefghi-abcdefghi-abcdefghi-abcd": errInvalidName, - } { - if valid != validateReleaseName(name) { - t.Errorf("Expected %q to be %t", name, valid) - } - } -} - -func TestGetVersionSet(t *testing.T) { - rs := rsFixture() - vs, err := GetVersionSet(rs.clientset.Discovery()) - if err != nil { - t.Error(err) - } - if !vs.Has("v1") { - t.Errorf("Expected supported versions to at least include v1.") - } - if vs.Has("nosuchversion/v1") { - t.Error("Non-existent version is reported found.") - } -} - -func TestUniqName(t *testing.T) { - rs := rsFixture() - - rel1 := releaseStub() - rel2 := releaseStub() - rel2.Name = "happy-panda" - rel2.Info.Status.Code = release.Status_DELETED - - rs.env.Releases.Create(rel1) - rs.env.Releases.Create(rel2) - - tests := []struct { - name string - expect string - reuse bool - err bool - }{ - {"first", "first", false, false}, - {"", "[a-z]+-[a-z]+", false, false}, - {"angry-panda", "", false, true}, - {"happy-panda", "", false, true}, - {"happy-panda", "happy-panda", true, false}, - {"hungry-hungry-hungry-hungry-hungry-hungry-hungry-hungry-hippos", "", true, true}, // Exceeds max name length - } - - for _, tt := range tests { - u, err := rs.uniqName(tt.name, tt.reuse) - if err != nil { - if tt.err { - continue - } - t.Fatal(err) - } - if tt.err { - t.Errorf("Expected an error for %q", tt.name) - } - if match, err := regexp.MatchString(tt.expect, u); err != nil { - t.Fatal(err) - } else if !match { - t.Errorf("Expected %q to match %q", u, tt.expect) - } - } -} - -func releaseWithKeepStub(rlsName string) *release.Release { - ch := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "bunnychart", - }, - Templates: []*chart.Template{ - {Name: "templates/configmap", Data: []byte(manifestWithKeep)}, - }, - } - - date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0} - return &release.Release{ - Name: rlsName, - Info: &release.Info{ - FirstDeployed: &date, - LastDeployed: &date, - Status: &release.Status{Code: release.Status_DEPLOYED}, - }, - Chart: ch, - Config: &chart.Config{Raw: `name: value`}, - Version: 1, - Manifest: manifestWithKeep, - } -} - -func MockEnvironment() *environment.Environment { - e := environment.New() - e.Releases = storage.Init(driver.NewMemory()) - e.KubeClient = &environment.PrintingKubeClient{Out: ioutil.Discard} - return e -} - -func newUpdateFailingKubeClient() *updateFailingKubeClient { - return &updateFailingKubeClient{ - PrintingKubeClient: environment.PrintingKubeClient{Out: os.Stdout}, - } - -} - -type updateFailingKubeClient struct { - environment.PrintingKubeClient -} - -func (u *updateFailingKubeClient) Update(namespace string, originalReader, modifiedReader io.Reader, force bool, recreate bool, timeout int64, shouldWait bool) error { - return errors.New("Failed update in kube client") -} - -func newHookFailingKubeClient() *hookFailingKubeClient { - return &hookFailingKubeClient{ - PrintingKubeClient: environment.PrintingKubeClient{Out: ioutil.Discard}, - } -} - -type hookFailingKubeClient struct { - environment.PrintingKubeClient -} - -func (h *hookFailingKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error { - return errors.New("Failed watch") -} - -type mockListServer struct { - val *services.ListReleasesResponse -} - -func (l *mockListServer) Send(res *services.ListReleasesResponse) error { - l.val = res - return nil -} - -func (l *mockListServer) Context() context.Context { return helm.NewContext() } -func (l *mockListServer) SendMsg(v interface{}) error { return nil } -func (l *mockListServer) RecvMsg(v interface{}) error { return nil } -func (l *mockListServer) SendHeader(m metadata.MD) error { return nil } -func (l *mockListServer) SetTrailer(m metadata.MD) {} -func (l *mockListServer) SetHeader(m metadata.MD) error { return nil } - -type mockRunReleaseTestServer struct{} - -func (rs mockRunReleaseTestServer) Send(m *services.TestReleaseResponse) error { - return nil -} -func (rs mockRunReleaseTestServer) SetHeader(m metadata.MD) error { return nil } -func (rs mockRunReleaseTestServer) SendHeader(m metadata.MD) error { return nil } -func (rs mockRunReleaseTestServer) SetTrailer(m metadata.MD) {} -func (rs mockRunReleaseTestServer) SendMsg(v interface{}) error { return nil } -func (rs mockRunReleaseTestServer) RecvMsg(v interface{}) error { return nil } -func (rs mockRunReleaseTestServer) Context() context.Context { return helm.NewContext() } - -type mockHooksManifest struct { - Metadata struct { - Name string - Annotations map[string]string - } -} -type mockHooksKubeClient struct { - Resources map[string]*mockHooksManifest -} - -var errResourceExists = errors.New("resource already exists") - -func (kc *mockHooksKubeClient) makeManifest(r io.Reader) (*mockHooksManifest, error) { - b, err := ioutil.ReadAll(r) - if err != nil { - return nil, err - } - - manifest := &mockHooksManifest{} - err = yaml.Unmarshal(b, manifest) - if err != nil { - return nil, err - } - - return manifest, nil -} -func (kc *mockHooksKubeClient) Create(ns string, r io.Reader, timeout int64, shouldWait bool) error { - manifest, err := kc.makeManifest(r) - if err != nil { - return err - } - - if _, hasKey := kc.Resources[manifest.Metadata.Name]; hasKey { - return errResourceExists - } - - kc.Resources[manifest.Metadata.Name] = manifest - - return nil -} -func (kc *mockHooksKubeClient) Get(ns string, r io.Reader) (string, error) { - return "", nil -} -func (kc *mockHooksKubeClient) Delete(ns string, r io.Reader) error { - manifest, err := kc.makeManifest(r) - if err != nil { - return err - } - - delete(kc.Resources, manifest.Metadata.Name) - - return nil -} -func (kc *mockHooksKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error { - paramManifest, err := kc.makeManifest(r) - if err != nil { - return err - } - - manifest, hasManifest := kc.Resources[paramManifest.Metadata.Name] - if !hasManifest { - return fmt.Errorf("mockHooksKubeClient.WatchUntilReady: no such resource %s found", paramManifest.Metadata.Name) - } - - if manifest.Metadata.Annotations["mockHooksKubeClient/Emulate"] == "hook-failed" { - return fmt.Errorf("mockHooksKubeClient.WatchUntilReady: hook-failed") - } - - return nil -} -func (kc *mockHooksKubeClient) Update(ns string, currentReader, modifiedReader io.Reader, force bool, recreate bool, timeout int64, shouldWait bool) error { - return nil -} -func (kc *mockHooksKubeClient) Build(ns string, reader io.Reader) (kube.Result, error) { - return []*resource.Info{}, nil -} -func (kc *mockHooksKubeClient) BuildUnstructured(ns string, reader io.Reader) (kube.Result, error) { - return []*resource.Info{}, nil -} -func (kc *mockHooksKubeClient) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (core.PodPhase, error) { - return core.PodUnknown, nil -} - -func deletePolicyStub(kubeClient *mockHooksKubeClient) *ReleaseServer { - e := environment.New() - e.Releases = storage.Init(driver.NewMemory()) - e.KubeClient = kubeClient - - clientset := fake.NewSimpleClientset() - return &ReleaseServer{ - ReleaseModule: &LocalReleaseModule{ - clientset: clientset, - }, - env: e, - clientset: clientset, - Log: func(_ string, _ ...interface{}) {}, - } -} - -func deletePolicyHookStub(hookName string, extraAnnotations map[string]string, DeletePolicies []release.Hook_DeletePolicy) *release.Hook { - extraAnnotationsStr := "" - for k, v := range extraAnnotations { - extraAnnotationsStr += fmt.Sprintf(" \"%s\": \"%s\"\n", k, v) - } - - return &release.Hook{ - Name: hookName, - Kind: "Job", - Path: hookName, - Manifest: fmt.Sprintf(`kind: Job -metadata: - name: %s - annotations: - "helm.sh/hook": pre-install,pre-upgrade -%sdata: -name: value`, hookName, extraAnnotationsStr), - Events: []release.Hook_Event{ - release.Hook_PRE_INSTALL, - release.Hook_PRE_UPGRADE, - }, - DeletePolicies: DeletePolicies, - } -} - -func execHookShouldSucceed(rs *ReleaseServer, hook *release.Hook, releaseName string, namespace string, hookType string) error { - err := rs.execHook([]*release.Hook{hook}, releaseName, namespace, hookType, 600) - if err != nil { - return fmt.Errorf("expected hook %s to be successful: %s", hook.Name, err) - } - return nil -} - -func execHookShouldFail(rs *ReleaseServer, hook *release.Hook, releaseName string, namespace string, hookType string) error { - err := rs.execHook([]*release.Hook{hook}, releaseName, namespace, hookType, 600) - if err == nil { - return fmt.Errorf("expected hook %s to be failed", hook.Name) - } - return nil -} - -func execHookShouldFailWithError(rs *ReleaseServer, hook *release.Hook, releaseName string, namespace string, hookType string, expectedError error) error { - err := rs.execHook([]*release.Hook{hook}, releaseName, namespace, hookType, 600) - if err != expectedError { - return fmt.Errorf("expected hook %s to fail with error %v, got %v", hook.Name, expectedError, err) - } - return nil -} - -type deletePolicyContext struct { - ReleaseServer *ReleaseServer - ReleaseName string - Namespace string - HookName string - KubeClient *mockHooksKubeClient -} - -func newDeletePolicyContext() *deletePolicyContext { - kubeClient := &mockHooksKubeClient{ - Resources: make(map[string]*mockHooksManifest), - } - - return &deletePolicyContext{ - KubeClient: kubeClient, - ReleaseServer: deletePolicyStub(kubeClient), - ReleaseName: "flying-carp", - Namespace: "river", - HookName: "migration-job", - } -} - -func TestSuccessfulHookWithoutDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - hook := deletePolicyHookStub(ctx.HookName, nil, nil) - - err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be created by kube client", hook.Name) - } -} - -func TestFailedHookWithoutDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{"mockHooksKubeClient/Emulate": "hook-failed"}, - nil, - ) - - err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be created by kube client", hook.Name) - } -} - -func TestSuccessfulHookWithSucceededDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{"helm.sh/hook-delete-policy": "hook-succeeded"}, - []release.Hook_DeletePolicy{release.Hook_SUCCEEDED}, - ) - - err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook succeeded", hook.Name) - } -} - -func TestSuccessfulHookWithFailedDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{"helm.sh/hook-delete-policy": "hook-failed"}, - []release.Hook_DeletePolicy{release.Hook_FAILED}, - ) - - err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook succeeded", hook.Name) - } -} - -func TestFailedHookWithSucceededDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "mockHooksKubeClient/Emulate": "hook-failed", - "helm.sh/hook-delete-policy": "hook-succeeded", - }, - []release.Hook_DeletePolicy{release.Hook_SUCCEEDED}, - ) - - err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook failed", hook.Name) - } -} - -func TestFailedHookWithFailedDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "mockHooksKubeClient/Emulate": "hook-failed", - "helm.sh/hook-delete-policy": "hook-failed", - }, - []release.Hook_DeletePolicy{release.Hook_FAILED}, - ) - - err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook failed", hook.Name) - } -} - -func TestSuccessfulHookWithSuccededOrFailedDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "helm.sh/hook-delete-policy": "hook-succeeded,hook-failed", - }, - []release.Hook_DeletePolicy{release.Hook_SUCCEEDED, release.Hook_FAILED}, - ) - - err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook succeeded", hook.Name) - } -} - -func TestFailedHookWithSuccededOrFailedDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "mockHooksKubeClient/Emulate": "hook-failed", - "helm.sh/hook-delete-policy": "hook-succeeded,hook-failed", - }, - []release.Hook_DeletePolicy{release.Hook_SUCCEEDED, release.Hook_FAILED}, - ) - - err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook failed", hook.Name) - } -} - -func TestHookAlreadyExists(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, nil, nil) - - err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook succeeded", hook.Name) - } - - err = execHookShouldFailWithError(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreUpgrade, errResourceExists) - if err != nil { - t.Error(err) - } - - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after already exists error", hook.Name) - } -} - -func TestHookDeletingWithBeforeHookCreationDeletePolicy(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{"helm.sh/hook-delete-policy": "before-hook-creation"}, - []release.Hook_DeletePolicy{release.Hook_BEFORE_HOOK_CREATION}, - ) - - err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook succeeded", hook.Name) - } - - err = execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreUpgrade) - if err != nil { - t.Error(err) - } - - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook succeeded", hook.Name) - } -} - -func TestSuccessfulHookWithMixedDeletePolicies(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "helm.sh/hook-delete-policy": "hook-succeeded,before-hook-creation", - }, - []release.Hook_DeletePolicy{release.Hook_SUCCEEDED, release.Hook_BEFORE_HOOK_CREATION}, - ) - - err := execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook succeeded", hook.Name) - } - - err = execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreUpgrade) - if err != nil { - t.Error(err) - } - - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook succeeded", hook.Name) - } -} - -func TestFailedHookWithMixedDeletePolicies(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "mockHooksKubeClient/Emulate": "hook-failed", - "helm.sh/hook-delete-policy": "hook-succeeded,before-hook-creation", - }, - []release.Hook_DeletePolicy{release.Hook_SUCCEEDED, release.Hook_BEFORE_HOOK_CREATION}, - ) - - err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook failed", hook.Name) - } - - err = execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreUpgrade) - if err != nil { - t.Error(err) - } - - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook failed", hook.Name) - } -} - -func TestFailedThenSuccessfulHookWithMixedDeletePolicies(t *testing.T) { - ctx := newDeletePolicyContext() - - hook := deletePolicyHookStub(ctx.HookName, - map[string]string{ - "mockHooksKubeClient/Emulate": "hook-failed", - "helm.sh/hook-delete-policy": "hook-succeeded,before-hook-creation", - }, - []release.Hook_DeletePolicy{release.Hook_SUCCEEDED, release.Hook_BEFORE_HOOK_CREATION}, - ) - - err := execHookShouldFail(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreInstall) - if err != nil { - t.Error(err) - } - - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; !hasResource { - t.Errorf("expected resource %s to be existing after hook failed", hook.Name) - } - - hook = deletePolicyHookStub(ctx.HookName, - map[string]string{ - "helm.sh/hook-delete-policy": "hook-succeeded,before-hook-creation", - }, - []release.Hook_DeletePolicy{release.Hook_SUCCEEDED, release.Hook_BEFORE_HOOK_CREATION}, - ) - - err = execHookShouldSucceed(ctx.ReleaseServer, hook, ctx.ReleaseName, ctx.Namespace, hooks.PreUpgrade) - if err != nil { - t.Error(err) - } - - if _, hasResource := ctx.KubeClient.Resources[hook.Name]; hasResource { - t.Errorf("expected resource %s to be unexisting after hook succeeded", hook.Name) - } -} diff --git a/pkg/tiller/release_status.go b/pkg/tiller/release_status.go deleted file mode 100644 index e0d75877d9c..00000000000 --- a/pkg/tiller/release_status.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -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 tiller - -import ( - "errors" - "fmt" - - ctx "golang.org/x/net/context" - - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" -) - -// GetReleaseStatus gets the status information for a named release. -func (s *ReleaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) { - if err := validateReleaseName(req.Name); err != nil { - s.Log("getStatus: Release name is invalid: %s", req.Name) - return nil, err - } - - var rel *release.Release - - if req.Version <= 0 { - var err error - rel, err = s.env.Releases.Last(req.Name) - if err != nil { - return nil, fmt.Errorf("getting deployed release %q: %s", req.Name, err) - } - } else { - var err error - if rel, err = s.env.Releases.Get(req.Name, req.Version); err != nil { - return nil, fmt.Errorf("getting release '%s' (v%d): %s", req.Name, req.Version, err) - } - } - - if rel.Info == nil { - return nil, errors.New("release info is missing") - } - if rel.Chart == nil { - return nil, errors.New("release chart is missing") - } - - sc := rel.Info.Status.Code - statusResp := &services.GetReleaseStatusResponse{ - Name: rel.Name, - Namespace: rel.Namespace, - Info: rel.Info, - } - - // Ok, we got the status of the release as we had jotted down, now we need to match the - // manifest we stashed away with reality from the cluster. - resp, err := s.ReleaseModule.Status(rel, req, s.env) - if sc == release.Status_DELETED || sc == release.Status_FAILED { - // Skip errors if this is already deleted or failed. - return statusResp, nil - } else if err != nil { - s.Log("warning: Get for %s failed: %v", rel.Name, err) - return nil, err - } - rel.Info.Status.Resources = resp - return statusResp, nil -} diff --git a/pkg/tiller/release_status_test.go b/pkg/tiller/release_status_test.go deleted file mode 100644 index 4ba0f6cd50f..00000000000 --- a/pkg/tiller/release_status_test.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -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 tiller - -import ( - "testing" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" -) - -func TestGetReleaseStatus(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - if err := rs.env.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - - res, err := rs.GetReleaseStatus(c, &services.GetReleaseStatusRequest{Name: rel.Name, Version: 1}) - if err != nil { - t.Errorf("Error getting release content: %s", err) - } - - if res.Name != rel.Name { - t.Errorf("Expected name %q, got %q", rel.Name, res.Name) - } - if res.Info.Status.Code != release.Status_DEPLOYED { - t.Errorf("Expected %d, got %d", release.Status_DEPLOYED, res.Info.Status.Code) - } -} - -func TestGetReleaseStatusDeleted(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rel.Info.Status.Code = release.Status_DELETED - if err := rs.env.Releases.Create(rel); err != nil { - t.Fatalf("Could not store mock release: %s", err) - } - - res, err := rs.GetReleaseStatus(c, &services.GetReleaseStatusRequest{Name: rel.Name, Version: 1}) - if err != nil { - t.Fatalf("Error getting release content: %s", err) - } - - if res.Info.Status.Code != release.Status_DELETED { - t.Errorf("Expected %d, got %d", release.Status_DELETED, res.Info.Status.Code) - } -} diff --git a/pkg/tiller/release_testing.go b/pkg/tiller/release_testing.go deleted file mode 100644 index a44b67e6fbc..00000000000 --- a/pkg/tiller/release_testing.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -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 tiller - -import ( - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - reltesting "k8s.io/helm/pkg/releasetesting" -) - -// RunReleaseTest runs pre-defined tests stored as hooks on a given release -func (s *ReleaseServer) RunReleaseTest(req *services.TestReleaseRequest, stream services.ReleaseService_RunReleaseTestServer) error { - - if err := validateReleaseName(req.Name); err != nil { - s.Log("releaseTest: Release name is invalid: %s", req.Name) - return err - } - - // finds the non-deleted release with the given name - rel, err := s.env.Releases.Last(req.Name) - if err != nil { - return err - } - - testEnv := &reltesting.Environment{ - Namespace: rel.Namespace, - KubeClient: s.env.KubeClient, - Timeout: req.Timeout, - Stream: stream, - } - s.Log("running tests for release %s", rel.Name) - tSuite, err := reltesting.NewTestSuite(rel) - if err != nil { - s.Log("error creating test suite for %s: %s", rel.Name, err) - return err - } - - if err := tSuite.Run(testEnv); err != nil { - s.Log("error running test suite for %s: %s", rel.Name, err) - return err - } - - rel.Info.Status.LastTestSuiteRun = &release.TestSuite{ - StartedAt: tSuite.StartedAt, - CompletedAt: tSuite.CompletedAt, - Results: tSuite.Results, - } - - if req.Cleanup { - testEnv.DeleteTestPods(tSuite.TestManifests) - } - - if err := s.env.Releases.Update(rel); err != nil { - s.Log("test: Failed to store updated release: %s", err) - } - - return nil -} diff --git a/pkg/tiller/release_testing_test.go b/pkg/tiller/release_testing_test.go deleted file mode 100644 index f8d92ebcc48..00000000000 --- a/pkg/tiller/release_testing_test.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -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 tiller - -import ( - "testing" - - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" -) - -func TestRunReleaseTest(t *testing.T) { - rs := rsFixture() - rel := namedReleaseStub("nemo", release.Status_DEPLOYED) - rs.env.Releases.Create(rel) - - req := &services.TestReleaseRequest{Name: "nemo", Timeout: 2} - err := rs.RunReleaseTest(req, mockRunReleaseTestServer{}) - if err != nil { - t.Fatalf("failed to run release tests on %s: %s", rel.Name, err) - } -} diff --git a/pkg/tiller/release_uninstall.go b/pkg/tiller/release_uninstall.go deleted file mode 100644 index 423b6e7ef7f..00000000000 --- a/pkg/tiller/release_uninstall.go +++ /dev/null @@ -1,128 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - "strings" - - ctx "golang.org/x/net/context" - - "k8s.io/helm/pkg/hooks" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - relutil "k8s.io/helm/pkg/releaseutil" - "k8s.io/helm/pkg/timeconv" -) - -// UninstallRelease deletes all of the resources associated with this release, and marks the release DELETED. -func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { - if err := validateReleaseName(req.Name); err != nil { - s.Log("uninstallRelease: Release name is invalid: %s", req.Name) - return nil, err - } - - rels, err := s.env.Releases.History(req.Name) - if err != nil { - s.Log("uninstall: Release not loaded: %s", req.Name) - return nil, err - } - if len(rels) < 1 { - return nil, errMissingRelease - } - - relutil.SortByRevision(rels) - rel := rels[len(rels)-1] - - // TODO: Are there any cases where we want to force a delete even if it's - // already marked deleted? - if rel.Info.Status.Code == release.Status_DELETED { - if req.Purge { - if err := s.purgeReleases(rels...); err != nil { - s.Log("uninstall: Failed to purge the release: %s", err) - return nil, err - } - return &services.UninstallReleaseResponse{Release: rel}, nil - } - return nil, fmt.Errorf("the release named %q is already deleted", req.Name) - } - - s.Log("uninstall: Deleting %s", req.Name) - rel.Info.Status.Code = release.Status_DELETING - rel.Info.Deleted = timeconv.Now() - rel.Info.Description = "Deletion in progress (or silently failed)" - res := &services.UninstallReleaseResponse{Release: rel} - - if !req.DisableHooks { - if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, hooks.PreDelete, req.Timeout); err != nil { - return res, err - } - } else { - s.Log("delete hooks disabled for %s", req.Name) - } - - // From here on out, the release is currently considered to be in Status_DELETING - // state. - if err := s.env.Releases.Update(rel); err != nil { - s.Log("uninstall: Failed to store updated release: %s", err) - } - - kept, errs := s.ReleaseModule.Delete(rel, req, s.env) - res.Info = kept - - es := make([]string, 0, len(errs)) - for _, e := range errs { - s.Log("error: %v", e) - es = append(es, e.Error()) - } - - if !req.DisableHooks { - if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, hooks.PostDelete, req.Timeout); err != nil { - es = append(es, err.Error()) - } - } - - rel.Info.Status.Code = release.Status_DELETED - rel.Info.Description = "Deletion complete" - - if req.Purge { - s.Log("purge requested for %s", req.Name) - err := s.purgeReleases(rels...) - if err != nil { - s.Log("uninstall: Failed to purge the release: %s", err) - } - return res, err - } - - if err := s.env.Releases.Update(rel); err != nil { - s.Log("uninstall: Failed to store updated release: %s", err) - } - - if len(es) > 0 { - return res, fmt.Errorf("deletion completed with %d error(s): %s", len(es), strings.Join(es, "; ")) - } - return res, nil -} - -func (s *ReleaseServer) purgeReleases(rels ...*release.Release) error { - for _, rel := range rels { - if _, err := s.env.Releases.Delete(rel.Name, rel.Version); err != nil { - return err - } - } - return nil -} diff --git a/pkg/tiller/release_uninstall_test.go b/pkg/tiller/release_uninstall_test.go deleted file mode 100644 index 20bfd24860d..00000000000 --- a/pkg/tiller/release_uninstall_test.go +++ /dev/null @@ -1,178 +0,0 @@ -/* -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 tiller - -import ( - "strings" - "testing" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" -) - -func TestUninstallRelease(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rs.env.Releases.Create(releaseStub()) - - req := &services.UninstallReleaseRequest{ - Name: "angry-panda", - } - - res, err := rs.UninstallRelease(c, req) - if err != nil { - t.Fatalf("Failed uninstall: %s", err) - } - - if res.Release.Name != "angry-panda" { - t.Errorf("Expected angry-panda, got %q", res.Release.Name) - } - - if res.Release.Info.Status.Code != release.Status_DELETED { - 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) - } - - if res.Release.Info.Description != "Deletion complete" { - t.Errorf("Expected Deletion complete, got %q", res.Release.Info.Description) - } -} - -func TestUninstallPurgeRelease(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rs.env.Releases.Create(rel) - upgradedRel := upgradeReleaseVersion(rel) - rs.env.Releases.Update(rel) - rs.env.Releases.Create(upgradedRel) - - req := &services.UninstallReleaseRequest{ - Name: "angry-panda", - Purge: true, - } - - res, err := rs.UninstallRelease(c, req) - if err != nil { - t.Fatalf("Failed uninstall: %s", err) - } - - if res.Release.Name != "angry-panda" { - t.Errorf("Expected angry-panda, got %q", res.Release.Name) - } - - if res.Release.Info.Status.Code != release.Status_DELETED { - t.Errorf("Expected status code to be DELETED, got %d", res.Release.Info.Status.Code) - } - - if res.Release.Info.Deleted.Seconds <= 0 { - t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds) - } - rels, err := rs.GetHistory(helm.NewContext(), &services.GetHistoryRequest{Name: "angry-panda"}) - if err != nil { - t.Fatal(err) - } - if len(rels.Releases) != 0 { - t.Errorf("Expected no releases in storage, got %d", len(rels.Releases)) - } -} - -func TestUninstallPurgeDeleteRelease(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rs.env.Releases.Create(releaseStub()) - - req := &services.UninstallReleaseRequest{ - Name: "angry-panda", - } - - _, err := rs.UninstallRelease(c, req) - if err != nil { - t.Fatalf("Failed uninstall: %s", err) - } - - req2 := &services.UninstallReleaseRequest{ - Name: "angry-panda", - Purge: true, - } - - _, err2 := rs.UninstallRelease(c, req2) - if err2 != nil && err2.Error() != "'angry-panda' has no deployed releases" { - t.Errorf("Failed uninstall: %s", err2) - } -} - -func TestUninstallReleaseWithKeepPolicy(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - name := "angry-bunny" - rs.env.Releases.Create(releaseWithKeepStub(name)) - - req := &services.UninstallReleaseRequest{ - Name: name, - } - - res, err := rs.UninstallRelease(c, req) - if err != nil { - t.Fatalf("Failed uninstall: %s", err) - } - - if res.Release.Name != name { - t.Errorf("Expected angry-bunny, got %q", res.Release.Name) - } - - if res.Release.Info.Status.Code != release.Status_DELETED { - t.Errorf("Expected status code to be DELETED, got %d", res.Release.Info.Status.Code) - } - - if res.Info == "" { - t.Errorf("Expected response info to not be empty") - } else { - if !strings.Contains(res.Info, "[ConfigMap] test-cm-keep") { - t.Errorf("unexpected output: %s", res.Info) - } - } -} - -func TestUninstallReleaseNoHooks(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rs.env.Releases.Create(releaseStub()) - - req := &services.UninstallReleaseRequest{ - Name: "angry-panda", - DisableHooks: true, - } - - res, err := rs.UninstallRelease(c, req) - if err != nil { - t.Errorf("Failed uninstall: %s", err) - } - - // The default value for a protobuf timestamp is nil. - if res.Release.Hooks[0].LastRun != nil { - t.Errorf("Expected LastRun to be zero, got %d.", res.Release.Hooks[0].LastRun.Seconds) - } -} diff --git a/pkg/tiller/release_update.go b/pkg/tiller/release_update.go deleted file mode 100644 index 6f5d3733122..00000000000 --- a/pkg/tiller/release_update.go +++ /dev/null @@ -1,293 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - "strings" - - ctx "golang.org/x/net/context" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hooks" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/timeconv" -) - -// UpdateRelease takes an existing release and new information, and upgrades the release. -func (s *ReleaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { - if err := validateReleaseName(req.Name); err != nil { - s.Log("updateRelease: Release name is invalid: %s", req.Name) - return nil, err - } - s.Log("preparing update for %s", req.Name) - currentRelease, updatedRelease, err := s.prepareUpdate(req) - if err != nil { - if req.Force { - // Use the --force, Luke. - return s.performUpdateForce(req) - } - return nil, err - } - - if !req.DryRun { - s.Log("creating updated release for %s", req.Name) - if err := s.env.Releases.Create(updatedRelease); err != nil { - return nil, err - } - } - - s.Log("performing update for %s", req.Name) - res, err := s.performUpdate(currentRelease, updatedRelease, req) - if err != nil { - return res, err - } - - if !req.DryRun { - s.Log("updating status for updated release for %s", req.Name) - if err := s.env.Releases.Update(updatedRelease); err != nil { - return res, err - } - } - - return res, nil -} - -// prepareUpdate builds an updated release for an update operation. -func (s *ReleaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*release.Release, *release.Release, error) { - if req.Chart == nil { - return nil, nil, errMissingChart - } - - // finds the deployed release with the given name - currentRelease, err := s.env.Releases.Deployed(req.Name) - if err != nil { - return nil, nil, err - } - - // determine if values will be reused - if err := s.reuseValues(req, currentRelease); err != nil { - return nil, nil, err - } - - // finds the non-deleted release with the given name - lastRelease, err := s.env.Releases.Last(req.Name) - if err != nil { - return nil, nil, err - } - - // Increment revision count. This is passed to templates, and also stored on - // the release object. - revision := lastRelease.Version + 1 - - ts := timeconv.Now() - options := chartutil.ReleaseOptions{ - Name: req.Name, - Time: ts, - Namespace: currentRelease.Namespace, - IsUpgrade: true, - Revision: int(revision), - } - - caps, err := capabilities(s.clientset.Discovery()) - if err != nil { - return nil, nil, err - } - valuesToRender, err := chartutil.ToRenderValuesCaps(req.Chart, req.Values, options, caps) - if err != nil { - return nil, nil, err - } - - hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender, caps.APIVersions) - if err != nil { - return nil, nil, err - } - - // Store an updated release. - updatedRelease := &release.Release{ - Name: req.Name, - Namespace: currentRelease.Namespace, - Chart: req.Chart, - Config: req.Values, - Info: &release.Info{ - FirstDeployed: currentRelease.Info.FirstDeployed, - LastDeployed: ts, - Status: &release.Status{Code: release.Status_PENDING_UPGRADE}, - Description: "Preparing upgrade", // This should be overwritten later. - }, - Version: revision, - Manifest: manifestDoc.String(), - Hooks: hooks, - } - - if len(notesTxt) > 0 { - updatedRelease.Info.Status.Notes = notesTxt - } - err = validateManifest(s.env.KubeClient, currentRelease.Namespace, manifestDoc.Bytes()) - return currentRelease, updatedRelease, err -} - -// performUpdateForce performs the same action as a `helm delete && helm install --replace`. -func (s *ReleaseServer) performUpdateForce(req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { - // find the last release with the given name - oldRelease, err := s.env.Releases.Last(req.Name) - if err != nil { - return nil, err - } - - newRelease, err := s.prepareRelease(&services.InstallReleaseRequest{ - Chart: req.Chart, - Values: req.Values, - DryRun: req.DryRun, - Name: req.Name, - DisableHooks: req.DisableHooks, - Namespace: oldRelease.Namespace, - ReuseName: true, - Timeout: req.Timeout, - Wait: req.Wait, - }) - res := &services.UpdateReleaseResponse{Release: newRelease} - if err != nil { - s.Log("failed update prepare step: %s", err) - // On dry run, append the manifest contents to a failed release. This is - // a stop-gap until we can revisit an error backchannel post-2.0. - if req.DryRun && strings.HasPrefix(err.Error(), "YAML parse error") { - err = fmt.Errorf("%s\n%s", err, newRelease.Manifest) - } - return res, err - } - - // From here on out, the release is considered to be in Status_DELETING or Status_DELETED - // state. There is no turning back. - oldRelease.Info.Status.Code = release.Status_DELETING - oldRelease.Info.Deleted = timeconv.Now() - oldRelease.Info.Description = "Deletion in progress (or silently failed)" - s.recordRelease(oldRelease, true) - - // pre-delete hooks - if !req.DisableHooks { - if err := s.execHook(oldRelease.Hooks, oldRelease.Name, oldRelease.Namespace, hooks.PreDelete, req.Timeout); err != nil { - return res, err - } - } else { - s.Log("hooks disabled for %s", req.Name) - } - - // delete manifests from the old release - _, errs := s.ReleaseModule.Delete(oldRelease, nil, s.env) - - oldRelease.Info.Status.Code = release.Status_DELETED - oldRelease.Info.Description = "Deletion complete" - s.recordRelease(oldRelease, true) - - if len(errs) > 0 { - es := make([]string, 0, len(errs)) - for _, e := range errs { - s.Log("error: %v", e) - es = append(es, e.Error()) - } - return res, fmt.Errorf("Upgrade --force successfully deleted the previous release, but encountered %d error(s) and cannot continue: %s", len(es), strings.Join(es, "; ")) - } - - // post-delete hooks - if !req.DisableHooks { - if err := s.execHook(oldRelease.Hooks, oldRelease.Name, oldRelease.Namespace, hooks.PostDelete, req.Timeout); err != nil { - return res, err - } - } - - // pre-install hooks - if !req.DisableHooks { - if err := s.execHook(newRelease.Hooks, newRelease.Name, newRelease.Namespace, hooks.PreInstall, req.Timeout); err != nil { - return res, err - } - } - - // update new release with next revision number so as to append to the old release's history - newRelease.Version = oldRelease.Version + 1 - s.recordRelease(newRelease, false) - if err := s.ReleaseModule.Update(oldRelease, newRelease, req, s.env); err != nil { - msg := fmt.Sprintf("Upgrade %q failed: %s", newRelease.Name, err) - s.Log("warning: %s", msg) - newRelease.Info.Status.Code = release.Status_FAILED - newRelease.Info.Description = msg - s.recordRelease(newRelease, true) - return res, err - } - - // post-install hooks - if !req.DisableHooks { - if err := s.execHook(newRelease.Hooks, newRelease.Name, newRelease.Namespace, hooks.PostInstall, req.Timeout); err != nil { - msg := fmt.Sprintf("Release %q failed post-install: %s", newRelease.Name, err) - s.Log("warning: %s", msg) - newRelease.Info.Status.Code = release.Status_FAILED - newRelease.Info.Description = msg - s.recordRelease(newRelease, true) - return res, err - } - } - - newRelease.Info.Status.Code = release.Status_DEPLOYED - newRelease.Info.Description = "Upgrade complete" - s.recordRelease(newRelease, true) - - return res, nil -} - -func (s *ReleaseServer) performUpdate(originalRelease, updatedRelease *release.Release, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { - res := &services.UpdateReleaseResponse{Release: updatedRelease} - - if req.DryRun { - s.Log("dry run for %s", updatedRelease.Name) - res.Release.Info.Description = "Dry run complete" - return res, nil - } - - // pre-upgrade hooks - if !req.DisableHooks { - if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, hooks.PreUpgrade, req.Timeout); err != nil { - return res, err - } - } else { - s.Log("update hooks disabled for %s", req.Name) - } - if err := s.ReleaseModule.Update(originalRelease, updatedRelease, req, s.env); err != nil { - msg := fmt.Sprintf("Upgrade %q failed: %s", updatedRelease.Name, err) - s.Log("warning: %s", msg) - updatedRelease.Info.Status.Code = release.Status_FAILED - updatedRelease.Info.Description = msg - s.recordRelease(originalRelease, true) - s.recordRelease(updatedRelease, true) - return res, err - } - - // post-upgrade hooks - if !req.DisableHooks { - if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, hooks.PostUpgrade, req.Timeout); err != nil { - return res, err - } - } - - originalRelease.Info.Status.Code = release.Status_SUPERSEDED - s.recordRelease(originalRelease, true) - - updatedRelease.Info.Status.Code = release.Status_DEPLOYED - updatedRelease.Info.Description = "Upgrade complete" - - return res, nil -} diff --git a/pkg/tiller/release_update_test.go b/pkg/tiller/release_update_test.go deleted file mode 100644 index a1b9a4bff1b..00000000000 --- a/pkg/tiller/release_update_test.go +++ /dev/null @@ -1,446 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - "strings" - "testing" - - "github.com/golang/protobuf/proto" - - "k8s.io/helm/pkg/helm" - "k8s.io/helm/pkg/proto/hapi/chart" - "k8s.io/helm/pkg/proto/hapi/release" - "k8s.io/helm/pkg/proto/hapi/services" -) - -func TestUpdateRelease(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rs.env.Releases.Create(rel) - - req := &services.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - } - res, err := rs.UpdateRelease(c, req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - - if res.Release.Name == "" { - t.Errorf("Expected release name.") - } - - if res.Release.Name != rel.Name { - t.Errorf("Updated release name does not match previous release name. Expected %s, got %s", rel.Name, res.Release.Name) - } - - if res.Release.Namespace != rel.Namespace { - t.Errorf("Expected release namespace '%s', got '%s'.", rel.Namespace, res.Release.Namespace) - } - - updated := compareStoredAndReturnedRelease(t, *rs, *res) - - if len(updated.Hooks) != 1 { - t.Fatalf("Expected 1 hook, got %d", len(updated.Hooks)) - } - if updated.Hooks[0].Manifest != manifestWithUpgradeHooks { - t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest) - } - - if updated.Hooks[0].Events[0] != release.Hook_POST_UPGRADE { - t.Errorf("Expected event 0 to be post upgrade") - } - - if updated.Hooks[0].Events[1] != release.Hook_PRE_UPGRADE { - t.Errorf("Expected event 0 to be pre upgrade") - } - - if len(updated.Manifest) == 0 { - t.Errorf("Expected manifest in %v", res) - } - - if res.Release.Config == nil { - t.Errorf("Got release without config: %#v", res.Release) - } else if res.Release.Config.Raw != rel.Config.Raw { - t.Errorf("Expected release values %q, got %q", rel.Config.Raw, res.Release.Config.Raw) - } - - if !strings.Contains(updated.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { - t.Errorf("unexpected output: %s", updated.Manifest) - } - - if res.Release.Version != 2 { - t.Errorf("Expected release version to be %v, got %v", 2, res.Release.Version) - } - - edesc := "Upgrade complete" - if got := res.Release.Info.Description; got != edesc { - t.Errorf("Expected description %q, got %q", edesc, got) - } -} -func TestUpdateRelease_ResetValues(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rs.env.Releases.Create(rel) - - req := &services.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - ResetValues: true, - } - res, err := rs.UpdateRelease(c, req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - // This should have been unset. Config: &chart.Config{Raw: `name: value`}, - if res.Release.Config != nil && res.Release.Config.Raw != "" { - t.Errorf("Expected chart config to be empty, got %q", res.Release.Config.Raw) - } -} - -// This is a regression test for bug found in issue #3655 -func TestUpdateRelease_ComplexReuseValues(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - - installReq := &services.InstallReleaseRequest{ - Namespace: "spaced", - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithHook)}, - }, - Values: &chart.Config{Raw: "defaultFoo: defaultBar"}, - }, - Values: &chart.Config{Raw: "foo: bar"}, - } - - fmt.Println("Running Install release with foo: bar override") - installResp, err := rs.InstallRelease(c, installReq) - if err != nil { - t.Fatal(err) - } - - rel := installResp.Release - req := &services.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - Values: &chart.Config{Raw: "defaultFoo: defaultBar"}, - }, - } - - fmt.Println("Running Update release with no overrides and no reuse-values flag") - res, err := rs.UpdateRelease(c, req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - - expect := "foo: bar" - if res.Release.Config != nil && res.Release.Config.Raw != expect { - t.Errorf("Expected chart values to be %q, got %q", expect, res.Release.Config.Raw) - } - - rel = res.Release - req = &services.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - Values: &chart.Config{Raw: "defaultFoo: defaultBar"}, - }, - Values: &chart.Config{Raw: "foo2: bar2"}, - ReuseValues: true, - } - - fmt.Println("Running Update release with foo2: bar2 override and reuse-values") - res, err = rs.UpdateRelease(c, req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - - // This should have the newly-passed overrides. - expect = "foo: bar\nfoo2: bar2\n" - if res.Release.Config != nil && res.Release.Config.Raw != expect { - t.Errorf("Expected request config to be %q, got %q", expect, res.Release.Config.Raw) - } - - rel = res.Release - req = &services.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - Values: &chart.Config{Raw: "defaultFoo: defaultBar"}, - }, - Values: &chart.Config{Raw: "foo: baz"}, - ReuseValues: true, - } - - fmt.Println("Running Update release with foo=baz override with reuse-values flag") - res, err = rs.UpdateRelease(c, req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - expect = "foo: baz\nfoo2: bar2\n" - if res.Release.Config != nil && res.Release.Config.Raw != expect { - t.Errorf("Expected chart values to be %q, got %q", expect, res.Release.Config.Raw) - } -} - -func TestUpdateRelease_ReuseValues(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rs.env.Releases.Create(rel) - - req := &services.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - // Since reuseValues is set, this should get ignored. - Values: &chart.Config{Raw: "foo: bar\n"}, - }, - Values: &chart.Config{Raw: "name2: val2"}, - ReuseValues: true, - } - res, err := rs.UpdateRelease(c, req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - // This should have been overwritten with the old value. - expect := "name: value\n" - if res.Release.Chart.Values != nil && res.Release.Chart.Values.Raw != expect { - t.Errorf("Expected chart values to be %q, got %q", expect, res.Release.Chart.Values.Raw) - } - // This should have the newly-passed overrides and any other computed values. `name: value` comes from release Config via releaseStub() - expect = "name: value\nname2: val2\n" - if res.Release.Config != nil && res.Release.Config.Raw != expect { - t.Errorf("Expected request config to be %q, got %q", expect, res.Release.Config.Raw) - } - compareStoredAndReturnedRelease(t, *rs, *res) -} - -func TestUpdateRelease_ResetReuseValues(t *testing.T) { - // This verifies that when both reset and reuse are set, reset wins. - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rs.env.Releases.Create(rel) - - req := &services.UpdateReleaseRequest{ - Name: rel.Name, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - ResetValues: true, - ReuseValues: true, - } - res, err := rs.UpdateRelease(c, req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - // This should have been unset. Config: &chart.Config{Raw: `name: value`}, - if res.Release.Config != nil && res.Release.Config.Raw != "" { - t.Errorf("Expected chart config to be empty, got %q", res.Release.Config.Raw) - } - compareStoredAndReturnedRelease(t, *rs, *res) -} - -func TestUpdateReleaseFailure(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rs.env.Releases.Create(rel) - rs.env.KubeClient = newUpdateFailingKubeClient() - rs.Log = t.Logf - - req := &services.UpdateReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/something", Data: []byte("hello: world")}, - }, - }, - } - - res, err := rs.UpdateRelease(c, req) - if err == nil { - t.Error("Expected failed update") - } - - if updatedStatus := res.Release.Info.Status.Code; updatedStatus != release.Status_FAILED { - t.Errorf("Expected FAILED release. Got %d", updatedStatus) - } - - compareStoredAndReturnedRelease(t, *rs, *res) - - expectedDescription := "Upgrade \"angry-panda\" failed: Failed update in kube client" - if got := res.Release.Info.Description; got != expectedDescription { - t.Errorf("Expected description %q, got %q", expectedDescription, got) - } - - oldRelease, err := rs.env.Releases.Get(rel.Name, rel.Version) - if err != nil { - t.Errorf("Expected to be able to get previous release") - } - if oldStatus := oldRelease.Info.Status.Code; oldStatus != release.Status_DEPLOYED { - t.Errorf("Expected Deployed status on previous Release version. Got %v", oldStatus) - } -} - -func TestUpdateReleaseFailure_Force(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := namedReleaseStub("forceful-luke", release.Status_FAILED) - rs.env.Releases.Create(rel) - rs.Log = t.Logf - - req := &services.UpdateReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/something", Data: []byte("text: 'Did you ever hear the tragedy of Darth Plagueis the Wise? I thought not. It’s not a story the Jedi would tell you. It’s a Sith legend. Darth Plagueis was a Dark Lord of the Sith, so powerful and so wise he could use the Force to influence the Midichlorians to create life... He had such a knowledge of the Dark Side that he could even keep the ones he cared about from dying. The Dark Side of the Force is a pathway to many abilities some consider to be unnatural. He became so powerful... The only thing he was afraid of was losing his power, which eventually, of course, he did. Unfortunately, he taught his apprentice everything he knew, then his apprentice killed him in his sleep. Ironic. He could save others from death, but not himself.'")}, - }, - }, - Force: true, - } - - res, err := rs.UpdateRelease(c, req) - if err != nil { - t.Errorf("Expected successful update, got %v", err) - } - - if updatedStatus := res.Release.Info.Status.Code; updatedStatus != release.Status_DEPLOYED { - t.Errorf("Expected DEPLOYED release. Got %d", updatedStatus) - } - - compareStoredAndReturnedRelease(t, *rs, *res) - - expectedDescription := "Upgrade complete" - if got := res.Release.Info.Description; got != expectedDescription { - t.Errorf("Expected description %q, got %q", expectedDescription, got) - } - - oldRelease, err := rs.env.Releases.Get(rel.Name, rel.Version) - if err != nil { - t.Errorf("Expected to be able to get previous release") - } - if oldStatus := oldRelease.Info.Status.Code; oldStatus != release.Status_DELETED { - t.Errorf("Expected Deleted status on previous Release version. Got %v", oldStatus) - } -} - -func TestUpdateReleaseNoHooks(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rs.env.Releases.Create(rel) - - req := &services.UpdateReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - Chart: &chart.Chart{ - Metadata: &chart.Metadata{Name: "hello"}, - Templates: []*chart.Template{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithUpgradeHooks)}, - }, - }, - } - - res, err := rs.UpdateRelease(c, req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } - - if hl := res.Release.Hooks[0].LastRun; hl != nil { - t.Errorf("Expected that no hooks were run. Got %d", hl) - } - -} - -func TestUpdateReleaseNoChanges(t *testing.T) { - c := helm.NewContext() - rs := rsFixture() - rel := releaseStub() - rs.env.Releases.Create(rel) - - req := &services.UpdateReleaseRequest{ - Name: rel.Name, - DisableHooks: true, - Chart: rel.GetChart(), - } - - _, err := rs.UpdateRelease(c, req) - if err != nil { - t.Fatalf("Failed updated: %s", err) - } -} - -func compareStoredAndReturnedRelease(t *testing.T, rs ReleaseServer, res services.UpdateReleaseResponse) *release.Release { - storedRelease, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) - if err != nil { - t.Fatalf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) - } - - if !proto.Equal(storedRelease, res.Release) { - t.Errorf("Stored release doesn't match returned Release") - } - - return storedRelease -} diff --git a/pkg/tiller/release_version.go b/pkg/tiller/release_version.go deleted file mode 100644 index 66b7137bba7..00000000000 --- a/pkg/tiller/release_version.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -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 tiller - -import ( - ctx "golang.org/x/net/context" - - "k8s.io/helm/pkg/proto/hapi/services" - "k8s.io/helm/pkg/version" -) - -// GetVersion sends the server version. -func (s *ReleaseServer) GetVersion(c ctx.Context, req *services.GetVersionRequest) (*services.GetVersionResponse, error) { - v := version.GetVersionProto() - return &services.GetVersionResponse{Version: v}, nil -} diff --git a/pkg/tiller/resource_policy.go b/pkg/tiller/resource_policy.go deleted file mode 100644 index 66da1283ff2..00000000000 --- a/pkg/tiller/resource_policy.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -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 tiller - -import ( - "bytes" - "strings" - - "k8s.io/helm/pkg/kube" - "k8s.io/helm/pkg/tiller/environment" -) - -// resourcePolicyAnno is the annotation name for a resource policy -const resourcePolicyAnno = "helm.sh/resource-policy" - -// keepPolicy is the resource policy type for keep -// -// This resource policy type allows resources to skip being deleted -// during an uninstallRelease action. -const keepPolicy = "keep" - -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 { - remaining = append(remaining, m) - continue - } - - resourcePolicyType, ok := m.Head.Metadata.Annotations[resourcePolicyAnno] - if !ok { - remaining = append(remaining, m) - continue - } - - resourcePolicyType = strings.ToLower(strings.TrimSpace(resourcePolicyType)) - if resourcePolicyType == keepPolicy { - keep = append(keep, m) - } - - } - return keep, remaining -} - -func summarizeKeptManifests(manifests []Manifest, kubeClient environment.KubeClient, namespace string) string { - var message string - for _, m := range manifests { - // check if m is in fact present from k8s client's POV. - output, err := kubeClient.Get(namespace, bytes.NewBufferString(m.Content)) - if err != nil || strings.Contains(output, kube.MissingGetHeader) { - continue - } - - details := "[" + m.Head.Kind + "] " + m.Head.Metadata.Name + "\n" - if message == "" { - message = "These resources were kept due to the resource policy:\n" - } - message = message + details - } - return message -} diff --git a/pkg/tiller/server.go b/pkg/tiller/server.go deleted file mode 100644 index 818cfd47abc..00000000000 --- a/pkg/tiller/server.go +++ /dev/null @@ -1,96 +0,0 @@ -/* -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 tiller - -import ( - "fmt" - "log" - "strings" - - goprom "github.com/grpc-ecosystem/go-grpc-prometheus" - "golang.org/x/net/context" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - - "k8s.io/helm/pkg/version" -) - -// maxMsgSize use 20MB as the default message size limit. -// grpc library default is 4MB -const maxMsgSize = 1024 * 1024 * 20 - -// DefaultServerOpts returns the set of default grpc ServerOption's that Tiller requires. -func DefaultServerOpts() []grpc.ServerOption { - return []grpc.ServerOption{ - grpc.MaxRecvMsgSize(maxMsgSize), - grpc.MaxSendMsgSize(maxMsgSize), - grpc.UnaryInterceptor(newUnaryInterceptor()), - grpc.StreamInterceptor(newStreamInterceptor()), - } -} - -// NewServer creates a new grpc server. -func NewServer(opts ...grpc.ServerOption) *grpc.Server { - return grpc.NewServer(append(DefaultServerOpts(), opts...)...) -} - -func newUnaryInterceptor() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - if err := checkClientVersion(ctx); err != nil { - // whitelist GetVersion() from the version check - if _, m := splitMethod(info.FullMethod); m != "GetVersion" { - log.Println(err) - return nil, err - } - } - return goprom.UnaryServerInterceptor(ctx, req, info, handler) - } -} - -func newStreamInterceptor() grpc.StreamServerInterceptor { - return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - if err := checkClientVersion(ss.Context()); err != nil { - log.Println(err) - return err - } - return goprom.StreamServerInterceptor(srv, ss, info, handler) - } -} - -func splitMethod(fullMethod string) (string, string) { - if frags := strings.Split(fullMethod, "/"); len(frags) == 3 { - return frags[1], frags[2] - } - return "unknown", "unknown" -} - -func versionFromContext(ctx context.Context) string { - if md, ok := metadata.FromIncomingContext(ctx); ok { - if v, ok := md["x-helm-api-client"]; ok && len(v) > 0 { - return v[0] - } - } - return "" -} - -func checkClientVersion(ctx context.Context) error { - clientVersion := versionFromContext(ctx) - if !version.IsCompatible(clientVersion, version.GetVersion()) { - return fmt.Errorf("incompatible versions client[%s] server[%s]", clientVersion, version.GetVersion()) - } - return nil -} diff --git a/pkg/time/time.go b/pkg/time/time.go new file mode 100644 index 00000000000..44f3fedfb2e --- /dev/null +++ b/pkg/time/time.go @@ -0,0 +1,91 @@ +/* +Copyright The Helm Authors. + +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 time contains a wrapper for time.Time in the standard library and +// associated methods. This package mainly exists to workaround an issue in Go +// where the serializer doesn't omit an empty value for time: +// https://github.com/golang/go/issues/11939. As such, this can be removed if a +// proposal is ever accepted for Go +package time + +import ( + "bytes" + "time" +) + +// emptyString contains an empty JSON string value to be used as output +var emptyString = `""` + +// Time is a convenience wrapper around stdlib time, but with different +// marshalling and unmarshaling for zero values +type Time struct { + time.Time +} + +// Now returns the current time. It is a convenience wrapper around time.Now() +func Now() Time { + return Time{time.Now()} +} + +func (t Time) MarshalJSON() ([]byte, error) { + if t.Time.IsZero() { + return []byte(emptyString), nil + } + + return t.Time.MarshalJSON() +} + +func (t *Time) UnmarshalJSON(b []byte) error { + if bytes.Equal(b, []byte("null")) { + return nil + } + // If it is empty, we don't have to set anything since time.Time is not a + // pointer and will be set to the zero value + if bytes.Equal([]byte(emptyString), b) { + return nil + } + + return t.Time.UnmarshalJSON(b) +} + +func Parse(layout, value string) (Time, error) { + t, err := time.Parse(layout, value) + return Time{Time: t}, err +} +func ParseInLocation(layout, value string, loc *time.Location) (Time, error) { + t, err := time.ParseInLocation(layout, value, loc) + return Time{Time: t}, err +} + +func Date(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) Time { + return Time{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)} +} + +func Unix(sec int64, nsec int64) Time { return Time{Time: time.Unix(sec, nsec)} } + +func (t Time) Add(d time.Duration) Time { return Time{Time: t.Time.Add(d)} } +func (t Time) AddDate(years int, months int, days int) Time { + return Time{Time: t.Time.AddDate(years, months, days)} +} +func (t Time) After(u Time) bool { return t.Time.After(u.Time) } +func (t Time) Before(u Time) bool { return t.Time.Before(u.Time) } +func (t Time) Equal(u Time) bool { return t.Time.Equal(u.Time) } +func (t Time) In(loc *time.Location) Time { return Time{Time: t.Time.In(loc)} } +func (t Time) Local() Time { return Time{Time: t.Time.Local()} } +func (t Time) Round(d time.Duration) Time { return Time{Time: t.Time.Round(d)} } +func (t Time) Sub(u Time) time.Duration { return t.Time.Sub(u.Time) } +func (t Time) Truncate(d time.Duration) Time { return Time{Time: t.Time.Truncate(d)} } +func (t Time) UTC() Time { return Time{Time: t.Time.UTC()} } diff --git a/pkg/time/time_test.go b/pkg/time/time_test.go new file mode 100644 index 00000000000..20f0f8e2912 --- /dev/null +++ b/pkg/time/time_test.go @@ -0,0 +1,83 @@ +/* +Copyright The Helm Authors. + +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 time + +import ( + "encoding/json" + "testing" + "time" +) + +var ( + testingTime, _ = Parse(time.RFC3339, "1977-09-02T22:04:05Z") + testingTimeString = `"1977-09-02T22:04:05Z"` +) + +func TestNonZeroValueMarshal(t *testing.T) { + res, err := json.Marshal(testingTime) + if err != nil { + t.Fatal(err) + } + if testingTimeString != string(res) { + t.Errorf("expected a marshaled value of %s, got %s", testingTimeString, res) + } +} + +func TestZeroValueMarshal(t *testing.T) { + res, err := json.Marshal(Time{}) + if err != nil { + t.Fatal(err) + } + if string(res) != emptyString { + t.Errorf("expected zero value to marshal to empty string, got %s", res) + } +} + +func TestNonZeroValueUnmarshal(t *testing.T) { + var myTime Time + err := json.Unmarshal([]byte(testingTimeString), &myTime) + if err != nil { + t.Fatal(err) + } + if !myTime.Equal(testingTime) { + t.Errorf("expected time to be equal to %v, got %v", testingTime, myTime) + } +} + +func TestEmptyStringUnmarshal(t *testing.T) { + var myTime Time + err := json.Unmarshal([]byte(emptyString), &myTime) + if err != nil { + t.Fatal(err) + } + if !myTime.IsZero() { + t.Errorf("expected time to be equal to zero value, got %v", myTime) + } +} + +func TestZeroValueUnmarshal(t *testing.T) { + // This test ensures that we can unmarshal any time value that was output + // with the current go default value of "0001-01-01T00:00:00Z" + var myTime Time + err := json.Unmarshal([]byte(`"0001-01-01T00:00:00Z"`), &myTime) + if err != nil { + t.Fatal(err) + } + if !myTime.IsZero() { + t.Errorf("expected time to be equal to zero value, got %v", myTime) + } +} diff --git a/pkg/timeconv/doc.go b/pkg/timeconv/doc.go deleted file mode 100644 index 23516739130..00000000000 --- a/pkg/timeconv/doc.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -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 timeconv contains utilities for converting time. - -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 // import "k8s.io/helm/pkg/timeconv" diff --git a/pkg/timeconv/timeconv.go b/pkg/timeconv/timeconv.go deleted file mode 100644 index 24ff10f4e03..00000000000 --- a/pkg/timeconv/timeconv.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -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 timeconv - -import ( - "time" - - "github.com/golang/protobuf/ptypes/timestamp" -) - -// Now creates a timestamp.Timestamp representing the current time. -func Now() *timestamp.Timestamp { - return Timestamp(time.Now()) -} - -// Timestamp converts a time.Time to a protobuf *timestamp.Timestamp. -func Timestamp(t time.Time) *timestamp.Timestamp { - return ×tamp.Timestamp{ - Seconds: t.Unix(), - Nanos: int32(t.Nanosecond()), - } -} - -// Time converts a protobuf *timestamp.Timestamp to a time.Time. -func Time(ts *timestamp.Timestamp) time.Time { - return time.Unix(ts.Seconds, int64(ts.Nanos)) -} - -// Format formats a *timestamp.Timestamp into a string. -// -// This follows the rules for time.Time.Format(). -func Format(ts *timestamp.Timestamp, layout string) string { - return Time(ts).Format(layout) -} - -// String formats the timestamp into a user-friendly string. -// -// Currently, this uses the 'time.ANSIC' format string, but there is no guarantee -// that this will not change. -// -// This is a convenience function for formatting timestamps for user display. -func String(ts *timestamp.Timestamp) string { - return Format(ts, time.ANSIC) -} diff --git a/pkg/timeconv/timeconv_test.go b/pkg/timeconv/timeconv_test.go deleted file mode 100644 index f673df3c97f..00000000000 --- a/pkg/timeconv/timeconv_test.go +++ /dev/null @@ -1,62 +0,0 @@ -/* -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 timeconv - -import ( - "testing" - "time" -) - -func TestNow(t *testing.T) { - now := time.Now() - ts := Now() - var drift int64 = 5 - if ts.Seconds < int64(now.Second())-drift { - t.Errorf("Unexpected time drift: %d", ts.Seconds) - } -} - -func TestTimestamp(t *testing.T) { - now := time.Now() - ts := Timestamp(now) - - if now.Unix() != ts.Seconds { - t.Errorf("Unexpected time drift: %d to %d", now.Second(), ts.Seconds) - } - - if now.Nanosecond() != int(ts.Nanos) { - t.Errorf("Unexpected nano drift: %d to %d", now.Nanosecond(), ts.Nanos) - } -} - -func TestTime(t *testing.T) { - nowts := Now() - now := Time(nowts) - - if now.Unix() != nowts.Seconds { - t.Errorf("Unexpected time drift %d", now.Unix()) - } -} - -func TestFormat(t *testing.T) { - now := time.Now() - nowts := Timestamp(now) - - if now.Format(time.ANSIC) != Format(nowts, time.ANSIC) { - t.Error("Format mismatch") - } -} diff --git a/pkg/tlsutil/cfg.go b/pkg/tlsutil/cfg.go deleted file mode 100644 index 9ce3109e1bd..00000000000 --- a/pkg/tlsutil/cfg.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -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 tlsutil - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "os" -) - -// Options represents configurable options used to create client and server TLS configurations. -type Options struct { - CaCertFile string - // If either the KeyFile or CertFile is empty, ClientConfig() will not load them, - // preventing Helm from authenticating to Tiller. They are required to be non-empty - // when calling ServerConfig, otherwise an error is returned. - KeyFile string - CertFile string - // Client-only options - InsecureSkipVerify bool - // Server-only options - ClientAuth tls.ClientAuthType -} - -// ClientConfig retusn a TLS configuration for use by a Helm client. -func ClientConfig(opts Options) (cfg *tls.Config, err error) { - var cert *tls.Certificate - var pool *x509.CertPool - - if opts.CertFile != "" || opts.KeyFile != "" { - if cert, err = CertFromFilePair(opts.CertFile, opts.KeyFile); err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("could not load x509 key pair (cert: %q, key: %q): %v", opts.CertFile, opts.KeyFile, err) - } - return nil, fmt.Errorf("could not read x509 key pair (cert: %q, key: %q): %v", opts.CertFile, opts.KeyFile, err) - } - } - if !opts.InsecureSkipVerify && opts.CaCertFile != "" { - if pool, err = CertPoolFromFile(opts.CaCertFile); err != nil { - return nil, err - } - } - - cfg = &tls.Config{InsecureSkipVerify: opts.InsecureSkipVerify, Certificates: []tls.Certificate{*cert}, RootCAs: pool} - return cfg, nil -} - -// ServerConfig returns a TLS configuration for use by the Tiller server. -func ServerConfig(opts Options) (cfg *tls.Config, err error) { - var cert *tls.Certificate - var pool *x509.CertPool - - if cert, err = CertFromFilePair(opts.CertFile, opts.KeyFile); err != nil { - if os.IsNotExist(err) { - return nil, fmt.Errorf("could not load x509 key pair (cert: %q, key: %q): %v", opts.CertFile, opts.KeyFile, err) - } - return nil, fmt.Errorf("could not read x509 key pair (cert: %q, key: %q): %v", opts.CertFile, opts.KeyFile, err) - } - if opts.ClientAuth >= tls.VerifyClientCertIfGiven && opts.CaCertFile != "" { - if pool, err = CertPoolFromFile(opts.CaCertFile); err != nil { - return nil, err - } - } - - cfg = &tls.Config{MinVersion: tls.VersionTLS12, ClientAuth: opts.ClientAuth, Certificates: []tls.Certificate{*cert}, ClientCAs: pool} - return cfg, nil -} diff --git a/pkg/tlsutil/tlsutil_test.go b/pkg/tlsutil/tlsutil_test.go deleted file mode 100644 index 4f04d50ab46..00000000000 --- a/pkg/tlsutil/tlsutil_test.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -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 tlsutil - -import ( - "crypto/tls" - "path/filepath" - "testing" -) - -const tlsTestDir = "../../testdata" - -const ( - testCaCertFile = "ca.pem" - testCertFile = "crt.pem" - testKeyFile = "key.pem" -) - -func TestClientConfig(t *testing.T) { - opts := Options{ - CaCertFile: testfile(t, testCaCertFile), - CertFile: testfile(t, testCertFile), - KeyFile: testfile(t, testKeyFile), - InsecureSkipVerify: false, - } - - cfg, err := ClientConfig(opts) - if err != nil { - t.Fatalf("error building tls client config: %v", err) - } - - if got := len(cfg.Certificates); got != 1 { - t.Fatalf("expecting 1 client certificates, got %d", got) - } - if cfg.InsecureSkipVerify { - t.Fatalf("insecure skip verify mistmatch, expecting false") - } - if cfg.RootCAs == nil { - t.Fatalf("mismatch tls RootCAs, expecting non-nil") - } -} - -func TestServerConfig(t *testing.T) { - opts := Options{ - CaCertFile: testfile(t, testCaCertFile), - CertFile: testfile(t, testCertFile), - KeyFile: testfile(t, testKeyFile), - ClientAuth: tls.RequireAndVerifyClientCert, - } - - cfg, err := ServerConfig(opts) - if err != nil { - t.Fatalf("error building tls server config: %v", err) - } - if got := cfg.MinVersion; got != tls.VersionTLS12 { - t.Errorf("expecting TLS version 1.2, got %d", got) - } - if got := cfg.ClientCAs; got == nil { - t.Errorf("expecting non-nil CA pool") - } -} - -func testfile(t *testing.T, file string) (path string) { - var err error - if path, err = filepath.Abs(filepath.Join(tlsTestDir, file)); err != nil { - t.Fatalf("error getting absolute path to test file %q: %v", file, err) - } - return path -} diff --git a/pkg/version/compatible.go b/pkg/version/compatible.go deleted file mode 100644 index 735610778be..00000000000 --- a/pkg/version/compatible.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -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 version // import "k8s.io/helm/pkg/version" - -import ( - "fmt" - "strings" - - "github.com/Masterminds/semver" -) - -// IsCompatible tests if a client and server version are compatible. -func IsCompatible(client, server string) bool { - if isUnreleased(client) || isUnreleased(server) { - return true - } - cv, err := semver.NewVersion(client) - if err != nil { - return false - } - sv, err := semver.NewVersion(server) - if err != nil { - return false - } - - constraint := fmt.Sprintf("^%d.%d.x", cv.Major(), cv.Minor()) - if cv.Prerelease() != "" || sv.Prerelease() != "" { - constraint = cv.String() - } - - return IsCompatibleRange(constraint, server) -} - -// IsCompatibleRange compares a version to a constraint. -// It returns true if the version matches the constraint, and false in all other cases. -func IsCompatibleRange(constraint, ver string) bool { - sv, err := semver.NewVersion(ver) - if err != nil { - return false - } - - c, err := semver.NewConstraint(constraint) - if err != nil { - return false - } - return c.Check(sv) -} - -func isUnreleased(v string) bool { - return strings.HasSuffix(v, "unreleased") -} diff --git a/pkg/version/compatible_test.go b/pkg/version/compatible_test.go deleted file mode 100644 index adc1c489e73..00000000000 --- a/pkg/version/compatible_test.go +++ /dev/null @@ -1,66 +0,0 @@ -/* -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 version represents the current version of the project. -package version // import "k8s.io/helm/pkg/version" - -import "testing" - -func TestIsCompatible(t *testing.T) { - tests := []struct { - client string - server string - expected bool - }{ - {"v2.0.0-alpha.4", "v2.0.0-alpha.4", true}, - {"v2.0.0-alpha.3", "v2.0.0-alpha.4", false}, - {"v2.0.0", "v2.0.0-alpha.4", false}, - {"v2.0.0-alpha.4", "v2.0.0", false}, - {"v2.0.0", "v2.0.1", true}, - {"v2.0.1", "v2.0.0", true}, - {"v2.0.0", "v2.1.1", true}, - {"v2.1.0", "v2.0.1", false}, - } - - for _, tt := range tests { - if IsCompatible(tt.client, tt.server) != tt.expected { - t.Errorf("expected client(%s) and server(%s) to be %v", tt.client, tt.server, tt.expected) - } - } -} - -func TestIsCompatibleRange(t *testing.T) { - tests := []struct { - constraint string - ver string - expected bool - }{ - {"v2.0.0-alpha.4", "v2.0.0-alpha.4", true}, - {"v2.0.0-alpha.3", "v2.0.0-alpha.4", false}, - {"v2.0.0", "v2.0.0-alpha.4", false}, - {"v2.0.0-alpha.4", "v2.0.0", false}, - {"~v2.0.0", "v2.0.1", true}, - {"v2", "v2.0.0", true}, - {">2.0.0", "v2.1.1", true}, - {"v2.1.*", "v2.1.1", true}, - } - - for _, tt := range tests { - if IsCompatibleRange(tt.constraint, tt.ver) != tt.expected { - t.Errorf("expected constraint %s to be %v for %s", tt.constraint, tt.expected, tt.ver) - } - } -} diff --git a/pkg/version/doc.go b/pkg/version/doc.go deleted file mode 100644 index 23c9e500d57..00000000000 --- a/pkg/version/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -/* -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 version represents the current version of the project. -package version // import "k8s.io/helm/pkg/version" diff --git a/pkg/version/version.go b/pkg/version/version.go deleted file mode 100644 index 6f5a1a45274..00000000000 --- a/pkg/version/version.go +++ /dev/null @@ -1,54 +0,0 @@ -/* -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 version // import "k8s.io/helm/pkg/version" - -import "k8s.io/helm/pkg/proto/hapi/version" - -var ( - // Version is the current version of the Helm. - // Update this whenever making a new release. - // The version is of the format Major.Minor.Patch[-Prerelease][+BuildMetadata] - // - // Increment major number for new feature additions and behavioral changes. - // Increment minor number for bug fixes and performance enhancements. - // Increment patch number for critical fixes to existing releases. - Version = "v2.8" - - // BuildMetadata is extra build time data - BuildMetadata = "unreleased" - // GitCommit is the git sha1 - GitCommit = "" - // GitTreeState is the state of the git tree - GitTreeState = "" -) - -// GetVersion returns the semver string of the version -func GetVersion() string { - if BuildMetadata == "" { - return Version - } - return Version + "+" + BuildMetadata -} - -// GetVersionProto returns protobuf representing the version -func GetVersionProto() *version.Version { - return &version.Version{ - SemVer: GetVersion(), - GitCommit: GitCommit, - GitTreeState: GitTreeState, - } -} diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go deleted file mode 100644 index e0e4cac0fb4..00000000000 --- a/pkg/version/version_test.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -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 version represents the current version of the project. -package version // import "k8s.io/helm/pkg/version" - -import "testing" -import "k8s.io/helm/pkg/proto/hapi/version" - -func TestGetVersionProto(t *testing.T) { - tests := []struct { - version string - buildMetadata string - gitCommit string - gitTreeState string - expected version.Version - }{ - {"", "", "", "", version.Version{SemVer: "", GitCommit: "", GitTreeState: ""}}, - {"v1.0.0", "", "", "", version.Version{SemVer: "v1.0.0", GitCommit: "", GitTreeState: ""}}, - {"v1.0.0", "79d5c5f7", "", "", version.Version{SemVer: "v1.0.0+79d5c5f7", GitCommit: "", GitTreeState: ""}}, - {"v1.0.0", "79d5c5f7", "0d399baec2acda578a217d1aec8d7d707c71e44d", "", version.Version{SemVer: "v1.0.0+79d5c5f7", GitCommit: "0d399baec2acda578a217d1aec8d7d707c71e44d", GitTreeState: ""}}, - {"v1.0.0", "79d5c5f7", "0d399baec2acda578a217d1aec8d7d707c71e44d", "clean", version.Version{SemVer: "v1.0.0+79d5c5f7", GitCommit: "0d399baec2acda578a217d1aec8d7d707c71e44d", GitTreeState: "clean"}}, - } - for _, tt := range tests { - Version = tt.version - BuildMetadata = tt.buildMetadata - GitCommit = tt.gitCommit - GitTreeState = tt.gitTreeState - if versionProto := GetVersionProto(); *versionProto != tt.expected { - t.Errorf("expected Semver(%s), GitCommit(%s) and GitTreeState(%s) to be %v", tt.expected, tt.gitCommit, tt.gitTreeState, *versionProto) - } - } - -} diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile deleted file mode 100644 index 53757cd8d5e..00000000000 --- a/rootfs/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2016 The Kubernetes Authors. -# -# 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. - -FROM alpine:3.3 - -RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* - -ENV HOME /tmp - -COPY tiller /tiller - -EXPOSE 44134 - -CMD ["/tiller"] - diff --git a/rootfs/Dockerfile.experimental b/rootfs/Dockerfile.experimental deleted file mode 100644 index 990bcde516c..00000000000 --- a/rootfs/Dockerfile.experimental +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2017 The Kubernetes Authors. -# -# 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. - -FROM alpine:3.3 - -RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* - -ENV HOME /tmp - -COPY tiller /tiller - -EXPOSE 44134 - -CMD ["/tiller", "--experimental-release"] - diff --git a/rootfs/Dockerfile.rudder b/rootfs/Dockerfile.rudder deleted file mode 100644 index 6bb3a2d9236..00000000000 --- a/rootfs/Dockerfile.rudder +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2017 The Kubernetes Authors. -# -# 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. - -FROM alpine:3.3 - -RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/* - -ENV HOME /tmp - -COPY rudder /rudder - -EXPOSE 10001 - -CMD ["/rudder"] diff --git a/rootfs/README.md b/rootfs/README.md deleted file mode 100644 index cf8b2e61e74..00000000000 --- a/rootfs/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# RootFS - -This directory stores all files that should be copied to the rootfs of a -Docker container. The files should be stored according to the correct -directory structure of the destination container. For example: - -``` -rootfs/bin -> /bin -rootfs/usr/local/share -> /usr/local/share -``` - -## Dockerfile - -A Dockerfile in the rootfs is used to build the image. Where possible, -compilation should not be done in this Dockerfile, since we are -interested in deploying the smallest possible images. - -Example: - -```Dockerfile -FROM alpine:3.2 - -COPY . / - -ENTRYPOINT ["/usr/local/bin/boot"] -``` - diff --git a/scripts/completions.bash b/scripts/completions.bash deleted file mode 100644 index c24f3d25730..00000000000 --- a/scripts/completions.bash +++ /dev/null @@ -1,1677 +0,0 @@ -# bash completion for helm -*- shell-script -*- - -__debug() -{ - if [[ -n ${BASH_COMP_DEBUG_FILE} ]]; then - echo "$*" >> "${BASH_COMP_DEBUG_FILE}" - fi -} - -# Homebrew on Macs have version 1.3 of bash-completion which doesn't include -# _init_completion. This is a very minimal version of that function. -__my_init_completion() -{ - COMPREPLY=() - _get_comp_words_by_ref "$@" cur prev words cword -} - -__index_of_word() -{ - local w word=$1 - shift - index=0 - for w in "$@"; do - [[ $w = "$word" ]] && return - index=$((index+1)) - done - index=-1 -} - -__contains_word() -{ - local w word=$1; shift - for w in "$@"; do - [[ $w = "$word" ]] && return - done - return 1 -} - -__handle_reply() -{ - __debug "${FUNCNAME[0]}" - case $cur in - -*) - if [[ $(type -t compopt) = "builtin" ]]; then - compopt -o nospace - fi - local allflags - if [ ${#must_have_one_flag[@]} -ne 0 ]; then - allflags=("${must_have_one_flag[@]}") - else - allflags=("${flags[*]} ${two_word_flags[*]}") - fi - COMPREPLY=( $(compgen -W "${allflags[*]}" -- "$cur") ) - if [[ $(type -t compopt) = "builtin" ]]; then - [[ "${COMPREPLY[0]}" == *= ]] || compopt +o nospace - fi - - # complete after --flag=abc - if [[ $cur == *=* ]]; then - if [[ $(type -t compopt) = "builtin" ]]; then - compopt +o nospace - fi - - local index flag - flag="${cur%%=*}" - __index_of_word "${flag}" "${flags_with_completion[@]}" - if [[ ${index} -ge 0 ]]; then - COMPREPLY=() - PREFIX="" - cur="${cur#*=}" - ${flags_completion[${index}]} - if [ -n "${ZSH_VERSION}" ]; then - # zfs completion needs --flag= prefix - eval "COMPREPLY=( \"\${COMPREPLY[@]/#/${flag}=}\" )" - fi - fi - fi - return 0; - ;; - esac - - # check if we are handling a flag with special work handling - local index - __index_of_word "${prev}" "${flags_with_completion[@]}" - if [[ ${index} -ge 0 ]]; then - ${flags_completion[${index}]} - return - fi - - # we are parsing a flag and don't have a special handler, no completion - if [[ ${cur} != "${words[cword]}" ]]; then - return - fi - - local completions - completions=("${commands[@]}") - if [[ ${#must_have_one_noun[@]} -ne 0 ]]; then - completions=("${must_have_one_noun[@]}") - fi - if [[ ${#must_have_one_flag[@]} -ne 0 ]]; then - completions+=("${must_have_one_flag[@]}") - fi - COMPREPLY=( $(compgen -W "${completions[*]}" -- "$cur") ) - - if [[ ${#COMPREPLY[@]} -eq 0 && ${#noun_aliases[@]} -gt 0 && ${#must_have_one_noun[@]} -ne 0 ]]; then - COMPREPLY=( $(compgen -W "${noun_aliases[*]}" -- "$cur") ) - fi - - if [[ ${#COMPREPLY[@]} -eq 0 ]]; then - declare -F __custom_func >/dev/null && __custom_func - fi - - __ltrim_colon_completions "$cur" -} - -# The arguments should be in the form "ext1|ext2|extn" -__handle_filename_extension_flag() -{ - local ext="$1" - _filedir "@(${ext})" -} - -__handle_subdirs_in_dir_flag() -{ - local dir="$1" - pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 -} - -__handle_flag() -{ - __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" - - # if a command required a flag, and we found it, unset must_have_one_flag() - local flagname=${words[c]} - local flagvalue - # if the word contained an = - if [[ ${words[c]} == *"="* ]]; then - flagvalue=${flagname#*=} # take in as flagvalue after the = - flagname=${flagname%%=*} # strip everything after the = - flagname="${flagname}=" # but put the = back - fi - __debug "${FUNCNAME[0]}: looking for ${flagname}" - if __contains_word "${flagname}" "${must_have_one_flag[@]}"; then - must_have_one_flag=() - fi - - # if you set a flag which only applies to this command, don't show subcommands - if __contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then - commands=() - fi - - # keep flag value with flagname as flaghash - if [ -n "${flagvalue}" ] ; then - flaghash[${flagname}]=${flagvalue} - elif [ -n "${words[ $((c+1)) ]}" ] ; then - flaghash[${flagname}]=${words[ $((c+1)) ]} - else - flaghash[${flagname}]="true" # pad "true" for bool flag - fi - - # skip the argument to a two word flag - if __contains_word "${words[c]}" "${two_word_flags[@]}"; then - c=$((c+1)) - # if we are looking for a flags value, don't show commands - if [[ $c -eq $cword ]]; then - commands=() - fi - fi - - c=$((c+1)) - -} - -__handle_noun() -{ - __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" - - if __contains_word "${words[c]}" "${must_have_one_noun[@]}"; then - must_have_one_noun=() - elif __contains_word "${words[c]}" "${noun_aliases[@]}"; then - must_have_one_noun=() - fi - - nouns+=("${words[c]}") - c=$((c+1)) -} - -__handle_command() -{ - __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" - - local next_command - if [[ -n ${last_command} ]]; then - next_command="_${last_command}_${words[c]//:/__}" - else - if [[ $c -eq 0 ]]; then - next_command="_$(basename "${words[c]//:/__}")" - else - next_command="_${words[c]//:/__}" - fi - fi - c=$((c+1)) - __debug "${FUNCNAME[0]}: looking for ${next_command}" - declare -F $next_command >/dev/null && $next_command -} - -__handle_word() -{ - if [[ $c -ge $cword ]]; then - __handle_reply - return - fi - __debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}" - if [[ "${words[c]}" == -* ]]; then - __handle_flag - elif __contains_word "${words[c]}" "${commands[@]}"; then - __handle_command - elif [[ $c -eq 0 ]] && __contains_word "$(basename "${words[c]}")" "${commands[@]}"; then - __handle_command - else - __handle_noun - fi - __handle_word -} - -_helm_completion() -{ - last_command="helm_completion" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - must_have_one_noun+=("bash") - must_have_one_noun+=("zsh") - noun_aliases=() -} - -_helm_create() -{ - last_command="helm_create" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--starter=") - two_word_flags+=("-p") - local_nonpersistent_flags+=("--starter=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_delete() -{ - last_command="helm_delete" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--dry-run") - local_nonpersistent_flags+=("--dry-run") - flags+=("--no-hooks") - local_nonpersistent_flags+=("--no-hooks") - flags+=("--purge") - local_nonpersistent_flags+=("--purge") - flags+=("--timeout=") - local_nonpersistent_flags+=("--timeout=") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_dependency_build() -{ - last_command="helm_dependency_build" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--keyring=") - local_nonpersistent_flags+=("--keyring=") - flags+=("--verify") - local_nonpersistent_flags+=("--verify") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_dependency_list() -{ - last_command="helm_dependency_list" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_dependency_update() -{ - last_command="helm_dependency_update" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--keyring=") - local_nonpersistent_flags+=("--keyring=") - flags+=("--skip-refresh") - local_nonpersistent_flags+=("--skip-refresh") - flags+=("--verify") - local_nonpersistent_flags+=("--verify") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_dependency() -{ - last_command="helm_dependency" - commands=() - commands+=("build") - commands+=("list") - commands+=("update") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_fetch() -{ - last_command="helm_fetch" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--ca-file=") - local_nonpersistent_flags+=("--ca-file=") - flags+=("--cert-file=") - local_nonpersistent_flags+=("--cert-file=") - flags+=("--destination=") - two_word_flags+=("-d") - local_nonpersistent_flags+=("--destination=") - flags+=("--devel") - local_nonpersistent_flags+=("--devel") - flags+=("--key-file=") - local_nonpersistent_flags+=("--key-file=") - flags+=("--keyring=") - local_nonpersistent_flags+=("--keyring=") - flags+=("--prov") - local_nonpersistent_flags+=("--prov") - flags+=("--repo=") - local_nonpersistent_flags+=("--repo=") - flags+=("--untar") - local_nonpersistent_flags+=("--untar") - flags+=("--untardir=") - local_nonpersistent_flags+=("--untardir=") - flags+=("--verify") - local_nonpersistent_flags+=("--verify") - flags+=("--version=") - local_nonpersistent_flags+=("--version=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_get_hooks() -{ - last_command="helm_get_hooks" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--revision=") - local_nonpersistent_flags+=("--revision=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_get_manifest() -{ - last_command="helm_get_manifest" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--revision=") - local_nonpersistent_flags+=("--revision=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_get_values() -{ - last_command="helm_get_values" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--all") - flags+=("-a") - local_nonpersistent_flags+=("--all") - flags+=("--revision=") - local_nonpersistent_flags+=("--revision=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_get() -{ - last_command="helm_get" - commands=() - commands+=("hooks") - commands+=("manifest") - commands+=("values") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--revision=") - local_nonpersistent_flags+=("--revision=") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_history() -{ - last_command="helm_history" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--max=") - local_nonpersistent_flags+=("--max=") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_home() -{ - last_command="helm_home" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_init() -{ - last_command="helm_init" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--canary-image") - local_nonpersistent_flags+=("--canary-image") - flags+=("--client-only") - flags+=("-c") - local_nonpersistent_flags+=("--client-only") - flags+=("--dry-run") - local_nonpersistent_flags+=("--dry-run") - flags+=("--local-repo-url=") - local_nonpersistent_flags+=("--local-repo-url=") - flags+=("--net-host") - local_nonpersistent_flags+=("--net-host") - flags+=("--service-account=") - local_nonpersistent_flags+=("--service-account=") - flags+=("--skip-refresh") - local_nonpersistent_flags+=("--skip-refresh") - flags+=("--stable-repo-url=") - local_nonpersistent_flags+=("--stable-repo-url=") - flags+=("--tiller-image=") - two_word_flags+=("-i") - local_nonpersistent_flags+=("--tiller-image=") - flags+=("--tiller-tls") - local_nonpersistent_flags+=("--tiller-tls") - flags+=("--tiller-tls-cert=") - local_nonpersistent_flags+=("--tiller-tls-cert=") - flags+=("--tiller-tls-key=") - local_nonpersistent_flags+=("--tiller-tls-key=") - flags+=("--tiller-tls-verify") - local_nonpersistent_flags+=("--tiller-tls-verify") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--upgrade") - local_nonpersistent_flags+=("--upgrade") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_inspect_chart() -{ - last_command="helm_inspect_chart" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--ca-file=") - local_nonpersistent_flags+=("--ca-file=") - flags+=("--cert-file=") - local_nonpersistent_flags+=("--cert-file=") - flags+=("--key-file=") - local_nonpersistent_flags+=("--key-file=") - flags+=("--keyring=") - local_nonpersistent_flags+=("--keyring=") - flags+=("--repo=") - local_nonpersistent_flags+=("--repo=") - flags+=("--verify") - local_nonpersistent_flags+=("--verify") - flags+=("--version=") - local_nonpersistent_flags+=("--version=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_inspect_values() -{ - last_command="helm_inspect_values" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--ca-file=") - local_nonpersistent_flags+=("--ca-file=") - flags+=("--cert-file=") - local_nonpersistent_flags+=("--cert-file=") - flags+=("--key-file=") - local_nonpersistent_flags+=("--key-file=") - flags+=("--keyring=") - local_nonpersistent_flags+=("--keyring=") - flags+=("--repo=") - local_nonpersistent_flags+=("--repo=") - flags+=("--verify") - local_nonpersistent_flags+=("--verify") - flags+=("--version=") - local_nonpersistent_flags+=("--version=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_inspect() -{ - last_command="helm_inspect" - commands=() - commands+=("chart") - commands+=("values") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--ca-file=") - local_nonpersistent_flags+=("--ca-file=") - flags+=("--cert-file=") - local_nonpersistent_flags+=("--cert-file=") - flags+=("--key-file=") - local_nonpersistent_flags+=("--key-file=") - flags+=("--keyring=") - local_nonpersistent_flags+=("--keyring=") - flags+=("--repo=") - local_nonpersistent_flags+=("--repo=") - flags+=("--verify") - local_nonpersistent_flags+=("--verify") - flags+=("--version=") - local_nonpersistent_flags+=("--version=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_install() -{ - last_command="helm_install" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--ca-file=") - local_nonpersistent_flags+=("--ca-file=") - flags+=("--cert-file=") - local_nonpersistent_flags+=("--cert-file=") - flags+=("--devel") - local_nonpersistent_flags+=("--devel") - flags+=("--dry-run") - local_nonpersistent_flags+=("--dry-run") - flags+=("--key-file=") - local_nonpersistent_flags+=("--key-file=") - flags+=("--keyring=") - local_nonpersistent_flags+=("--keyring=") - flags+=("--name=") - two_word_flags+=("-n") - local_nonpersistent_flags+=("--name=") - flags+=("--name-template=") - local_nonpersistent_flags+=("--name-template=") - flags+=("--namespace=") - local_nonpersistent_flags+=("--namespace=") - flags+=("--no-hooks") - local_nonpersistent_flags+=("--no-hooks") - flags+=("--replace") - local_nonpersistent_flags+=("--replace") - flags+=("--repo=") - local_nonpersistent_flags+=("--repo=") - flags+=("--set=") - local_nonpersistent_flags+=("--set=") - flags+=("--set-string=") - local_nonpersistent_flags+=("--set-string=") - flags+=("--timeout=") - local_nonpersistent_flags+=("--timeout=") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--values=") - two_word_flags+=("-f") - local_nonpersistent_flags+=("--values=") - flags+=("--verify") - local_nonpersistent_flags+=("--verify") - flags+=("--version=") - local_nonpersistent_flags+=("--version=") - flags+=("--wait") - local_nonpersistent_flags+=("--wait") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_lint() -{ - last_command="helm_lint" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--strict") - local_nonpersistent_flags+=("--strict") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_list() -{ - last_command="helm_list" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--all") - local_nonpersistent_flags+=("--all") - flags+=("--date") - flags+=("-d") - local_nonpersistent_flags+=("--date") - flags+=("--deleted") - local_nonpersistent_flags+=("--deleted") - flags+=("--deleting") - local_nonpersistent_flags+=("--deleting") - flags+=("--deployed") - local_nonpersistent_flags+=("--deployed") - flags+=("--failed") - local_nonpersistent_flags+=("--failed") - flags+=("--max=") - two_word_flags+=("-m") - local_nonpersistent_flags+=("--max=") - flags+=("--namespace=") - local_nonpersistent_flags+=("--namespace=") - flags+=("--offset=") - two_word_flags+=("-o") - local_nonpersistent_flags+=("--offset=") - flags+=("--reverse") - flags+=("-r") - local_nonpersistent_flags+=("--reverse") - flags+=("--short") - flags+=("-q") - local_nonpersistent_flags+=("--short") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_package() -{ - last_command="helm_package" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--destination=") - two_word_flags+=("-d") - local_nonpersistent_flags+=("--destination=") - flags+=("--key=") - local_nonpersistent_flags+=("--key=") - flags+=("--keyring=") - local_nonpersistent_flags+=("--keyring=") - flags+=("--save") - local_nonpersistent_flags+=("--save") - flags+=("--sign") - local_nonpersistent_flags+=("--sign") - flags+=("--version=") - local_nonpersistent_flags+=("--version=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_plugin_install() -{ - last_command="helm_plugin_install" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--version=") - local_nonpersistent_flags+=("--version=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_plugin_list() -{ - last_command="helm_plugin_list" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_plugin_remove() -{ - last_command="helm_plugin_remove" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_plugin_update() -{ - last_command="helm_plugin_update" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_plugin() -{ - last_command="helm_plugin" - commands=() - commands+=("install") - commands+=("list") - commands+=("remove") - commands+=("update") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_repo_add() -{ - last_command="helm_repo_add" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--ca-file=") - local_nonpersistent_flags+=("--ca-file=") - flags+=("--cert-file=") - local_nonpersistent_flags+=("--cert-file=") - flags+=("--key-file=") - local_nonpersistent_flags+=("--key-file=") - flags+=("--no-update") - local_nonpersistent_flags+=("--no-update") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_repo_index() -{ - last_command="helm_repo_index" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--merge=") - local_nonpersistent_flags+=("--merge=") - flags+=("--url=") - local_nonpersistent_flags+=("--url=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_repo_list() -{ - last_command="helm_repo_list" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_repo_remove() -{ - last_command="helm_repo_remove" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_repo_update() -{ - last_command="helm_repo_update" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_repo() -{ - last_command="helm_repo" - commands=() - commands+=("add") - commands+=("index") - commands+=("list") - commands+=("remove") - commands+=("update") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_reset() -{ - last_command="helm_reset" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--force") - flags+=("-f") - local_nonpersistent_flags+=("--force") - flags+=("--remove-helm-home") - local_nonpersistent_flags+=("--remove-helm-home") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_rollback() -{ - last_command="helm_rollback" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--dry-run") - local_nonpersistent_flags+=("--dry-run") - flags+=("--force") - local_nonpersistent_flags+=("--force") - flags+=("--no-hooks") - local_nonpersistent_flags+=("--no-hooks") - flags+=("--recreate-pods") - local_nonpersistent_flags+=("--recreate-pods") - flags+=("--timeout=") - local_nonpersistent_flags+=("--timeout=") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--wait") - local_nonpersistent_flags+=("--wait") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_search() -{ - last_command="helm_search" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--regexp") - flags+=("-r") - local_nonpersistent_flags+=("--regexp") - flags+=("--version=") - two_word_flags+=("-v") - local_nonpersistent_flags+=("--version=") - flags+=("--versions") - flags+=("-l") - local_nonpersistent_flags+=("--versions") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_serve() -{ - last_command="helm_serve" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--address=") - local_nonpersistent_flags+=("--address=") - flags+=("--repo-path=") - local_nonpersistent_flags+=("--repo-path=") - flags+=("--url=") - local_nonpersistent_flags+=("--url=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_status() -{ - last_command="helm_status" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--revision=") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_test() -{ - last_command="helm_test" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--cleanup") - local_nonpersistent_flags+=("--cleanup") - flags+=("--timeout=") - local_nonpersistent_flags+=("--timeout=") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_upgrade() -{ - last_command="helm_upgrade" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--ca-file=") - local_nonpersistent_flags+=("--ca-file=") - flags+=("--cert-file=") - local_nonpersistent_flags+=("--cert-file=") - flags+=("--devel") - local_nonpersistent_flags+=("--devel") - flags+=("--disable-hooks") - local_nonpersistent_flags+=("--disable-hooks") - flags+=("--dry-run") - local_nonpersistent_flags+=("--dry-run") - flags+=("--force") - local_nonpersistent_flags+=("--force") - flags+=("--install") - flags+=("-i") - local_nonpersistent_flags+=("--install") - flags+=("--key-file=") - local_nonpersistent_flags+=("--key-file=") - flags+=("--keyring=") - local_nonpersistent_flags+=("--keyring=") - flags+=("--namespace=") - local_nonpersistent_flags+=("--namespace=") - flags+=("--no-hooks") - local_nonpersistent_flags+=("--no-hooks") - flags+=("--recreate-pods") - local_nonpersistent_flags+=("--recreate-pods") - flags+=("--repo=") - local_nonpersistent_flags+=("--repo=") - flags+=("--reset-values") - local_nonpersistent_flags+=("--reset-values") - flags+=("--reuse-values") - local_nonpersistent_flags+=("--reuse-values") - flags+=("--set=") - local_nonpersistent_flags+=("--set=") - flags+=("--timeout=") - local_nonpersistent_flags+=("--timeout=") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--values=") - two_word_flags+=("-f") - local_nonpersistent_flags+=("--values=") - flags+=("--verify") - local_nonpersistent_flags+=("--verify") - flags+=("--version=") - local_nonpersistent_flags+=("--version=") - flags+=("--wait") - local_nonpersistent_flags+=("--wait") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_verify() -{ - last_command="helm_verify" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--keyring=") - local_nonpersistent_flags+=("--keyring=") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm_version() -{ - last_command="helm_version" - commands=() - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--client") - flags+=("-c") - local_nonpersistent_flags+=("--client") - flags+=("--server") - flags+=("-s") - local_nonpersistent_flags+=("--server") - flags+=("--short") - local_nonpersistent_flags+=("--short") - flags+=("--tls") - local_nonpersistent_flags+=("--tls") - flags+=("--tls-ca-cert=") - local_nonpersistent_flags+=("--tls-ca-cert=") - flags+=("--tls-cert=") - local_nonpersistent_flags+=("--tls-cert=") - flags+=("--tls-key=") - local_nonpersistent_flags+=("--tls-key=") - flags+=("--tls-verify") - local_nonpersistent_flags+=("--tls-verify") - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -_helm() -{ - last_command="helm" - commands=() - commands+=("completion") - commands+=("create") - commands+=("delete") - commands+=("dependency") - commands+=("fetch") - commands+=("get") - commands+=("history") - commands+=("home") - commands+=("init") - commands+=("inspect") - commands+=("install") - commands+=("lint") - commands+=("list") - commands+=("package") - commands+=("plugin") - commands+=("repo") - commands+=("reset") - commands+=("rollback") - commands+=("search") - commands+=("serve") - commands+=("status") - commands+=("test") - commands+=("upgrade") - commands+=("verify") - commands+=("version") - - flags=() - two_word_flags=() - local_nonpersistent_flags=() - flags_with_completion=() - flags_completion=() - - flags+=("--debug") - flags+=("--home=") - flags+=("--host=") - flags+=("--kube-context=") - flags+=("--tiller-namespace=") - - must_have_one_flag=() - must_have_one_noun=() - noun_aliases=() -} - -__start_helm() -{ - local cur prev words cword - declare -A flaghash 2>/dev/null || : - if declare -F _init_completion >/dev/null 2>&1; then - _init_completion -s || return - else - __my_init_completion -n "=" || return - fi - - local c=0 - local flags=() - local two_word_flags=() - local local_nonpersistent_flags=() - local flags_with_completion=() - local flags_completion=() - local commands=("helm") - local must_have_one_flag=() - local must_have_one_noun=() - local last_command - local nouns=() - - __handle_word -} - -if [[ $(type -t compopt) = "builtin" ]]; then - complete -o default -F __start_helm helm -else - complete -o default -o nospace -F __start_helm helm -fi - -# ex: ts=4 sw=4 et filetype=sh diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 1863d583587..bdbfaa9918c 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. +# Copyright The Helm Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,11 +20,12 @@ covermode=${COVERMODE:-atomic} coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX) profile="${coverdir}/cover.out" +pushd / hash goveralls 2>/dev/null || go get github.com/mattn/goveralls -hash godir 2>/dev/null || go get github.com/Masterminds/godir +popd generate_cover_data() { - for d in $(godir) ; do + for d in $(go list ./...) ; do ( local output="${coverdir}/${d//\//-}.cover" go test -coverprofile="${output}" -covermode="$covermode" "$d" diff --git a/scripts/get b/scripts/get index 943ca415811..38c7912234f 100755 --- a/scripts/get +++ b/scripts/get @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. +# Copyright The Helm Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,9 @@ # the package manager for Go: https://github.com/Masterminds/glide.sh/blob/master/get PROJECT_NAME="helm" +TILLER_NAME="tiller" +: ${USE_SUDO:="true"} : ${HELM_INSTALL_DIR:="/usr/local/bin"} # initArch discovers the architecture for this system. @@ -27,7 +29,7 @@ initArch() { case $ARCH in armv5*) ARCH="armv5";; armv6*) ARCH="armv6";; - armv7*) ARCH="armv7";; + armv7*) ARCH="arm";; aarch64) ARCH="arm64";; x86) ARCH="386";; x86_64) ARCH="amd64";; @@ -48,22 +50,20 @@ initOS() { # runs the given command as root (detects if we are root already) runAsRoot() { - local CMD="$*" - - if [ $EUID -ne 0 ]; then - CMD="sudo $CMD" + if [ $EUID -ne 0 -a "$USE_SUDO" = "true" ]; then + sudo "${@}" + else + "${@}" fi - - $CMD } # verifySupported checks that the os/arch combination is supported for # binary builds. verifySupported() { - local supported="darwin-386\ndarwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nwindows-386\nwindows-amd64" + local supported="darwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nwindows-amd64" if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then echo "No prebuilt binary for ${OS}-${ARCH}." - echo "To build from source, go to https://github.com/kubernetes/helm" + echo "To build from source, go to https://github.com/helm/helm" exit 1 fi @@ -75,16 +75,17 @@ verifySupported() { # checkDesiredVersion checks if the desired version is available. checkDesiredVersion() { - # Use the GitHub releases webpage for the project to find the desired version for this project. - local release_url="https://github.com/kubernetes/helm/releases/${DESIRED_VERSION:-latest}" - if type "curl" > /dev/null; then - TAG=$(curl -SsL $release_url | awk '/\/tag\//' | grep -v no-underline | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') - elif type "wget" > /dev/null; then - TAG=$(wget -q -O - $release_url | awk '/\/tag\//' | grep -v no-underline | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') - fi - if [ "x$TAG" == "x" ]; then - echo "Cannot determine ${DESIRED_VERSION} tag." - exit 1 + if [ "x$DESIRED_VERSION" == "x" ]; then + # Get tag from release URL + local release_url="https://github.com/helm/helm/releases" + if type "curl" > /dev/null; then + + TAG=$(curl -Ls $release_url | grep 'href="/helm/helm/releases/tag/v2.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') + elif type "wget" > /dev/null; then + TAG=$(wget $release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v2.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') + fi + else + TAG=$DESIRED_VERSION fi } @@ -92,7 +93,7 @@ checkDesiredVersion() { # if it needs to be changed. checkHelmInstalledVersion() { if [[ -f "${HELM_INSTALL_DIR}/${PROJECT_NAME}" ]]; then - local version=$(helm version | grep '^Client' | cut -d'"' -f2) + local version=$("${HELM_INSTALL_DIR}/${PROJECT_NAME}" version -c | grep '^Client' | cut -d'"' -f2) if [[ "$version" == "$TAG" ]]; then echo "Helm ${version} is already ${DESIRED_VERSION:-latest}" return 0 @@ -109,7 +110,7 @@ checkHelmInstalledVersion() { # for that binary. downloadFile() { HELM_DIST="helm-$TAG-$OS-$ARCH.tar.gz" - DOWNLOAD_URL="https://kubernetes-helm.storage.googleapis.com/$HELM_DIST" + DOWNLOAD_URL="https://get.helm.sh/$HELM_DIST" CHECKSUM_URL="$DOWNLOAD_URL.sha256" HELM_TMP_ROOT="$(mktemp -dt helm-installer-XXXXXX)" HELM_TMP_FILE="$HELM_TMP_ROOT/$HELM_DIST" @@ -141,8 +142,16 @@ installFile() { mkdir -p "$HELM_TMP" tar xf "$HELM_TMP_FILE" -C "$HELM_TMP" HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/$PROJECT_NAME" - echo "Preparing to install into ${HELM_INSTALL_DIR}" + TILLER_TMP_BIN="$HELM_TMP/$OS-$ARCH/$TILLER_NAME" + echo "Preparing to install $PROJECT_NAME and $TILLER_NAME into ${HELM_INSTALL_DIR}" runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR" + echo "$PROJECT_NAME installed into $HELM_INSTALL_DIR/$PROJECT_NAME" + if [ -x "$TILLER_TMP_BIN" ]; then + runAsRoot cp "$TILLER_TMP_BIN" "$HELM_INSTALL_DIR" + echo "$TILLER_NAME installed into $HELM_INSTALL_DIR/$TILLER_NAME" + else + echo "info: $TILLER_NAME binary was not found in this release; skipping $TILLER_NAME installation" + fi } # fail_trap is executed if an error occurs. @@ -155,7 +164,7 @@ fail_trap() { else echo "Failed to install $PROJECT_NAME" fi - echo -e "\tFor support, go to https://github.com/kubernetes/helm." + echo -e "\tFor support, go to https://github.com/helm/helm." fi cleanup exit $result @@ -164,8 +173,7 @@ fail_trap() { # testVersion tests the installed client to make sure it is working. testVersion() { set +e - echo "$PROJECT_NAME installed into $HELM_INSTALL_DIR/$PROJECT_NAME" - HELM="$(which $PROJECT_NAME)" + HELM="$(command -v $PROJECT_NAME)" if [ "$?" = "1" ]; then echo "$PROJECT_NAME not found. Is $HELM_INSTALL_DIR on your "'$PATH?' exit 1 @@ -178,11 +186,12 @@ testVersion() { help () { echo "Accepted cli arguments are:" echo -e "\t[--help|-h ] ->> prints this help" - echo -e "\t[--version|-v ] . When not defined it defaults to latest" + echo -e "\t[--version|-v ]" echo -e "\te.g. --version v2.4.0 or -v latest" + echo -e "\t[--no-sudo] ->> install without sudo" } -# cleanup temporary files to avoid https://github.com/kubernetes/helm/issues/2977 +# cleanup temporary files to avoid https://github.com/helm/helm/issues/2977 cleanup() { if [[ -d "${HELM_TMP_ROOT:-}" ]]; then rm -rf "$HELM_TMP_ROOT" @@ -209,6 +218,9 @@ while [[ $# -gt 0 ]]; do exit 0 fi ;; + '--no-sudo') + USE_SUDO="false" + ;; '--help'|-h) help exit 0 diff --git a/scripts/get-helm-3 b/scripts/get-helm-3 new file mode 100755 index 00000000000..90f103451d1 --- /dev/null +++ b/scripts/get-helm-3 @@ -0,0 +1,326 @@ +#!/usr/bin/env bash + +# Copyright The Helm Authors. +# +# 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. + +# The install script is based off of the MIT-licensed script from glide, +# the package manager for Go: https://github.com/Masterminds/glide.sh/blob/master/get + +: ${BINARY_NAME:="helm"} +: ${USE_SUDO:="true"} +: ${DEBUG:="false"} +: ${VERIFY_CHECKSUM:="true"} +: ${VERIFY_SIGNATURES:="false"} +: ${HELM_INSTALL_DIR:="/usr/local/bin"} +: ${GPG_PUBRING:="pubring.kbx"} + +HAS_CURL="$(type "curl" &> /dev/null && echo true || echo false)" +HAS_WGET="$(type "wget" &> /dev/null && echo true || echo false)" +HAS_OPENSSL="$(type "openssl" &> /dev/null && echo true || echo false)" +HAS_GPG="$(type "gpg" &> /dev/null && echo true || echo false)" + +# initArch discovers the architecture for this system. +initArch() { + ARCH=$(uname -m) + case $ARCH in + armv5*) ARCH="armv5";; + armv6*) ARCH="armv6";; + armv7*) ARCH="arm";; + aarch64) ARCH="arm64";; + x86) ARCH="386";; + x86_64) ARCH="amd64";; + i686) ARCH="386";; + i386) ARCH="386";; + esac +} + +# initOS discovers the operating system for this system. +initOS() { + OS=$(echo `uname`|tr '[:upper:]' '[:lower:]') + + case "$OS" in + # Minimalist GNU for Windows + mingw*) OS='windows';; + esac +} + +# runs the given command as root (detects if we are root already) +runAsRoot() { + if [ $EUID -ne 0 -a "$USE_SUDO" = "true" ]; then + sudo "${@}" + else + "${@}" + fi +} + +# verifySupported checks that the os/arch combination is supported for +# binary builds, as well whether or not necessary tools are present. +verifySupported() { + local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nwindows-amd64" + if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then + echo "No prebuilt binary for ${OS}-${ARCH}." + echo "To build from source, go to https://github.com/helm/helm" + exit 1 + fi + + if [ "${HAS_CURL}" != "true" ] && [ "${HAS_WGET}" != "true" ]; then + echo "Either curl or wget is required" + exit 1 + fi + + if [ "${VERIFY_CHECKSUM}" == "true" ] && [ "${HAS_OPENSSL}" != "true" ]; then + echo "In order to verify checksum, openssl must first be installed." + echo "Please install openssl or set VERIFY_CHECKSUM=false in your environment." + exit 1 + fi + + if [ "${VERIFY_SIGNATURES}" == "true" ]; then + if [ "${HAS_GPG}" != "true" ]; then + echo "In order to verify signatures, gpg must first be installed." + echo "Please install gpg or set VERIFY_SIGNATURES=false in your environment." + exit 1 + fi + if [ "${OS}" != "linux" ]; then + echo "Signature verification is currently only supported on Linux." + echo "Please set VERIFY_SIGNATURES=false or verify the signatures manually." + exit 1 + fi + fi +} + +# checkDesiredVersion checks if the desired version is available. +checkDesiredVersion() { + if [ "x$DESIRED_VERSION" == "x" ]; then + # Get tag from release URL + local latest_release_url="https://github.com/helm/helm/releases" + if [ "${HAS_CURL}" == "true" ]; then + TAG=$(curl -Ls $latest_release_url | grep 'href="/helm/helm/releases/tag/v3.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') + elif [ "${HAS_WGET}" == "true" ]; then + TAG=$(wget $latest_release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v3.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') + fi + else + TAG=$DESIRED_VERSION + fi +} + +# checkHelmInstalledVersion checks which version of helm is installed and +# if it needs to be changed. +checkHelmInstalledVersion() { + if [[ -f "${HELM_INSTALL_DIR}/${BINARY_NAME}" ]]; then + local version=$("${HELM_INSTALL_DIR}/${BINARY_NAME}" version --template="{{ .Version }}") + if [[ "$version" == "$TAG" ]]; then + echo "Helm ${version} is already ${DESIRED_VERSION:-latest}" + return 0 + else + echo "Helm ${TAG} is available. Changing from version ${version}." + return 1 + fi + else + return 1 + fi +} + +# downloadFile downloads the latest binary package and also the checksum +# for that binary. +downloadFile() { + HELM_DIST="helm-$TAG-$OS-$ARCH.tar.gz" + DOWNLOAD_URL="https://get.helm.sh/$HELM_DIST" + CHECKSUM_URL="$DOWNLOAD_URL.sha256" + HELM_TMP_ROOT="$(mktemp -dt helm-installer-XXXXXX)" + HELM_TMP_FILE="$HELM_TMP_ROOT/$HELM_DIST" + HELM_SUM_FILE="$HELM_TMP_ROOT/$HELM_DIST.sha256" + echo "Downloading $DOWNLOAD_URL" + if [ "${HAS_CURL}" == "true" ]; then + curl -SsL "$CHECKSUM_URL" -o "$HELM_SUM_FILE" + curl -SsL "$DOWNLOAD_URL" -o "$HELM_TMP_FILE" + elif [ "${HAS_WGET}" == "true" ]; then + wget -q -O "$HELM_SUM_FILE" "$CHECKSUM_URL" + wget -q -O "$HELM_TMP_FILE" "$DOWNLOAD_URL" + fi +} + +# verifyFile verifies the SHA256 checksum of the binary package +# and the GPG signatures for both the package and checksum file +# (depending on settings in environment). +verifyFile() { + if [ "${VERIFY_CHECKSUM}" == "true" ]; then + verifyChecksum + fi + if [ "${VERIFY_SIGNATURES}" == "true" ]; then + verifySignatures + fi +} + +# installFile installs the Helm binary. +installFile() { + HELM_TMP="$HELM_TMP_ROOT/$BINARY_NAME" + mkdir -p "$HELM_TMP" + tar xf "$HELM_TMP_FILE" -C "$HELM_TMP" + HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/helm" + echo "Preparing to install $BINARY_NAME into ${HELM_INSTALL_DIR}" + runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR/$BINARY_NAME" + echo "$BINARY_NAME installed into $HELM_INSTALL_DIR/$BINARY_NAME" +} + +# verifyChecksum verifies the SHA256 checksum of the binary package. +verifyChecksum() { + printf "Verifying checksum... " + 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_FILE} does not match. Aborting." + exit 1 + fi + echo "Done." +} + +# verifySignatures obtains the latest KEYS file from GitHub master branch +# as well as the signature .asc files from the specific GitHub release, +# then verifies that the release artifacts were signed by a maintainer's key. +verifySignatures() { + printf "Verifying signatures... " + local keys_filename="KEYS" + local github_keys_url="https://raw.githubusercontent.com/helm/helm/master/${keys_filename}" + if [ "${HAS_CURL}" == "true" ]; then + curl -SsL "${github_keys_url}" -o "${HELM_TMP_ROOT}/${keys_filename}" + elif [ "${HAS_WGET}" == "true" ]; then + wget -q -O "${HELM_TMP_ROOT}/${keys_filename}" "${github_keys_url}" + fi + local gpg_keyring="${HELM_TMP_ROOT}/keyring.gpg" + local gpg_homedir="${HELM_TMP_ROOT}/gnupg" + mkdir -p -m 0700 "${gpg_homedir}" + local gpg_stderr_device="/dev/null" + if [ "${DEBUG}" == "true" ]; then + gpg_stderr_device="/dev/stderr" + fi + gpg --batch --quiet --homedir="${gpg_homedir}" --import "${HELM_TMP_ROOT}/${keys_filename}" 2> "${gpg_stderr_device}" + gpg --batch --no-default-keyring --keyring "${gpg_homedir}/${GPG_PUBRING}" --export > "${gpg_keyring}" + local github_release_url="https://github.com/helm/helm/releases/download/${TAG}" + if [ "${HAS_CURL}" == "true" ]; then + curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" + curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" + elif [ "${HAS_WGET}" == "true" ]; then + wget -q -O "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" + wget -q -O "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" + fi + local error_text="If you think this might be a potential security issue," + error_text="${error_text}\nplease see here: https://github.com/helm/community/blob/master/SECURITY.md" + local num_goodlines_sha=$(gpg --verify --keyring="${gpg_keyring}" --status-fd=1 "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" 2> "${gpg_stderr_device}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)') + if [[ ${num_goodlines_sha} -lt 2 ]]; then + echo "Unable to verify the signature of helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256!" + echo -e "${error_text}" + exit 1 + fi + local num_goodlines_tar=$(gpg --verify --keyring="${gpg_keyring}" --status-fd=1 "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" 2> "${gpg_stderr_device}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)') + if [[ ${num_goodlines_tar} -lt 2 ]]; then + echo "Unable to verify the signature of helm-${TAG}-${OS}-${ARCH}.tar.gz!" + echo -e "${error_text}" + exit 1 + fi + echo "Done." +} + +# fail_trap is executed if an error occurs. +fail_trap() { + result=$? + if [ "$result" != "0" ]; then + if [[ -n "$INPUT_ARGUMENTS" ]]; then + echo "Failed to install $BINARY_NAME with the arguments provided: $INPUT_ARGUMENTS" + help + else + echo "Failed to install $BINARY_NAME" + fi + echo -e "\tFor support, go to https://github.com/helm/helm." + fi + cleanup + exit $result +} + +# testVersion tests the installed client to make sure it is working. +testVersion() { + set +e + HELM="$(command -v $BINARY_NAME)" + if [ "$?" = "1" ]; then + echo "$BINARY_NAME not found. Is $HELM_INSTALL_DIR on your "'$PATH?' + exit 1 + fi + set -e +} + +# help provides possible cli installation arguments +help () { + echo "Accepted cli arguments are:" + echo -e "\t[--help|-h ] ->> prints this help" + echo -e "\t[--version|-v ] . When not defined it fetches the latest release from GitHub" + echo -e "\te.g. --version v3.0.0 or -v canary" + echo -e "\t[--no-sudo] ->> install without sudo" +} + +# cleanup temporary files to avoid https://github.com/helm/helm/issues/2977 +cleanup() { + if [[ -d "${HELM_TMP_ROOT:-}" ]]; then + rm -rf "$HELM_TMP_ROOT" + fi +} + +# Execution + +#Stop execution on any error +trap "fail_trap" EXIT +set -e + +# Set debug if desired +if [ "${DEBUG}" == "true" ]; then + set -x +fi + +# Parsing input arguments (if any) +export INPUT_ARGUMENTS="${@}" +set -u +while [[ $# -gt 0 ]]; do + case $1 in + '--version'|-v) + shift + if [[ $# -ne 0 ]]; then + export DESIRED_VERSION="${1}" + else + echo -e "Please provide the desired version. e.g. --version v3.0.0 or -v canary" + exit 0 + fi + ;; + '--no-sudo') + USE_SUDO="false" + ;; + '--help'|-h) + help + exit 0 + ;; + *) exit 1 + ;; + esac + shift +done +set +u + +initArch +initOS +verifySupported +checkDesiredVersion +if ! checkHelmInstalledVersion; then + downloadFile + verifyFile + installFile +fi +testVersion +cleanup diff --git a/scripts/release-notes.sh b/scripts/release-notes.sh new file mode 100755 index 00000000000..0f6ab4167b5 --- /dev/null +++ b/scripts/release-notes.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash + +# Copyright The Helm Authors. +# +# 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. + +RELEASE=${RELEASE:-$2} +PREVIOUS_RELEASE=${PREVIOUS_RELEASE:-$1} + +## Ensure Correct Usage +if [[ -z "${PREVIOUS_RELEASE}" || -z "${RELEASE}" ]]; then + echo Usage: + echo ./scripts/release-notes.sh v3.0.0 v3.1.0 + echo or + echo PREVIOUS_RELEASE=v3.0.0 + echo RELEASE=v3.1.0 + echo ./scripts/release-notes.sh + exit 1 +fi + +## validate git tags +for tag in $RELEASE $PREVIOUS_RELEASE; do + OK=$(git tag -l ${tag} | wc -l) + if [[ "$OK" == "0" ]]; then + echo ${tag} is not a valid release version + exit 1 + fi +done + +## Check for hints that checksum files were downloaded +## from `make fetch-dist` +if [[ ! -e "./_dist/helm-${RELEASE}-darwin-amd64.tar.gz.sha256sum" ]]; then + echo "checksum file ./_dist/helm-${RELEASE}-darwin-amd64.tar.gz.sha256sum not found in ./_dist/" + echo "Did you forget to run \`make fetch-dist\` first ?" + exit 1 +fi + +## Generate CHANGELOG from git log +CHANGELOG=$(git log --no-merges --pretty=format:'- %s %H (%aN)' ${PREVIOUS_RELEASE}..${RELEASE}) +if [[ ! $? -eq 0 ]]; then + echo "Error creating changelog" + echo "try running \`git log --no-merges --pretty=format:'- %s %H (%aN)' ${PREVIOUS_RELEASE}..${RELEASE}\`" + exit 1 +fi + +## guess at MAJOR / MINOR / PATCH versions +MAJOR=$(echo ${RELEASE} | sed 's/^v//' | cut -f1 -d.) +MINOR=$(echo ${RELEASE} | sed 's/^v//' | cut -f2 -d.) +PATCH=$(echo ${RELEASE} | sed 's/^v//' | cut -f3 -d.) + +## Print release notes to stdout +cat <. Users are encouraged to upgrade for the best experience. + +The community keeps growing, and we'd love to see you there! + +- Join the discussion in [Kubernetes Slack](https://kubernetes.slack.com): + - `#helm-users` for questions and just to hang out + - `#helm-dev` for discussing PRs, code, and bugs +- Hang out at the Public Developer Call: Thursday, 9:30 Pacific via [Zoom](https://zoom.us/j/696660622) +- Test, debug, and contribute charts: [ArtifactHub/packages](https://artifacthub.io/packages/search?kind=0) + +## Notable Changes + +- Add list of +- notable changes here + +## Installation and Upgrading + +Download Helm ${RELEASE}. The common platform binaries are here: + +- [MacOS amd64](https://get.helm.sh/helm-${RELEASE}-darwin-amd64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-darwin-amd64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-darwin-amd64.tar.gz.sha256)) +- [MacOS arm64](https://get.helm.sh/helm-${RELEASE}-darwin-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-darwin-arm64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-darwin-arm64.tar.gz.sha256)) +- [Linux amd64](https://get.helm.sh/helm-${RELEASE}-linux-amd64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-amd64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-amd64.tar.gz.sha256)) +- [Linux arm](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm.tar.gz.sha256)) +- [Linux arm64](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm64.tar.gz.sha256)) +- [Linux i386](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-386.tar.gz.sha256)) +- [Linux ppc64le](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256)) +- [Linux s390x](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-s390x.tar.gz.sha256)) +- [Windows amd64](https://get.helm.sh/helm-${RELEASE}-windows-amd64.zip) ([checksum](https://get.helm.sh/helm-${RELEASE}-windows-amd64.zip.sha256sum) / $(cat _dist/helm-${RELEASE}-windows-amd64.zip.sha256)) + +The [Quickstart Guide](https://helm.sh/docs/intro/quickstart/) will get you going from there. For **upgrade instructions** or detailed installation notes, check the [install guide](https://helm.sh/docs/intro/install/). You can also use a [script to install](https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3) on any system with \`bash\`. + +## What's Next + +- ${MAJOR}.${MINOR}.$(expr ${PATCH} + 1) will contain only bug fixes. +- ${MAJOR}.$(expr ${MINOR} + 1).${PATCH} is the next feature release. This release will focus on ... + +## Changelog + +${CHANGELOG} +EOF diff --git a/scripts/sync-repo.sh b/scripts/sync-repo.sh index 3795b1a7c3f..4531020724c 100755 --- a/scripts/sync-repo.sh +++ b/scripts/sync-repo.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. +# Copyright The Helm Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/update-docs.sh b/scripts/update-docs.sh deleted file mode 100755 index e014b537efd..00000000000 --- a/scripts/update-docs.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2017 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. - -set -euo pipefail - -source scripts/util.sh - -if LANG=C sed --help 2>&1 | grep -q GNU; then - SED="sed" -elif which gsed &>/dev/null; then - SED="gsed" -else - echo "Failed to find GNU sed as sed or gsed. If you are on Mac: brew install gnu-sed." >&2 - exit 1 -fi - -kube::util::ensure-temp-dir - -export HELM_NO_PLUGINS=1 - -# Reset Helm Home because it is used in the generation of docs. -OLD_HELM_HOME=${HELM_HOME:-} -HELM_HOME="$HOME/.helm" -bin/helm init --client-only -mkdir -p ${KUBE_TEMP}/docs/helm -bin/helm docs --dir ${KUBE_TEMP}/docs/helm -HELM_HOME=$OLD_HELM_HOME - -FILES=$(find ${KUBE_TEMP} -type f) - -${SED} -i -e "s:${HOME}:~:" ${FILES} - -for i in ${FILES}; do - ret=0 - truepath=$(echo ${i} | ${SED} "s:${KUBE_TEMP}/::") - diff -NauprB -I 'Auto generated' "${i}" "${truepath}" > /dev/null || ret=$? - if [[ $ret -ne 0 ]]; then - echo "${truepath} changed. Updating.." - cp "${i}" "${truepath}" - fi -done diff --git a/scripts/util.sh b/scripts/util.sh index 09caaf972c5..c1e6c3751d1 100644 --- a/scripts/util.sh +++ b/scripts/util.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. +# Copyright The Helm Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/scripts/validate-go.sh b/scripts/validate-go.sh deleted file mode 100755 index 2ecf5dfb3fe..00000000000 --- a/scripts/validate-go.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/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. -set -euo pipefail - -exit_code=0 - -if ! hash gometalinter.v1 2>/dev/null ; then - go get -u gopkg.in/alecthomas/gometalinter.v1 - gometalinter.v1 --install -fi - -echo -echo "==> Running static validations <==" -# Run linters that should return errors -gometalinter.v1 \ - --disable-all \ - --enable deadcode \ - --severity deadcode:error \ - --enable gofmt \ - --enable ineffassign \ - --enable misspell \ - --enable vet \ - --tests \ - --vendor \ - --deadline 60s \ - ./... || exit_code=1 - -echo -echo "==> Running linters <==" -# Run linters that should return warnings -gometalinter.v1 \ - --disable-all \ - --enable golint \ - --vendor \ - --skip proto \ - --deadline 60s \ - ./... || : - -exit $exit_code diff --git a/scripts/validate-license.sh b/scripts/validate-license.sh index fe7ec481bee..dc247436f9d 100755 --- a/scripts/validate-license.sh +++ b/scripts/validate-license.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2016 The Kubernetes Authors All rights reserved. +# Copyright The Helm Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,18 +20,25 @@ find_files() { find . -not \( \ \( \ -wholename './vendor' \ - -o -wholename './pkg/proto' \ -o -wholename '*testdata*' \ + -o -wholename '*third_party*' \ \) -prune \ \) \ - \( -name '*.go' -o -name '*.sh' -o -name 'Dockerfile' \) + \( -name '*.go' -o -name '*.sh' \) } -failed=($(find_files | xargs grep -L 'Licensed under the Apache License, Version 2.0 (the "License");')) -if (( ${#failed[@]} > 0 )); then +# Use "|| :" to ignore the error code when grep returns empty +failed_license_header=($(find_files | xargs grep -L 'Licensed under the Apache License, Version 2.0 (the "License")' || :)) +if (( ${#failed_license_header[@]} > 0 )); then echo "Some source files are missing license headers." - for f in "${failed[@]}"; do - echo " $f" - done + printf '%s\n' "${failed_license_header[@]}" + exit 1 +fi + +# Use "|| :" to ignore the error code when grep returns empty +failed_copyright_header=($(find_files | xargs grep -L 'Copyright The Helm Authors.' || :)) +if (( ${#failed_copyright_header[@]} > 0 )); then + echo "Some source files are missing the copyright header." + printf '%s\n' "${failed_copyright_header[@]}" exit 1 fi diff --git a/scripts/verify-docs.sh b/scripts/verify-docs.sh deleted file mode 100755 index b0b799eacb0..00000000000 --- a/scripts/verify-docs.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2017 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. - -set -euo pipefail - -source scripts/util.sh - -if LANG=C sed --help 2>&1 | grep -q GNU; then - SED="sed" -elif which gsed &>/dev/null; then - SED="gsed" -else - echo "Failed to find GNU sed as sed or gsed. If you are on Mac: brew install gnu-sed." >&2 - exit 1 -fi - -kube::util::ensure-temp-dir - -export HELM_NO_PLUGINS=1 - -# Reset Helm Home because it is used in the generation of docs. -OLD_HELM_HOME=${HELM_HOME:-} -HELM_HOME="$HOME/.helm" -bin/helm init --client-only -mkdir -p ${KUBE_TEMP}/docs/helm -bin/helm docs --dir ${KUBE_TEMP}/docs/helm -HELM_HOME=$OLD_HELM_HOME - - -FILES=$(find ${KUBE_TEMP} -type f) - -${SED} -i -e "s:${HOME}:~:" ${FILES} -ret=0 -for i in ${FILES}; do - diff -NauprB -I 'Auto generated' ${i} $(echo ${i} | ${SED} "s:${KUBE_TEMP}/::") || ret=$? -done -if [[ $ret -eq 0 ]]; then - echo "helm docs up to date." -else - echo "helm docs are out of date. Please run \"make docs\"" - exit 1 -fi diff --git a/testdata/ca.pem b/testdata/ca.pem deleted file mode 100644 index 79d854a8de0..00000000000 --- a/testdata/ca.pem +++ /dev/null @@ -1,35 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIGADCCA+igAwIBAgIJALbFKeU+io3AMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDTzEQMA4GA1UEBxMHQm91bGRlcjEPMA0GA1UEChMG -VGlsbGVyMQ8wDQYDVQQLEwZUaWxsZXIxDTALBgNVBAMTBEhlbG0wHhcNMTcwNDA0 -MTYwNDQ5WhcNMTgwNDA0MTYwNDQ5WjBdMQswCQYDVQQGEwJVUzELMAkGA1UECBMC -Q08xEDAOBgNVBAcTB0JvdWxkZXIxDzANBgNVBAoTBlRpbGxlcjEPMA0GA1UECxMG -VGlsbGVyMQ0wCwYDVQQDEwRIZWxtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC -CgKCAgEAyFOriVMm3vTeVerwMuBEIt07EJFzAn+5R1eqdNEJ0k08/ZPKPLnhkg+/ -sRZuzah4lbszbAb7frtqtXKT8u28/tsQofCt5M9VZLK21yS4QX1kBS3CvN9mfw4r -S+yzoP/7oFPydwVhSsOZ3kRUrU7jyxZjFMPCLJU5O1WTRA/PEKagjf5Y63q0jhU7 -/VDPazeUKSvfyPW9HxVMLkWYK6hLb2sDoopbeV5L/wPDb66sLuIPcGw25SprzDqq -9OtM2pMG89h1cDhXeH8NJPOVzCkkalqwl+Ytl2alh9HWT8cb0nJ+TKhFtvTpM60U -Ku+H+zLTIaHBIUxKrNiTowBQe4JcHmyYp+IJnZv/l4kH5CkWIX3SIcOACSbLlzWB -QjBCWDtgmT4bdCDtnQF6eTVdMOy76/Yyzj9xLKUEr/fNqE4CtZMEfJdELHsX9hpC -Dq031NgKNZvMd+llv259QWFVltZ+GOctCaT4TlTWRiFYl0ysYnsZ5HbA6eKt810l -rpjtnrKCBenzrHLRCP+BGcfhGlisiutaclUwwgKow8/OV4+9Eg4RTeIhzWIIcfDI -UDgkecNcTPK2VZt4Kj6D2vvWJHqUNpiL1FVekki7FrhkoXR5BOvHfoDqpvl+BTyb -AfBmPyVx9/0zoAdYfpRsMUjVeWtS/oS9UDt2UJojSa1hMhd8pIECAwEAAaOBwjCB -vzAdBgNVHQ4EFgQU7NrQViMsDpfYfVZITtwOuT2J6HYwgY8GA1UdIwSBhzCBhIAU -7NrQViMsDpfYfVZITtwOuT2J6HahYaRfMF0xCzAJBgNVBAYTAlVTMQswCQYDVQQI -EwJDTzEQMA4GA1UEBxMHQm91bGRlcjEPMA0GA1UEChMGVGlsbGVyMQ8wDQYDVQQL -EwZUaWxsZXIxDTALBgNVBAMTBEhlbG2CCQC2xSnlPoqNwDAMBgNVHRMEBTADAQH/ -MA0GCSqGSIb3DQEBCwUAA4ICAQCs+RwppSZugKN+LZ226wf+A86+BEFXNyVQ5all -YgBA4Oiai3O3XGMpNmm60TbumjzVq8PrNNuQxR2VfK/N7qLLJMktIVBntRsiQnTR -Yw/EuhcuvYOhJ7P8RwifkhusZTLI6eQhES5bmUYuXmp887qkr/dN1XmiubTKLDTE -fZAhOVAvA55YgJzEvBkVAXpT5tzrOakjo+PM6NoUcEWQsh3z1RRgFowUi3aKjM7k -J38h5iCJCLlo5Av+bhdw/rP+qw7d6DgKemrxC91qyk48BhTXp3qR3XLmuqjtQq6u -xMPgKNs6/fornWbvCX+vQq9Hncm7X4ZHBdoaWAs5P9lpACuR77/Ad30rY026bM4m -br8VQxWU2qlTt8vfp8jIuiylJP/YU9aMsKc8lIue19As+Llw9t9Zdq3z/Q3xul7N -hXLa/NJeban9iTNgjzPWigSGpaXIFxYZ3fl0flYkMG2KzhuYttHVuWyIJ8WLpsPN -Os9SIkekZipwsCdtL65fCLj5DjAmX6LwnxVf6Z5K9hsOEM+uZvq0qsrLjndxmbrG -+Br+p4jxH8kkUNdoNVlbg1F+0+sgtD9drgSLM4cZ9wVWUl64qbDpQR+/pVlSepiQ -kPTthsGtcrW8sTSMlLY4XpCLcS/hwO4jwNCB+8bLsz/6p9vCDMIkb5zkhjPc/Awe -mlK3dw== ------END CERTIFICATE----- diff --git a/testdata/crt.pem b/testdata/crt.pem index 226b3a71b7b..715cd0f6561 100644 --- a/testdata/crt.pem +++ b/testdata/crt.pem @@ -1,29 +1,73 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 55:31:53:9b:41:72:05:dc:90:49:bd:48:13:7c:59:9e:5a:53:5e:86 + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=CO, L=Boulder, O=Helm, CN=helm.sh + Validity + Not Before: Nov 1 22:51:49 2019 GMT + Not After : Oct 29 22:51:49 2029 GMT + Subject: C=US, ST=CO, L=Boulder, O=Helm, CN=helm.sh + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:c8:89:55:0d:0b:f1:da:e6:c0:70:7d:d3:27:cd: + b8:a8:81:8b:7c:a4:89:e5:d1:b1:78:01:1d:df:44: + 88:0b:fc:d6:81:35:3d:d1:3b:5e:8f:bb:93:b3:7e: + 28:db:ed:ff:a0:13:3a:70:a3:fe:94:6b:0b:fe:fb: + 63:00:b0:cb:dc:81:cd:80:dc:d0:2f:bf:b2:4f:9a: + 81:d4:22:dc:97:c8:8f:27:86:59:91:fa:92:05:75: + c4:cc:6b:f5:a9:6b:74:1e:f5:db:a9:f8:bf:8c:a2: + 25:fd:a0:cc:79:f4:25:57:74:a9:23:9b:e2:b7:22: + 7a:14:7a:3d:ea:f1:7e:32:6b:57:6c:2e:c6:4f:75: + 54:f9:6b:54:d2:ca:eb:54:1c:af:39:15:9b:d0:7c: + 0f:f8:55:51:04:ea:da:fa:7b:8b:63:0f:ac:39:b1: + f6:4b:8e:4e:f6:ea:e9:7b:e6:ba:5e:5a:8e:91:ef: + dc:b1:7d:52:3f:73:83:52:46:83:48:49:ff:f2:2d: + ca:54:f2:36:bb:49:cc:59:99:c0:9e:cf:8e:78:55: + 6c:ed:7d:7e:83:b8:59:2c:7d:f8:1a:81:f0:7d:f5: + 27:f2:db:ae:d4:31:54:38:fe:47:b2:ee:16:20:0f: + f1:db:2d:28:bf:6f:38:eb:11:bb:9a:d4:b2:5a:3a: + 4a:7f + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:helm.sh, IP Address:127.0.0.1 + Signature Algorithm: sha256WithRSAEncryption + 4e:17:27:3d:36:4e:6c:2b:f7:d4:28:33:7e:05:26:7a:42:a0: + 2c:44:57:04:a0:de:df:40:fb:af:70:27:e6:55:20:f1:f8:c0: + 50:63:ab:b8:f1:31:5d:1e:f4:ca:8d:65:0b:d4:5e:5b:77:2f: + 2a:af:74:5f:18:2d:92:29:7f:2d:97:fb:ec:aa:e3:1e:db:b3: + 8d:01:aa:82:1a:f6:28:a8:b3:ee:15:9f:9a:f5:76:37:30:f2: + 3b:38:13:b2:d4:14:94:c6:38:fa:f9:6e:94:e8:1f:11:0b:b0: + 69:1a:b3:f9:f1:27:b4:d2:f5:64:54:7c:8f:e7:83:31:f6:0d: + a7:0e:0e:66:d8:33:2f:e0:a1:93:56:92:58:bf:50:da:56:8e: + db:42:22:f5:0c:6f:f8:4c:ef:f5:7c:2d:a6:b8:60:e4:bb:df: + a3:6c:c2:6b:99:0b:d3:0a:ad:7c:f4:74:72:9a:52:5e:81:d9: + a2:a2:dd:68:38:fb:b7:54:7f:f6:aa:ee:53:de:3d:3a:0e:86: + 53:ad:af:72:db:fb:6b:18:ce:ac:e4:64:70:13:68:da:be:e1: + 6b:46:dd:a0:72:96:9b:3f:ba:cf:11:6e:98:03:0a:69:83:9e: + 37:25:c9:36:b9:68:4f:73:ca:c6:32:5c:be:46:64:bb:a8:cc: + 71:25:8f:be -----BEGIN CERTIFICATE----- -MIIFCDCCAvCgAwIBAgIJAMADBPQSkgPMMA0GCSqGSIb3DQEBCwUAMF0xCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDTzEQMA4GA1UEBxMHQm91bGRlcjEPMA0GA1UEChMG -VGlsbGVyMQ8wDQYDVQQLEwZUaWxsZXIxDTALBgNVBAMTBEhlbG0wHhcNMTcwNDA0 -MTYwNzM4WhcNMTgwNDA0MTYwNzM4WjARMQ8wDQYDVQQDEwZjbGllbnQwggIiMA0G -CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDnyxxZtTKZLOYyEDmo1pY8m6A1tot1 -UuiSxtwp4rNYIaVyCbpdKrNr68q6dRs40vEWGfH415OzFjK3RpbzdSqeB4U+toUl -bIYjf9N4/ZrAjqBO+Xd+JKUkhKcZIbMJHb2kOzqOL7LSWlKcyGCY/x7Tj4qdka9R -QiXB7zVUEqcTa13A+/rdrPWgzK/xGIYh7cCehOixxXSmfcCHR573BDC5j6s9KozA -T84obBgEgsVgu1+d+n1D+cqAr7ppSZTMWs/f+DwwJG/VWblIYsCuN3yNHLaYsL9M -MTw1ogulcRmFNyw9CSXdyVCxGjh/++sQ2f47TpadI+IzknrBkfPL7+zt2IyaORch -uGsdX+IwQl3aZjayMx7YjYSSbQIfpSF9y4KVPz4RHEUn10hsX/8qXPzitbXVLh7p -b9lUMGPHchTm/dd+oZAbL1TUIJQOJn2vGDMKsuBswBg12YNdhAp55EDZx54CCiM2 -sRtlVNTpkatr7Rvd5CDFuLAzwHnrEKTy5EOUrS9aYzqKaGOrMI+k1OCTp3LwLdPX -d7OV9+ZuSLHX6gvF4uAucK8HLp3Visj0GeWL7OzpTv2imjNX5C1wPH7UR6UsF+dg -bzqZOP63e5WR1eEqth5ieE+5jQ8nxvPF//qKHQNlgbD93Y3B3UfmjrnP1chgqFn9 -IAXWFsyZ7I8bXQIDAQABoxcwFTATBgNVHSUEDDAKBggrBgEFBQcDAjANBgkqhkiG -9w0BAQsFAAOCAgEAPIXMQOAgb2VlfS59HrvpdqbIapIfs/xBgPKlNfwNO3UpSYyq -XVK1xekLI+mEE639YP/oSc7HX2OrJi3SX5Ofzs0s9h+BNTXPqw1ju+G34cF8MKc0 -acynThdcI4eZGc2fKSAIw6RN7iIln74Sf4MNmEuQu6Dnq4QkZKAWtnY7Uq5ooFJS -JA+Joqif8SvEvMgq02XdUhjijlBAanxI/xp64k37k18+pHAxcS22HzrjwDQ4ELqY -gBq9g20JYXoUxjBFUfj+cxBx+LBKfPVTpcbicI4wwP4a2BA6LDUHgcnSMhle1zeq -pHuOIOT6XqYLhO0Yr7WRG9Yzuxs0GV4TH+FlDpDHWL8XG0gjDUZ/2viPlKBr+FoN -inW8jqQ2NYMzYF9zHNzXVGK+5oyH4Y7r/8WxQLfdSR/5S1DXPLSkzkYbduHf9UmF -Dvh6NrCGU0UxypA1NvF5o11cnTQ22GPywVSc0ILKWDRlu8DiGq71bYQu8hTTkTnb -2hOr5JHcGaloms7WM3q0hc2PIhwYXw2V3b9I9lbnvv3Y/yKPNN7IzU5No6siRuIH -paj83V0flMWj1EqJMDxk9ECHgDyl/1ftgJVx1G/f/+UnXoRdR2kFqVVeJTeSIZi7 -dSsAOIMN/weZMZF55Q61vgUgYXKp4g2/Zk8BJn0cx9pjEMIw/pc7Eq1x/R8= +MIIDRDCCAiygAwIBAgIUVTFTm0FyBdyQSb1IE3xZnlpTXoYwDQYJKoZIhvcNAQEL +BQAwTTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMRAwDgYDVQQHDAdCb3VsZGVy +MQ0wCwYDVQQKDARIZWxtMRAwDgYDVQQDDAdoZWxtLnNoMB4XDTE5MTEwMTIyNTE0 +OVoXDTI5MTAyOTIyNTE0OVowTTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMRAw +DgYDVQQHDAdCb3VsZGVyMQ0wCwYDVQQKDARIZWxtMRAwDgYDVQQDDAdoZWxtLnNo +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyIlVDQvx2ubAcH3TJ824 +qIGLfKSJ5dGxeAEd30SIC/zWgTU90Ttej7uTs34o2+3/oBM6cKP+lGsL/vtjALDL +3IHNgNzQL7+yT5qB1CLcl8iPJ4ZZkfqSBXXEzGv1qWt0HvXbqfi/jKIl/aDMefQl +V3SpI5vityJ6FHo96vF+MmtXbC7GT3VU+WtU0srrVByvORWb0HwP+FVRBOra+nuL +Yw+sObH2S45O9urpe+a6XlqOke/csX1SP3ODUkaDSEn/8i3KVPI2u0nMWZnAns+O +eFVs7X1+g7hZLH34GoHwffUn8tuu1DFUOP5Hsu4WIA/x2y0ov2846xG7mtSyWjpK +fwIDAQABoxwwGjAYBgNVHREEETAPggdoZWxtLnNohwR/AAABMA0GCSqGSIb3DQEB +CwUAA4IBAQBOFyc9Nk5sK/fUKDN+BSZ6QqAsRFcEoN7fQPuvcCfmVSDx+MBQY6u4 +8TFdHvTKjWUL1F5bdy8qr3RfGC2SKX8tl/vsquMe27ONAaqCGvYoqLPuFZ+a9XY3 +MPI7OBOy1BSUxjj6+W6U6B8RC7BpGrP58Se00vVkVHyP54Mx9g2nDg5m2DMv4KGT +VpJYv1DaVo7bQiL1DG/4TO/1fC2muGDku9+jbMJrmQvTCq189HRymlJegdmiot1o +OPu3VH/2qu5T3j06DoZTra9y2/trGM6s5GRwE2javuFrRt2gcpabP7rPEW6YAwpp +g543Jck2uWhPc8rGMly+RmS7qMxxJY++ -----END CERTIFICATE----- diff --git a/testdata/generate.sh b/testdata/generate.sh new file mode 100755 index 00000000000..9751ef304bf --- /dev/null +++ b/testdata/generate.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +openssl req -new -config openssl.conf -key key.pem -out key.csr +openssl ca -config openssl.conf -create_serial -batch -in key.csr -out crt.pem -key rootca.key -cert rootca.crt diff --git a/testdata/key.pem b/testdata/key.pem index 8b2cde82ed0..691e55087e3 100644 --- a/testdata/key.pem +++ b/testdata/key.pem @@ -1,51 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIJKQIBAAKCAgEA58scWbUymSzmMhA5qNaWPJugNbaLdVLoksbcKeKzWCGlcgm6 -XSqza+vKunUbONLxFhnx+NeTsxYyt0aW83UqngeFPraFJWyGI3/TeP2awI6gTvl3 -fiSlJISnGSGzCR29pDs6ji+y0lpSnMhgmP8e04+KnZGvUUIlwe81VBKnE2tdwPv6 -3az1oMyv8RiGIe3AnoToscV0pn3Ah0ee9wQwuY+rPSqMwE/OKGwYBILFYLtfnfp9 -Q/nKgK+6aUmUzFrP3/g8MCRv1Vm5SGLArjd8jRy2mLC/TDE8NaILpXEZhTcsPQkl -3clQsRo4f/vrENn+O06WnSPiM5J6wZHzy+/s7diMmjkXIbhrHV/iMEJd2mY2sjMe -2I2Ekm0CH6UhfcuClT8+ERxFJ9dIbF//Klz84rW11S4e6W/ZVDBjx3IU5v3XfqGQ -Gy9U1CCUDiZ9rxgzCrLgbMAYNdmDXYQKeeRA2ceeAgojNrEbZVTU6ZGra+0b3eQg -xbiwM8B56xCk8uRDlK0vWmM6imhjqzCPpNTgk6dy8C3T13ezlffmbkix1+oLxeLg -LnCvBy6d1YrI9Bnli+zs6U79opozV+QtcDx+1EelLBfnYG86mTj+t3uVkdXhKrYe -YnhPuY0PJ8bzxf/6ih0DZYGw/d2Nwd1H5o65z9XIYKhZ/SAF1hbMmeyPG10CAwEA -AQKCAgEAuFqW5dJzt9g6Db9R3LMvMm0kcxQIvvt99p8rJDUmJwY7rAOIsejwYvla -eAoD6KH9FXL1PNFYq6sQEyyVinS5vI6Gr2ZDZ4x0828LJsOtfVDyt106aJ2EqxLG -Q/rFho6c8i4ZWFUfiKZF5mSIT6c5QVJ9EO153ssZdLFoXMGpGIzgOEkxMXYKtiWW -Gc9Df2C1Pl6/JATDzldd9TpFeHlgt3VI4JEi+SF/+i5eu9e2XEUqu18qmhHluYwK -WwsmyZHAm4W3eSLBv5JpBuVkEiwXZ7Ralf6dZ2ARXybO1HqrrYRALxtDfq5K+1C7 -dy9JulFnHoxWxgxwMExkTehjWuQsL0vEqYEGfa9q3yz61uYB7Np3bKadhke4BftP -zsHciIcJJk1cwqAJMcE968SWLuARm5SK6UacVHujp0pB78kpz3VjWwICXKU5zVuh -BXkb5fTDAQB+8KklYSrg0XP9lav9fwmCrZtHosq88M8HPPW7vrx1Wr5cxKiEbJK2 -MeJxrhnTCQamHMWw/9zkWRCwLpMKTXc/6u7BtnacjDASqaJ+F+ZF9PHab6vBOdXK -zx5YLAKVGpVu8bZM7fduYJxOAIDtkA1RqA8cPkwUOA0zJMPeBO/mJYOYnDhS/456 -CYvNGjbQjgXxLmsXnVezt7cd+QsH45WNHV7qMTaC30r3//VKTwECggEBAPvPYIhI -EHH8rCCctD1pHQJtPFpbREukmycKGX9QRZG5ZyZcxrr6tde+zlSRQwk2/fxVZ4x2 -m6qCgB91gD+stNkASSsgeP9XSpX15DY9+7Wj6/PGlgPOaX9/lx0hadRXCgCNvsbc -ECy870NJKFSxXHVaab+9AqQginOJLYYoGOxlEbs0eXXeAvl5BGFi2hdDSjeb6P6R -/H/MMMoLeAZLGGRpncNHiDpBQ+h4k/5dgBSV1pMgfW+n/zYu3FnyYKnoXTsjx5eM -Sk+mEH5A/wwOrAA007vSUjDcTpKw1AVCic72/59MrR4C/oUMj0omP1GirLsYv6fx -dd3UiK/itP82vbECggEBAOumeDvH5zl2cepzuv+gx9vg17/r4yCzt0qTpStmakjT -d7xVurBxeNets3w0Tkcti2zJU3nUBPcFmYNmGvq5VB1mnmbo0DgDaxB4ZluBnadk -XOg9ItJrLyW6eeYKeLSvE5Q2cC6u8mfYWAfhT5WdGIX6gg1yOdSwP292qRtG4fdk -YZ5GYQQ9XRuPVHNOgdcXGxrx84aoH6W2Tp+CjIqekZvX5BKOA3p+8du0COetJ9yF -nB0RIDElF87UBFuAP4hNk1gDop3Xl6n4Wh+a1xFaQmUH12Q8ErXmxtAzlBsqFYeT -6U60wQMr0xF2I9irCH+V74wnoPFIkIcbwxbDfh24h20CggEAe9UGzt5JoBS2/S6z -AIRBrODVTkYVtvFTD4bK9S4fmENJ87aqUGdcp6WAyFvLUKvHiaDiVFQ7x0V4BoB9 -OlMPeKvIT7ofZsqhtk9/FCG1OCVNsstVGLgYb4fqY3v8FF1dYNpUGG0+UxHyw+8l -M0kpg9ibqpwjwVzzWU/7oD71ysMFTj/G/2zXn6GgwtefEtOXmvNESHS4bIyY7bNo -KggiDbdWyyLRXnycDaXGec+3Xeg15pKSvScrvZSb7mvgl43a02uMCv4FyVeMQtpp -0p8gfNV9zp7mpnqg9Uiaa5/GL46ONOO7OsgULI/5o2hduSK7uSK5lbiL0zRip8Rg -aCWecQKCAQEAx75ohcuxbBzA/IkyhcHEBtW0KyMId8y93cH+rCX4i1hsUsCcKTlV -xAOhcvNnMqAhYYnZbxfPSY9+i0l+Lu3upak5NWO8Mu56zxAvOvtIJf5FXjmMDa36 -3dENyHcxz33ja6slNfzmzi0smSlbaycpBU/M8xbSfD0U2CdNuihAG5IDyMRBMfXN -uTGp1L9EAYy9Vf6mfIp/oNhCFqTy+gDkzaOW2D92JVv7KE6XicFVW3AJXv4IOoAF -iTRfqSuxLpkK/vy912tKTDGOOuHl0Pif9MFLytO8zGEcPpipvsjSTQSMK0G9pTF9 -jHyGb/6ximwOC8//dOYcU9mtaNs2SH0ElQKCAQA3w+4zTnrD/VCK0dGJxaPUn6Kq -eaK71lEWfSA2kkKEItaEsRYwfzX6LSJyDgjpvZg5LIIVyxd0h8Q4Apw2LNbZqWVt -wBgi0H1SttHJ62z9IO8EEKHB1suGbtsPRDM4IoqgsPYD0GZ4fhgJzoy2Z3qvMlWB -/pz0+P1sCGaghEiwPOLbv+1uZXDOWVi2qaQq9uceldqitWSOFjiJFEOH3SdA0XDo -drA8S5vFWe3dgCIcHRmTGbOG3eID16Q2Zq636U7eM6Q2UZ3G+EwrefuG8q6DeYJ6 -7LcdWpKduPf3s/Jx23Otc8CNmAEixDkRFY0Glv/8e17rgUpLhiQsUIyqoTap +MIIEpgIBAAKCAQEAyIlVDQvx2ubAcH3TJ824qIGLfKSJ5dGxeAEd30SIC/zWgTU9 +0Ttej7uTs34o2+3/oBM6cKP+lGsL/vtjALDL3IHNgNzQL7+yT5qB1CLcl8iPJ4ZZ +kfqSBXXEzGv1qWt0HvXbqfi/jKIl/aDMefQlV3SpI5vityJ6FHo96vF+MmtXbC7G +T3VU+WtU0srrVByvORWb0HwP+FVRBOra+nuLYw+sObH2S45O9urpe+a6XlqOke/c +sX1SP3ODUkaDSEn/8i3KVPI2u0nMWZnAns+OeFVs7X1+g7hZLH34GoHwffUn8tuu +1DFUOP5Hsu4WIA/x2y0ov2846xG7mtSyWjpKfwIDAQABAoIBAQC/XB1m58EQzCVS +sx7t2qedVJEQjcpxHdql0xr4VOMl3U2r2mx03pxrt+lH3NmMlN3bmL2pgzSJ2GSI +Gsbsf8jpUIwTraKUDe9PevbswZ+Sz3Wbl96dKGhzAWCcWWEBHGKgsKe+2Hmg75Il +Jm446btAaziDnFuJukKYi9XN/kgYPxi914O8yz2KtCIVHEHHkl1FcSqjpghPtzU3 +hm1Nv/7tW2r5IrxCGRNJQTg6l4A4mdqif1u75ZUMcbp8dTaJ2/iYBIKIsh7sFMqy +TG6ZN0p3G92ijo7rtznxXS9rIE2rcg6qhusdK8eqhV0KHOqH2nkB4jWbw1NwKFzV +2jXm4S5RAoGBAPIExNBpE30c++Wl4ITuzODd99CczFj527ZBxUdT/H/IszR7adtJ +gHnayzzycul3GnCVMEGBUBp7q09OkcacA7MqS3/Zjn2zrpViz2iluP6jl0qfs2Sp +HaePLBKz9oFVi5m17ZYYnG7etSPVzcLaEi23ws5286HToXeqfUuGd+DlAoGBANQf +FJzQ0EbNu5QcNnQqwfAahvSqc+imPL0HuQWKEMvN3UXXU7Nn8bqba/JGVhgD7/5u +3g2DyyIou6gnocN669CqY8hm0jEboggD4pC8LVj+Iot25UzoNeNuHfqeu7wAlWWL +zjfC3UpSbh1O4H8i5chpFxe9N7syzOXBI5IVPBuTAoGBAITrrZSxQSzj8E0uj2Mz +LH8MKgD/PRRZFhzBfrIwJGuiNRpL9dWkRtWmHx14IziqW3Ed3wT7Gp2Q8oN6KYIl +SbrrLdAoEqRjPS16uWNGMZZZDszDbWmJoGnYrmIPSQG7lBJ14uke1zvlQSNPV9T+ +pCFL3cg7eI+WhgYNMwd58PkpAoGBAKTXFlyaxRAQtrFtjz+NLrMY2kFt6K8l6FN5 +meXdGhpW+5pXsBreLvK17xgSYrs87BbML1FPVt9Pyiztx36ymmjI0MweYz94Wt1h +r4KMSa07qLq6hYzTc3Uu0Ks/CWMbDP4hu/qHOxKTpjCuaDVEeE7ao/B1wcZ+vs3Y +3nyadeBzAoGBAJAZl50nHPwXpEIsHO3nC1ff51cVoV3+gpcCgQ270rLEa2Uv8+Zc +8rXD/LgcLzZ6Fvp0I3jv1mXlN8W0OruZS71lCM/zBd++E04HMxcvuv4lfqzcW+3E +V0ZBn2ErSTF9yKvGedRJk+vbCi7cy38WaA+z59ct/gpiw2Z3q6w85jlF -----END RSA PRIVATE KEY----- diff --git a/testdata/openssl.conf b/testdata/openssl.conf new file mode 100644 index 00000000000..9b27e445bbe --- /dev/null +++ b/testdata/openssl.conf @@ -0,0 +1,42 @@ +[ca] +default_ca = CA_default + +[CA_default] +dir = ./ +database = $dir/index.txt +new_certs_dir = ./ +serial = $dir/serial +private_key = ./rootca.key +certificate = ./rootca.crt +default_days = 3650 +default_md = sha256 +policy = policy_anything +copy_extensions = copyall + +[policy_anything] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ req ] +default_bits = 2048 +distinguished_name = req_distinguished_name +req_extensions = v3_req + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +stateOrProvinceName = State or Province Name (full name) +localityName = Locality Name (eg, city) +organizationName = Organization Name (eg, company) +commonName = Common Name (e.g. server FQDN or YOUR name) + +[ v3_req ] +subjectAltName = @alternate_names + +[alternate_names] +DNS.1 = helm.sh +IP.1 = 127.0.0.1 diff --git a/testdata/releases.yaml b/testdata/releases.yaml new file mode 100644 index 00000000000..e960e815d24 --- /dev/null +++ b/testdata/releases.yaml @@ -0,0 +1,43 @@ +# This file can be used as input to create test releases: +# HELM_MEMORY_DRIVER_DATA=./testdata/releases.yaml HELM_DRIVER=memory helm list --all-namespaces +- name: athos + version: 1 + namespace: default + info: + status: deployed + chart: + metadata: + name: athos-chart + version: 1.0.0 + appversion: 1.1.0 +- name: porthos + version: 2 + namespace: default + info: + status: deployed + chart: + metadata: + name: porthos-chart + version: 0.2.0 + appversion: 0.2.2 +- name: aramis + version: 3 + namespace: default + info: + status: deployed + chart: + metadata: + name: aramis-chart + version: 0.0.3 + appversion: 3.0.3 +- name: dartagnan + version: 4 + namespace: gascony + info: + status: deployed + chart: + metadata: + name: dartagnan-chart + version: 0.4.4 + appversion: 4.4.4 + diff --git a/testdata/rootca.crt b/testdata/rootca.crt new file mode 100644 index 00000000000..892104365f4 --- /dev/null +++ b/testdata/rootca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDITCCAgkCFAasUT/De3J4aee7b1VEESf+3ndyMA0GCSqGSIb3DQEBCwUAME0x +CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEQMA4GA1UEBwwHQm91bGRlcjENMAsG +A1UECgwESGVsbTEQMA4GA1UEAwwHaGVsbS5zaDAeFw0xOTExMDEyMjM2MzZaFw0y +MjA4MjEyMjM2MzZaME0xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDTzEQMA4GA1UE +BwwHQm91bGRlcjENMAsGA1UECgwESGVsbTEQMA4GA1UEAwwHaGVsbS5zaDCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMinBcDJwiG3OVb1bCWQqTAOS3s6 +QwWkEXkoYyFFpCNvqEzQPtp+OkfD6gczc0ByGQibDLBApEQhq17inqtAxIUrTgXP +ym3l+0/U7ejuTka3ue84slkw2lVobfVEvJWGro+93GzbxvVNNYGJcD2BKJqmCCxD +I6tdTEL855kzgQUAvGITzDUxABU9+f06CW/9AlZlmBIuwrzRVjFNjflBrcm1PIUG +upMCu8zaWat8o1TnLCDKizw1JJzCgCnMxGXfzeAd1MGUG/rOFkBImHf39Jakp/7L +Icq+2FDE+0vNai0lpUpxPVTp8dcug8U3//bL3q0OqROA7Ks4wc0URGH71W8CAwEA +ATANBgkqhkiG9w0BAQsFAAOCAQEAMJqzeg6cBbUkrh9a6+qa66IFR1Mf3wVB1c61 +JN6Z70kjgSdOZ/NexxxSu347fIPyKGkmokbnE1MJVEETPmzhpuTkQDcq7KT4IcQF +S+H4l0lNn09thIlIiAJmpQrNOlrHVtpLCFB4+YnsqqFKPlcO/dGy9U26L4xfn6+n +24/o7pNEu44GnktXPjfcbajaPUSKHxeYibjdftoUEYX/79ROu7E1QnNXj7mXymw0 +rqOgIlyCUGw8WvRR8RzR6m+1lnwOc+nxFKXzTt0LqOQt9sHI1V71WrxgDE+Lck+W +fybfsgodM2Y7VXnH4A4xoKeOHxW1YcqIKt0ribt8602lD1pYBg== +-----END CERTIFICATE----- diff --git a/testdata/rootca.key b/testdata/rootca.key new file mode 100644 index 00000000000..e3c1ce51e6e --- /dev/null +++ b/testdata/rootca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAyKcFwMnCIbc5VvVsJZCpMA5LezpDBaQReShjIUWkI2+oTNA+ +2n46R8PqBzNzQHIZCJsMsECkRCGrXuKeq0DEhStOBc/KbeX7T9Tt6O5ORre57ziy +WTDaVWht9US8lYauj73cbNvG9U01gYlwPYEomqYILEMjq11MQvznmTOBBQC8YhPM +NTEAFT35/ToJb/0CVmWYEi7CvNFWMU2N+UGtybU8hQa6kwK7zNpZq3yjVOcsIMqL +PDUknMKAKczEZd/N4B3UwZQb+s4WQEiYd/f0lqSn/sshyr7YUMT7S81qLSWlSnE9 +VOnx1y6DxTf/9sverQ6pE4DsqzjBzRREYfvVbwIDAQABAoIBAHwyTbBP8baWx4oY +rNDvoplZL8VdgaCbNimNIxa0GW3Jrh2lhFIPcZl8HX5JjVvlg7M87XSm/kYhpQY9 +NUMA+uMGs+uK+1xcztpSDNRxtMe27wKwUEw+ndXhprX6ztOqop/cP/StcI/jM2wz +muKm8HAQttxWzlxCinKoQd4k8AYcnqc728FSODP7EsdDgiU6BhBZDqjgmqggye0y +niog+JBPDgwTgGodJWtSYuP/G2iJDUvm7bGU2gftXTJstrATLftGKX8XOgJMmDx9 +8OgDtU21LzggarOQ/iwUKX2MEfYnP8kgGLgu5nNonJCHWYGeCZoxIn70rs3WoBsU +5+FzmHkCgYEA7MFYixlTSxXfen1MwctuZ9YiwoneSLfjmBb+LP0Pfa2r0CVMPaXM +OexroIY14h64nunb7y3YifGk01RXzCBpEF5KhsZuYXAl3lGxbjbTjncU5/11Dim+ +W9g+T4zDimlK2tuweAjMfWz6XG2inZ3xvK73mGkEsUnqhWQKXBRf7VsCgYEA2PZp +KAwbpRFSYFwcZoRm81fLijZ5NbmOJtND6oG1LZVaVSYuvljvjQzeVfL4+Iju6FzT +zbnEfVsatu0cTs6jMy0yJUl6wRbHlH/G6Ra8UxSvUUEFe1Xap33RmjkK+atzALQi +pZPCIfLr+f9qQWrPMdZwzRnws0u2pKepSdXR0H0CgYB9chDdWyTkIwnPmDakdIri +X/b5Bx4Nf8oLGxvAcLHVkMD5v9l+zKvCgT+hxZslXcvK//S17Z/Pr4b7JrSChyXE +M4HfmaKA5HBcNQMDd+9ujDA6n/R29a1UcubJNbeiThoIjuEZKOhZCPY7JShFxZuB +s1+jlPmUiqrF1PUcRvtxAwKBgQDGpuelmWB+hRutyujeHQC+cnaU+EeHH3y+o9Wd +lGG1ePia2jkWZAwCU/QHMk8wEQDelJAB38O/G3mcYAH5Tk4zf4BYj6zrutXGbDBO +H1kToO7dMPG5+eQYU6Vk1jHsZEUKMeU/QckQmIHkBy7c8tT/Rt9FjCjNodd7b2Ab +kMFpaQKBgQDggmgsPFSZmo+yYDZucueXqfc8cbSWd9K1UruKMaPOsyoUWJNYARHA +cpHTpaIjDth8MUp2zLIZnPUSDkSgEAOcRH4C5CxmgSkmeJdlEEzWMF2yugczlYGO +l9SOX07w4/WJCZFeRWTqRGWs7X6iL8um0P9yFelw3SZt33ON+1fRPg== +-----END RSA PRIVATE KEY----- diff --git a/versioning.mk b/versioning.mk deleted file mode 100644 index d1c348f9cd6..00000000000 --- a/versioning.mk +++ /dev/null @@ -1,57 +0,0 @@ -MUTABLE_VERSION := canary - -GIT_COMMIT = $(shell git rev-parse HEAD) -GIT_SHA = $(shell git rev-parse --short HEAD) -GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null) -GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean") - -ifdef VERSION - DOCKER_VERSION = $(VERSION) - BINARY_VERSION = $(VERSION) -endif - -DOCKER_VERSION ?= git-${GIT_SHA} -BINARY_VERSION ?= ${GIT_TAG} - -# Only set Version if building a tag or VERSION is set -ifneq ($(BINARY_VERSION),) - LDFLAGS += -X k8s.io/helm/pkg/version.Version=${BINARY_VERSION} -endif - -# Clear the "unreleased" string in BuildMetadata -ifneq ($(GIT_TAG),) - LDFLAGS += -X k8s.io/helm/pkg/version.BuildMetadata= -endif -LDFLAGS += -X k8s.io/helm/pkg/version.GitCommit=${GIT_COMMIT} -LDFLAGS += -X k8s.io/helm/pkg/version.GitTreeState=${GIT_DIRTY} - -IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${DOCKER_VERSION} -IMAGE_RUDDER := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME_RUDDER}:${DOCKER_VERSION} -MUTABLE_IMAGE := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME}:${MUTABLE_VERSION} -MUTABLE_IMAGE_RUDDER := ${DOCKER_REGISTRY}/${IMAGE_PREFIX}/${SHORT_NAME_RUDDER}:${MUTABLE_VERSION} - -DOCKER_PUSH = docker push -ifeq ($(DOCKER_REGISTRY),gcr.io) - DOCKER_PUSH = gcloud docker push -endif - -info: - @echo "Version: ${VERSION}" - @echo "Git Tag: ${GIT_TAG}" - @echo "Git Commit: ${GIT_COMMIT}" - @echo "Git Tree State: ${GIT_DIRTY}" - @echo "Docker Version: ${DOCKER_VERSION}" - @echo "Registry: ${DOCKER_REGISTRY}" - @echo "Immutable Image: ${IMAGE}" - @echo "Mutable Image: ${MUTABLE_IMAGE}" - -.PHONY: docker-push -docker-push: docker-mutable-push docker-immutable-push - -.PHONY: docker-immutable-push -docker-immutable-push: - ${DOCKER_PUSH} ${IMAGE} - -.PHONY: docker-mutable-push -docker-mutable-push: - ${DOCKER_PUSH} ${MUTABLE_IMAGE}