diff --git a/.circleci/bootstrap.sh b/.circleci/bootstrap.sh deleted file mode 100755 index 79d194077..000000000 --- a/.circleci/bootstrap.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/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. -set -euo pipefail - -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 7e43b1f89..b377a086c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,40 +1,14 @@ --- + +# This file can be removed when Helm no longer uses CircleCI on any release +# branches. Once CircleCI is turned off this file can be removed. version: 2 jobs: build: - working_directory: ~/helm.sh/helm docker: - - image: circleci/golang:1.16 - - auth: - username: $DOCKER_USER - password: $DOCKER_PASS - - environment: - GOCACHE: "/tmp/go/cache" - GOLANGCI_LINT_VERSION: "1.36.0" + - image: cimg/go:1.18 steps: - checkout - - run: - name: install test dependencies - command: .circleci/bootstrap.sh - - run: - name: test style - command: make test-style - - run: - name: test - command: make test-coverage - - deploy: - name: deploy - command: .circleci/deploy.sh -workflows: - version: 2 - build: - jobs: - - build: - filters: - tags: - only: /.*/ diff --git a/.circleci/deploy.sh b/.circleci/deploy.sh deleted file mode 100755 index 26bc07c73..000000000 --- a/.circleci/deploy.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/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. -set -euo pipefail - -# Skip on pull request builds -if [[ -n "${CIRCLE_PR_NUMBER:-}" ]]; then - exit -fi - -: ${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 - VERSION="${CIRCLE_TAG}" -elif [[ "${CIRCLE_BRANCH:-}" == "main" ]]; then - VERSION="canary" -else - echo "Skipping deploy step; this is neither a releasable branch or a tag" - exit -fi - -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 "Building helm binaries" -make build-cross -make dist checksum VERSION="${VERSION}" - -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/.github/dependabot.yml b/.github/dependabot.yml index 68334cf33..c9702f2cd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,4 +4,18 @@ updates: - package-ecosystem: "gomod" directory: "/" schedule: - interval: "daily" \ No newline at end of file + interval: "daily" + groups: + k8s.io: + patterns: + - "k8s.io/api" + - "k8s.io/apiextensions-apiserver" + - "k8s.io/apimachinery" + - "k8s.io/apiserver" + - "k8s.io/cli-runtime" + - "k8s.io/client-go" + - "k8s.io/kubectl" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/asset-transparency.yaml b/.github/workflows/asset-transparency.yaml new file mode 100644 index 000000000..ff58d1a5f --- /dev/null +++ b/.github/workflows/asset-transparency.yaml @@ -0,0 +1,18 @@ +name: Publish Release Assets to Asset Transparency Log + +on: + release: + types: [published, created, edited, released] + +jobs: + github_release_asset_transparency_log_publish_job: + runs-on: ubuntu-latest + name: Publish GitHub release asset digests to https://beta-asset.transparencylog.net + steps: + - name: Gather URLs from GitHub release and publish + id: asset-transparency + uses: transparencylog/github-releases-asset-transparency-verify-action@c77874b4514ae4003994ece9582675195fe012e2 # v11 + - name: List verified and published URLs + run: echo "Verified URLs ${{ steps.asset-transparency.outputs.verified }}" + - name: List failed URLs + run: echo "Failed URLs ${{ steps.asset-transparency.outputs.failed }}" diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 000000000..7696b4e21 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,36 @@ +name: build-test +on: + push: + branches: + - 'main' + - 'release-**' + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3.6.0 + - name: Setup Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0 + with: + go-version: '1.20' + - name: Install golangci-lint + run: | + curl -sSLO https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz + shasum -a 256 golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz | grep "^$GOLANGCI_LINT_SHA256 " > /dev/null + tar -xf golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz + 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* + env: + GOLANGCI_LINT_VERSION: '1.51.2' + GOLANGCI_LINT_SHA256: '4de479eb9d9bc29da51aec1834e7c255b333723d38dbd56781c68e5dddc6a90b' + - name: Test style + run: make test-style + - name: Run unit tests + run: make test-coverage + - name: Test build + run: make build diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 71d562727..8b904e223 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3.6.0 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # pinv2.22.3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # 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 + uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # pinv2.22.3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # pinv2.22.3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..0a32365db --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +name: release +on: + create: + tags: + - v* + push: + branches: + - main + +# Note the only differences between release and canary-release jobs are: +# - only canary passes --overwrite flag +# - the VERSION make variable passed to 'make dist checksum' is expected to +# be "canary" if the job is triggered by a push to "main" branch. If the +# job is triggered by a tag push, VERSION should be the tag ref. +jobs: + release: + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3.6.0 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0 + with: + go-version: '1.20' + + - name: Run unit tests + run: make test-coverage + + - name: Build Helm Binaries + run: | + make build-cross + make dist checksum VERSION="${{ github.ref_name }}" + + - name: Set latest version + run: | + # Push the latest semver tag, excluding prerelease tags + git tag | sort -r --version-sort | grep '^v[0-9]' | grep -v '-' | head -n1 > _dist/helm-latest-version + + - name: Upload Binaries + uses: bacongobbler/azure-blob-storage-upload@50f7d898b7697e864130ea04c303ca38b5751c50 # pin@3.0.0 + env: + AZURE_STORAGE_CONNECTION_STRING: "${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}" + AZURE_STORAGE_CONTAINER_NAME: "${{ secrets.AZURE_STORAGE_CONTAINER_NAME }}" + with: + source_dir: _dist + container_name: ${{ secrets.AZURE_STORAGE_CONTAINER_NAME }} + connection_string: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }} + extra_args: '--pattern helm-*' + + canary-release: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout source code + uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # pin@v3.6.0 + + - name: Setup Go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0 + with: + go-version: '1.20' + + - name: Run unit tests + run: make test-coverage + + - name: Build Helm Binaries + run: | + make build-cross + make dist checksum VERSION="canary" + + - name: Upload Binaries + uses: bacongobbler/azure-blob-storage-upload@50f7d898b7697e864130ea04c303ca38b5751c50 # pin@3.0.0 + with: + source_dir: _dist + container_name: ${{ secrets.AZURE_STORAGE_CONTAINER_NAME }} + connection_string: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }} + extra_args: '--pattern helm-*' + # WARNING: this will overwrite existing blobs in your blob storage + overwrite: 'true' diff --git a/.github/workflows/stale-issue-bot.yaml b/.github/workflows/stale-issue-bot.yaml index eac66b21a..85160634d 100644 --- a/.github/workflows/stale-issue-bot.yaml +++ b/.github/workflows/stale-issue-bot.yaml @@ -10,7 +10,7 @@ jobs: 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' + exempt-issue-labels: 'keep open,v4.x,in progress' days-before-stale: 90 days-before-close: 30 operations-per-run: 100 diff --git a/.golangci.yml b/.golangci.yml index 491e648a1..3cf50a0d4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,22 +1,19 @@ run: - timeout: 2m + timeout: 10m linters: disable-all: true enable: - - deadcode - dupl - gofmt - goimports - - golint - gosimple - govet - ineffassign - misspell - nakedret - - structcheck + - revive - unused - - varcheck - staticcheck linters-settings: diff --git a/Makefile b/Makefile index d897bc939..d61ac1507 100644 --- a/Makefile +++ b/Makefile @@ -18,12 +18,13 @@ ACCEPTANCE_DIR:=../acceptance-testing ACCEPTANCE_RUN_TESTS=. # go option -PKG := ./... -TAGS := -TESTS := . -TESTFLAGS := -LDFLAGS := -w -s -GOFLAGS := +PKG := ./... +TAGS := +TESTS := . +TESTFLAGS := +LDFLAGS := -w -s +GOFLAGS := +CGO_ENABLED ?= 0 # Rebuild the binary if any of these files change SRC := $(shell find . -type f -name '*.go' -print) go.mod go.sum @@ -77,7 +78,7 @@ all: build build: $(BINDIR)/$(BINNAME) $(BINDIR)/$(BINNAME): $(SRC) - GO111MODULE=on go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm + GO111MODULE=on CGO_ENABLED=$(CGO_ENABLED) go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm # ------------------------------------------------------------------------------ # install @@ -113,7 +114,7 @@ test-coverage: .PHONY: test-style test-style: - GO111MODULE=on golangci-lint run --timeout 5m0s + GO111MODULE=on golangci-lint run @scripts/validate-license.sh .PHONY: test-acceptance @@ -149,15 +150,15 @@ 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 +# If go install 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) + (cd /; GO111MODULE=on go install github.com/mitchellh/gox@latest) $(GOIMPORTS): - (cd /; GO111MODULE=on go get -u golang.org/x/tools/cmd/goimports) + (cd /; GO111MODULE=on go install golang.org/x/tools/cmd/goimports@latest) # ------------------------------------------------------------------------------ # release diff --git a/OWNERS b/OWNERS index aab2709e5..cc18ea522 100644 --- a/OWNERS +++ b/OWNERS @@ -1,21 +1,29 @@ maintainers: - - adamreese - - bacongobbler - hickeyma + - joejulian - jdolitsky - marckhouzam - mattfarina - - prydonius - - SlickNik + - sabre1041 + - scottrigby - technosophos +triage: + - yxxhero + - zonggen + - gjenkins8 + - z4ce emeritus: + - adamreese + - bacongobbler - fibonacci1729 - jascott1 - michelleN - migmartri - nebril + - prydonius - rimusz - seh + - SlickNik - thomastaylor312 - vaikas-google - viglesiasce diff --git a/README.md b/README.md index 5ae421183..b279d6af8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Helm -[![CircleCI](https://circleci.com/gh/helm/helm.svg?style=shield)](https://circleci.com/gh/helm/helm) +[![Build Status](https://github.com/helm/helm/workflows/release/badge.svg)](https://github.com/helm/helm/actions?workflow=release) [![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) @@ -30,7 +30,6 @@ Think of it like apt/yum/homebrew for Kubernetes. ## Install - 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! @@ -40,7 +39,6 @@ If you want to use a package manager: - [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://helm.sh/docs/intro/quickstart/). @@ -68,6 +66,10 @@ You can reach the Helm community and developers via the following channels: - [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)) +### Contribution + +If you're interested in contributing, please refer to the [Contributing Guide](CONTRIBUTING.md) **before submitting a pull request**. + ### Code of conduct Participation in the Helm community is governed by the [Code of Conduct](code-of-conduct.md). diff --git a/cmd/helm/chart.go b/cmd/helm/chart.go deleted file mode 100644 index adc874cfe..000000000 --- a/cmd/helm/chart.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index 67caf08d7..000000000 --- a/cmd/helm/chart_export.go +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index e81882ccc..000000000 --- a/cmd/helm/chart_list.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright The 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 { - chartList := action.NewChartList(cfg) - cmd := &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 chartList.Run(out) - }, - } - f := cmd.Flags() - f.UintVar(&chartList.ColumnWidth, "max-col-width", 60, "maximum column width for output table") - return cmd -} diff --git a/cmd/helm/chart_pull.go b/cmd/helm/chart_pull.go deleted file mode 100644 index 760ff3e2c..000000000 --- a/cmd/helm/chart_pull.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index ff34632b1..000000000 --- a/cmd/helm/chart_push.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index d952951fb..000000000 --- a/cmd/helm/chart_remove.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index 35b72cd07..000000000 --- a/cmd/helm/chart_save.go +++ /dev/null @@ -1,61 +0,0 @@ -/* -Copyright The 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 880e77c57..310c915b8 100644 --- a/cmd/helm/completion.go +++ b/cmd/helm/completion.go @@ -72,6 +72,16 @@ To load completions for every new session, execute once: You will need to start a new shell for this setup to take effect. ` +const powershellCompDesc = ` +Generate the autocompletion script for powershell. + +To load completions in your current shell session: +PS C:\> helm completion powershell | Out-String | Invoke-Expression + +To load completions for every new session, add the output of the above command +to your powershell profile. +` + const ( noDescFlagName = "no-descriptions" noDescFlagText = "disable completion descriptions" @@ -88,16 +98,16 @@ func newCompletionCmd(out io.Writer) *cobra.Command { } bash := &cobra.Command{ - Use: "bash", - Short: "generate autocompletion script for bash", - Long: bashCompDesc, - Args: require.NoArgs, - DisableFlagsInUseLine: true, - ValidArgsFunction: noCompletions, + Use: "bash", + Short: "generate autocompletion script for bash", + Long: bashCompDesc, + Args: require.NoArgs, + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { return runCompletionBash(out, cmd) }, } + bash.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) zsh := &cobra.Command{ Use: "zsh", @@ -123,13 +133,25 @@ func newCompletionCmd(out io.Writer) *cobra.Command { } fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) - cmd.AddCommand(bash, zsh, fish) + powershell := &cobra.Command{ + Use: "powershell", + Short: "generate autocompletion script for powershell", + Long: powershellCompDesc, + Args: require.NoArgs, + ValidArgsFunction: noCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return runCompletionPowershell(out, cmd) + }, + } + powershell.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText) + + cmd.AddCommand(bash, zsh, fish, powershell) return cmd } func runCompletionBash(out io.Writer, cmd *cobra.Command) error { - err := cmd.Root().GenBashCompletion(out) + err := cmd.Root().GenBashCompletionV2(out, !disableCompDescriptions) // 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 @@ -180,6 +202,13 @@ func runCompletionFish(out io.Writer, cmd *cobra.Command) error { return cmd.Root().GenFishCompletion(out, !disableCompDescriptions) } +func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error { + if disableCompDescriptions { + return cmd.Root().GenPowerShellCompletion(out) + } + return cmd.Root().GenPowerShellCompletionWithDesc(out) +} + // 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 index 6d53616d0..1143d6445 100644 --- a/cmd/helm/completion_test.go +++ b/cmd/helm/completion_test.go @@ -47,10 +47,10 @@ func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) { } if !strings.Contains(out, "ShellCompDirectiveNoFileComp") != shouldBePerformed { if shouldBePerformed { - t.Error(fmt.Sprintf("Unexpected directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName)) + t.Errorf("Unexpected directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName) } else { - t.Error(fmt.Sprintf("Did not receive directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName)) + t.Errorf("Did not receive directive ShellCompDirectiveNoFileComp when completing '%s'", cmdName) } t.Log(out) } diff --git a/cmd/helm/create_test.go b/cmd/helm/create_test.go index 1db6bed52..1a22d058f 100644 --- a/cmd/helm/create_test.go +++ b/cmd/helm/create_test.go @@ -18,7 +18,6 @@ package main import ( "fmt" - "io/ioutil" "os" "path/filepath" "testing" @@ -31,9 +30,9 @@ import ( ) func TestCreateCmd(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) cname := "testchart" - dir := ensure.TempDir(t) + dir := t.TempDir() defer testChdir(t, dir)() // Run a create @@ -62,7 +61,7 @@ func TestCreateCmd(t *testing.T) { } func TestCreateStarterCmd(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) cname := "testchart" defer resetEnv()() os.MkdirAll(helmpath.CachePath(), 0755) @@ -77,7 +76,7 @@ func TestCreateStarterCmd(t *testing.T) { t.Logf("Created %s", dest) } tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl") - if err := ioutil.WriteFile(tplpath, []byte("test"), 0644); err != nil { + if err := os.WriteFile(tplpath, []byte("test"), 0644); err != nil { t.Fatalf("Could not write template: %s", err) } @@ -128,7 +127,7 @@ func TestCreateStarterCmd(t *testing.T) { func TestCreateStarterAbsoluteCmd(t *testing.T) { defer resetEnv()() - defer ensure.HelmHome(t)() + ensure.HelmHome(t) cname := "testchart" // Create a starter. @@ -140,7 +139,7 @@ func TestCreateStarterAbsoluteCmd(t *testing.T) { t.Logf("Created %s", dest) } tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl") - if err := ioutil.WriteFile(tplpath, []byte("test"), 0644); err != nil { + if err := os.WriteFile(tplpath, []byte("test"), 0644); err != nil { t.Fatalf("Could not write template: %s", err) } diff --git a/cmd/helm/dependency_build_test.go b/cmd/helm/dependency_build_test.go index 33198a9dd..37e3242c4 100644 --- a/cmd/helm/dependency_build_test.go +++ b/cmd/helm/dependency_build_test.go @@ -45,16 +45,11 @@ func TestDependencyBuildCmd(t *testing.T) { ociChartName := "oci-depending-chart" c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL) - if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil { + if _, err := chartutil.Save(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...)...) } @@ -136,6 +131,9 @@ func TestDependencyBuildCmd(t *testing.T) { } // OCI dependencies + if err := chartutil.SaveDir(c, dir()); err != nil { + t.Fatal(err) + } cmd = fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json", dir(ociChartName), dir("repositories.yaml"), diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go index 9f7b0f303..967786b9a 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -17,7 +17,6 @@ package main import ( "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -47,16 +46,11 @@ func TestDependencyUpdateCmd(t *testing.T) { ociChartName := "oci-depending-chart" c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL) - if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil { + if _, err := chartutil.Save(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) } @@ -134,6 +128,9 @@ func TestDependencyUpdateCmd(t *testing.T) { } // test for OCI charts + if err := chartutil.SaveDir(c, dir()); err != nil { + t.Fatal(err) + } cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json", dir(ociChartName), dir("repositories.yaml"), @@ -152,7 +149,7 @@ func TestDependencyUpdateCmd(t *testing.T) { func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { defer resetEnv()() - defer ensure.HelmHome(t)() + ensure.HelmHome(t) srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") if err != nil { @@ -188,7 +185,7 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { } // Make sure charts dir still has dependencies - files, err := ioutil.ReadDir(filepath.Join(dir(chartname), "charts")) + files, err := os.ReadDir(filepath.Join(dir(chartname), "charts")) if err != nil { t.Fatal(err) } @@ -209,6 +206,61 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { } } +func TestDependencyUpdateCmd_WithRepoThatWasNotAdded(t *testing.T) { + srv := setupMockRepoServer(t) + srvForUnmanagedRepo := setupMockRepoServer(t) + defer srv.Stop() + defer srvForUnmanagedRepo.Stop() + + dir := func(p ...string) string { + return filepath.Join(append([]string{srv.Root()}, p...)...) + } + + chartname := "depup" + ch := createTestingMetadata(chartname, srv.URL()) + chartDependency := &chart.Dependency{ + Name: "signtest", + Version: "0.1.0", + Repository: srvForUnmanagedRepo.URL(), + } + ch.Metadata.Dependencies = append(ch.Metadata.Dependencies, chartDependency) + + if err := chartutil.SaveDir(ch, dir()); err != nil { + t.Fatal(err) + } + + _, 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) + } + + // This is written directly to stdout, so we have to capture as is + if !strings.Contains(out, `Getting updates for unmanaged Helm repositories...`) { + t.Errorf("No ‘unmanaged’ Helm repo used in test chartdependency or it doesn’t cause the creation "+ + "of an ‘ad hoc’ repo index cache file\n%s", out) + } +} + +func setupMockRepoServer(t *testing.T) *repotest.Server { + srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") + if err != nil { + t.Fatal(err) + } + + t.Logf("Listening on directory %s", srv.Root()) + + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + return srv +} + // createTestingMetadata creates a basic chart that depends on reqtest-0.1.0 // // The baseURL can be used to point to a particular repository server. diff --git a/cmd/helm/docs.go b/cmd/helm/docs.go index 1a28a47ec..523a96022 100644 --- a/cmd/helm/docs.go +++ b/cmd/helm/docs.go @@ -25,6 +25,8 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" + "golang.org/x/text/cases" + "golang.org/x/text/language" "helm.sh/helm/v3/cmd/helm/require" ) @@ -69,14 +71,7 @@ func newDocsCmd(out io.Writer) *cobra.Command { 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 []string{"bash", "man", "markdown"}, cobra.ShellCompDirectiveNoFileComp }) return cmd @@ -91,7 +86,7 @@ func (o *docsOptions) run(out io.Writer) error { hdrFunc := func(filename string) string { base := filepath.Base(filename) name := strings.TrimSuffix(base, path.Ext(base)) - title := strings.Title(strings.Replace(name, "_", " ", -1)) + title := cases.Title(language.Und, cases.NoLower).String(strings.Replace(name, "_", " ", -1)) return fmt.Sprintf("---\ntitle: \"%s\"\n---\n\n", title) } diff --git a/cmd/helm/docs_test.go b/cmd/helm/docs_test.go index f0082578a..fe5864d5e 100644 --- a/cmd/helm/docs_test.go +++ b/cmd/helm/docs_test.go @@ -26,9 +26,9 @@ func TestDocsTypeFlagCompletion(t *testing.T) { cmd: "__complete docs --type ''", golden: "output/docs-type-comp.txt", }, { - name: "completion for docs --type", + name: "completion for docs --type, no filter", cmd: "__complete docs --type mar", - golden: "output/docs-type-filtered-comp.txt", + golden: "output/docs-type-comp.txt", }} runTestCmd(t, tests) } diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go index aefa836c7..a8f25cb35 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -36,14 +36,19 @@ import ( "helm.sh/helm/v3/pkg/repo" ) -const outputFlag = "output" -const postRenderFlag = "post-renderer" +const ( + outputFlag = "output" + postRenderFlag = "post-renderer" + postRenderArgsFlag = "post-renderer-args" +) 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)") + f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)") + f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line") } func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { @@ -56,6 +61,7 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { 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.BoolVar(&c.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download") f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains") } @@ -69,9 +75,7 @@ func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) { 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)) - } + formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc)) } // Sort the results to get a deterministic order for the tests @@ -112,33 +116,85 @@ func (o *outputValue) Set(s string) error { } 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") + p := &postRendererOptions{varRef, "", []string{}} + cmd.Flags().Var(&postRendererString{p}, 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") + cmd.Flags().Var(&postRendererArgsSlice{p}, postRenderArgsFlag, "an argument to the post-renderer (can specify multiple)") +} + +type postRendererOptions struct { + renderer *postrender.PostRenderer + binaryPath string + args []string +} + +type postRendererString struct { + options *postRendererOptions +} + +func (p *postRendererString) String() string { + return p.options.binaryPath +} + +func (p *postRendererString) Type() string { + return "postRendererString" +} + +func (p *postRendererString) Set(val string) error { + if val == "" { + return nil + } + p.options.binaryPath = val + pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...) + if err != nil { + return err + } + *p.options.renderer = pr + return nil } -type postRenderer struct { - renderer *postrender.PostRenderer +type postRendererArgsSlice struct { + options *postRendererOptions } -func (p postRenderer) String() string { - return "exec" +func (p *postRendererArgsSlice) String() string { + return "[" + strings.Join(p.options.args, ",") + "]" } -func (p postRenderer) Type() string { - return "postrenderer" +func (p *postRendererArgsSlice) Type() string { + return "postRendererArgsSlice" } -func (p postRenderer) Set(s string) error { - if s == "" { +func (p *postRendererArgsSlice) Set(val string) error { + + // a post-renderer defined by a user may accept empty arguments + p.options.args = append(p.options.args, val) + + if p.options.binaryPath == "" { return nil } - pr, err := postrender.NewExec(s) + // overwrite if already create PostRenderer by `post-renderer` flags + pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...) if err != nil { return err } - *p.renderer = pr + *p.options.renderer = pr + return nil +} + +func (p *postRendererArgsSlice) Append(val string) error { + p.options.args = append(p.options.args, val) return nil } +func (p *postRendererArgsSlice) Replace(val []string) error { + p.options.args = val + return nil +} + +func (p *postRendererArgsSlice) GetSlice() []string { + return p.options.args +} + func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellCompDirective) { chartInfo := strings.Split(chartRef, "/") if len(chartInfo) != 2 { @@ -153,24 +209,21 @@ func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellC 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)) + 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", details.Metadata.Version, appVersionDesc, createdDesc, deprecated)) } } diff --git a/cmd/helm/flags_test.go b/cmd/helm/flags_test.go index d5576fe9f..07d28c460 100644 --- a/cmd/helm/flags_test.go +++ b/cmd/helm/flags_test.go @@ -83,6 +83,13 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) { rels: releasesMockWithStatus(&release.Info{ Status: release.StatusDeployed, }), + }, { + name: "completion for output flag, no filter", + cmd: fmt.Sprintf("__complete %s --output jso", 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 7c4854b59..727cdaf88 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -33,6 +33,7 @@ get extended information about the release, including: - The generated manifest file - The notes provided by the chart of the release - The hooks associated with the release +- The metadata of the release ` func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { @@ -48,6 +49,7 @@ func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cmd.AddCommand(newGetManifestCmd(cfg, out)) cmd.AddCommand(newGetHooksCmd(cfg, out)) cmd.AddCommand(newGetNotesCmd(cfg, out)) + cmd.AddCommand(newGetMetadataCmd(cfg, out)) return cmd } diff --git a/cmd/helm/get_all.go b/cmd/helm/get_all.go index bf367da7f..e51d50536 100644 --- a/cmd/helm/get_all.go +++ b/cmd/helm/get_all.go @@ -59,7 +59,7 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return tpl(template, data, out) } - return output.Table.Write(out, &statusPrinter{res, true, false}) + return output.Table.Write(out, &statusPrinter{res, true, false, false, true}) }, } diff --git a/cmd/helm/get_metadata.go b/cmd/helm/get_metadata.go new file mode 100644 index 000000000..adab891bd --- /dev/null +++ b/cmd/helm/get_metadata.go @@ -0,0 +1,94 @@ +/* +Copyright The 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" + "helm.sh/helm/v3/pkg/cli/output" +) + +type metadataWriter struct { + metadata *action.Metadata +} + +func newGetMetadataCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + var outfmt output.Format + client := action.NewGetMetadata(cfg) + + cmd := &cobra.Command{ + Use: "metadata RELEASE_NAME", + Short: "This command fetches metadata for a given release", + 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 { + releaseMetadata, err := client.Run(args[0]) + if err != nil { + return err + } + return outfmt.Write(out, &metadataWriter{releaseMetadata}) + }, + } + + f := cmd.Flags() + f.IntVar(&client.Version, "revision", 0, "specify release 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) + } + + bindOutputFlag(cmd, &outfmt) + + return cmd +} + +func (w metadataWriter) WriteTable(out io.Writer) error { + _, _ = fmt.Fprintf(out, "NAME: %v\n", w.metadata.Name) + _, _ = fmt.Fprintf(out, "CHART: %v\n", w.metadata.Chart) + _, _ = fmt.Fprintf(out, "VERSION: %v\n", w.metadata.Version) + _, _ = fmt.Fprintf(out, "APP_VERSION: %v\n", w.metadata.AppVersion) + _, _ = fmt.Fprintf(out, "NAMESPACE: %v\n", w.metadata.Namespace) + _, _ = fmt.Fprintf(out, "REVISION: %v\n", w.metadata.Revision) + _, _ = fmt.Fprintf(out, "STATUS: %v\n", w.metadata.Status) + _, _ = fmt.Fprintf(out, "DEPLOYED_AT: %v\n", w.metadata.DeployedAt) + return nil +} + +func (w metadataWriter) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, w.metadata) +} + +func (w metadataWriter) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, w.metadata) +} diff --git a/cmd/helm/get_metadata_test.go b/cmd/helm/get_metadata_test.go new file mode 100644 index 000000000..b6f0ab9f2 --- /dev/null +++ b/cmd/helm/get_metadata_test.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 main + +import ( + "testing" + + "helm.sh/helm/v3/pkg/release" +) + +func TestGetMetadataCmd(t *testing.T) { + tests := []cmdTestCase{{ + name: "get metadata with a release", + cmd: "get metadata thomas-guide", + golden: "output/get-metadata.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }, { + name: "get metadata requires release name arg", + cmd: "get metadata", + golden: "output/get-metadata-args.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + wantError: true, + }, { + name: "get metadata to json", + cmd: "get metadata thomas-guide --output json", + golden: "output/get-metadata.json", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }, { + name: "get metadata to yaml", + cmd: "get metadata thomas-guide --output yaml", + golden: "output/get-metadata.yaml", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }} + runTestCmd(t, tests) +} + +func TestGetMetadataCompletion(t *testing.T) { + checkReleaseCompletion(t, "get metadata", false) +} + +func TestGetMetadataRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get metadata") +} + +func TestGetMetadataOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "get metadata") +} + +func TestGetMetadataFileCompletion(t *testing.T) { + checkFileCompletion(t, "get metadata", false) + checkFileCompletion(t, "get metadata myrelease", false) +} diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index d0aab5c18..553da5098 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -18,7 +18,7 @@ package main // import "helm.sh/helm/v3/cmd/helm" import ( "fmt" - "io/ioutil" + "io" "log" "os" "strings" @@ -31,15 +31,12 @@ import ( "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/cli" - "helm.sh/helm/v3/pkg/gates" + "helm.sh/helm/v3/pkg/kube" kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" ) -// FeatureGateOCI is the feature gate for checking if `helm chart` and `helm registry` commands should work -const FeatureGateOCI = gates.Gate("HELM_EXPERIMENTAL_OCI") - var settings = cli.New() func init() { @@ -59,6 +56,12 @@ func warning(format string, v ...interface{}) { } func main() { + // Setting the name of the app for managedFields in the Kubernetes client. + // It is set here to the full name of "helm" so that renaming of helm to + // another name (e.g., helm2 or helm3) does not change the name of the + // manager as picked up by the automated name detection. + kube.ManagedFieldsManager = "helm" + actionConfig := new(action.Configuration) cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:]) if err != nil { @@ -88,15 +91,6 @@ func main() { } } -func checkOCIFeatureGate() func(_ *cobra.Command, _ []string) error { - return func(_ *cobra.Command, _ []string) error { - if !FeatureGateOCI.IsEnabled() { - return FeatureGateOCI.Error() - } - return nil - } -} - // This function loads releases into the memory storage if the // environment variable is properly set. func loadReleasesInMemory(actionConfig *action.Configuration) { @@ -112,10 +106,10 @@ func loadReleasesInMemory(actionConfig *action.Configuration) { return } - actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard} + actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard} for _, path := range filePaths { - b, err := ioutil.ReadFile(path) + b, err := os.ReadFile(path) if err != nil { log.Fatal("Unable to read memory driver data", err) } diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 5e59c41ed..b20b1a24d 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -18,7 +18,7 @@ package main import ( "bytes" - "io/ioutil" + "io" "os" "os/exec" "runtime" @@ -60,8 +60,11 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) { } 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.wantError && err == nil { + t.Errorf("expected error, got success with the following output:\n%s", out) + } + if !tt.wantError && err != nil { + t.Errorf("expected no error, got: '%v'", err) } if tt.golden != "" { test.AssertGoldenString(t, out, tt.golden) @@ -71,27 +74,6 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) { } } -func runTestActionCmd(t *testing.T, tests []cmdTestCase) { - t.Helper() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - defer resetEnv()() - - store := storageFixture() - for _, rel := range tt.rels { - store.Create(rel) - } - _, out, err := executeActionCommandC(store, tt.cmd) - if (err != nil) != tt.wantError { - t.Errorf("expected error, got '%v'", err) - } - if tt.golden != "" { - test.AssertGoldenString(t, out, tt.golden) - } - }) - } -} - func storageFixture() *storage.Storage { return storage.Init(driver.NewMemory()) } @@ -110,7 +92,7 @@ func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) actionConfig := &action.Configuration{ Releases: store, - KubeClient: &kubefake.PrintingKubeClient{Out: ioutil.Discard}, + KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, Capabilities: chartutil.DefaultCapabilities, Log: func(format string, v ...interface{}) {}, } diff --git a/cmd/helm/history.go b/cmd/helm/history.go index 06ec07d6d..ee6f391e4 100644 --- a/cmd/helm/history.go +++ b/cmd/helm/history.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "strconv" - "strings" "time" "github.com/gosuri/uitable" @@ -191,12 +190,9 @@ func compListRevisions(toComplete string, cfg *action.Configuration, releaseName 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)) - } + 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", strconv.Itoa(release.Version), appVersion, chartDesc)) } return revisions, cobra.ShellCompDirectiveNoFileComp } diff --git a/cmd/helm/history_test.go b/cmd/helm/history_test.go index 2663e9ee9..07f2d85df 100644 --- a/cmd/helm/history_test.go +++ b/cmd/helm/history_test.go @@ -95,6 +95,11 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) { cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName), rels: releases, golden: "output/revision-comp.txt", + }, { + name: "completion for revision flag, no filter", + cmd: fmt.Sprintf("__complete %s musketeers --revision 1", cmdName), + rels: releases, + golden: "output/revision-comp.txt", }, { name: "completion for revision flag with too few args", cmd: fmt.Sprintf("__complete %s --revision ''", cmdName), diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 04419d730..d987d300f 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -17,8 +17,13 @@ limitations under the License. package main import ( + "context" + "fmt" "io" "log" + "os" + "os/signal" + "syscall" "time" "github.com/pkg/errors" @@ -44,9 +49,10 @@ 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'. 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. +a string value use '--set-string'. You can use '--set-file' to set individual +values from a file when the value itself is too long for the command line +or is dynamically generated. You can also use '--set-json' to set json values +(scalars/objects/arrays) from the command line. $ helm install -f myvalues.yaml myredis ./redis @@ -62,6 +68,11 @@ or $ helm install --set-file my_script=dothings.sh myredis ./redis +or + + $ helm install --set-json 'master.sidecars=[{"name":"sidecar","image":"myImage","imagePullPolicy":"Always","ports":[{"name":"portname","containerPort":1234}]}]' 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: @@ -74,6 +85,13 @@ set for a key called 'foo', the 'newbar' value would take precedence: $ helm install --set foo=bar --set foo=newbar myredis ./redis +Similarly, in the following example 'foo' is set to '["four"]': + + $ helm install --set-json='foo=["one", "two", "three"]' --set-json='foo=["four"]' myredis ./redis + +And in the following example, 'foo' is set to '{"key1":"value1","key2":"bar"}': + + $ helm install --set-json='foo={"key1":"value1","key2":"value2"}' --set-json='foo.key2="bar"' myredis ./redis To check the generated manifests of a release without installing the chart, the '--debug' and '--dry-run' flags can be combined. @@ -81,13 +99,14 @@ 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: +There are six different ways you can express the chart you want to install: 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 +6. By OCI registries: helm install mynginx --version 1.2.3 oci://example.com/charts/nginx CHART REFERENCES @@ -117,12 +136,25 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return compInstall(args, toComplete, client) }, RunE: func(_ *cobra.Command, args []string) error { + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSverify, client.PlainHTTP) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + client.SetRegistryClient(registryClient) + + // This is for the case where "" is specifically passed in as a + // value. When there is no value passed in NoOptDefVal will be used + // and it is set to client. See addInstallFlags. + if client.DryRunOption == "" { + client.DryRunOption = "none" + } rel, err := runInstall(args, client, valueOpts, out) if err != nil { - return err + return errors.Wrap(err, "INSTALLATION FAILED") } - return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}) }, } @@ -135,7 +167,14 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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") + // --dry-run options with expected outcome: + // - Not set means no dry run and server is contacted. + // - Set with no value, a value of client, or a value of true and the server is not contacted + // - Set with a value of false, none, or false and the server is contacted + // The true/false part is meant to reflect some legacy behavior while none is equal to "". + f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.") + f.Lookup("dry-run").NoOptDefVal = "client" + f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") 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)") @@ -150,6 +189,8 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal 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") + f.StringToStringVarP(&client.Labels, "labels", "l", nil, "Labels that would be added to release metadata. Should be divided by comma.") + f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates") addValueOptionsFlags(f, valueOpts) addChartPathOptionsFlags(f, &client.ChartPathOptions) @@ -214,6 +255,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options // 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 { + err = errors.Wrap(err, "An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies") if client.DependencyUpdate { man := &downloader.Manager{ Out: out, @@ -224,6 +266,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, Debug: settings.Debug, + RegistryClient: client.GetRegistryClient(), } if err := man.Update(); err != nil { return nil, err @@ -239,7 +282,28 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options } client.Namespace = settings.Namespace() - return client.Run(chartRequested, vals) + + // Validate DryRunOption member is one of the allowed values + if err := validateDryRunOptionFlag(client.DryRunOption); err != nil { + return nil, err + } + + // Create context and prepare the handle of SIGTERM + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + // Set up channel on which to send signal notifications. + // We must use a buffered channel or risk missing the signal + // if we're not ready to receive when the signal is sent. + cSignal := make(chan os.Signal, 2) + signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) + go func() { + <-cSignal + fmt.Fprintf(out, "Release %s has been cancelled.\n", args[0]) + cancel() + }() + + return client.RunWithContext(ctx, chartRequested, vals) } // checkIfInstallable validates if a chart can be installed @@ -264,3 +328,19 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st } return nil, cobra.ShellCompDirectiveNoFileComp } + +func validateDryRunOptionFlag(dryRunOptionFlagValue string) error { + // Validate dry-run flag value with a set of allowed value + allowedDryRunValues := []string{"false", "true", "none", "client", "server"} + isAllowed := false + for _, v := range allowedDryRunValues { + if dryRunOptionFlagValue == v { + isAllowed = true + break + } + } + if !isAllowed { + return errors.New("Invalid dry-run flag. Flag must one of the following: false, true, none, client, server") + } + return nil +} diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 13c994b92..b34d1455c 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "path/filepath" "testing" "helm.sh/helm/v3/pkg/repo/repotest" @@ -48,6 +49,8 @@ func TestInstall(t *testing.T) { t.Fatal(err) } + repoFile := filepath.Join(srv.Root(), "repositories.yaml") + tests := []cmdTestCase{ // Install, base case { @@ -120,7 +123,7 @@ func TestInstall(t *testing.T) { // Install, using the name-template { name: "install with name-template", - cmd: "install testdata/testcharts/empty --name-template '{{upper \"foobar\"}}'", + cmd: "install testdata/testcharts/empty --name-template '{{ \"foobar\"}}'", golden: "output/install-name-template.txt", }, // Install, perform chart verification along the way. @@ -166,7 +169,7 @@ func TestInstall(t *testing.T) { name: "install library chart", cmd: "install libchart testdata/testcharts/lib-chart", wantError: true, - golden: "output/template-lib-chart.txt", + golden: "output/install-lib-chart.txt", }, // Install, chart with bad type { @@ -244,9 +247,14 @@ func TestInstall(t *testing.T) { cmd: "install aeneas reqtest --namespace default --repo " + srv2.URL + " --username username --password password --pass-credentials", golden: "output/install.txt", }, + { + name: "basic install with credentials and no repo", + cmd: fmt.Sprintf("install aeneas test/reqtest --username username --password password --repository-config %s --repository-cache %s", repoFile, srv.Root()), + golden: "output/install.txt", + }, } - runTestActionCmd(t, tests) + runTestCmd(t, tests) } func TestInstallOutputCompletion(t *testing.T) { @@ -267,6 +275,10 @@ func TestInstallVersionCompletion(t *testing.T) { 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, no filter", + cmd: fmt.Sprintf("%s __complete install releasename testing/alpine --version 0.3", 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), diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index a7aac172a..73a37b6fe 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -29,6 +29,7 @@ import ( "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/lint/support" ) var longLintHelp = ` @@ -76,12 +77,23 @@ func newLintCmd(out io.Writer) *cobra.Command { var message strings.Builder failed := 0 + errorsOrWarnings := 0 for _, path := range paths { - fmt.Fprintf(&message, "==> Linting %s\n", path) - result := client.Run([]string{path}, vals) + // If there is no errors/warnings and quiet flag is set + // go to the next chart + hasWarningsOrErrors := action.HasWarningsOrErrors(result) + if hasWarningsOrErrors { + errorsOrWarnings++ + } + if client.Quiet && !hasWarningsOrErrors { + continue + } + + fmt.Fprintf(&message, "==> Linting %s\n", path) + // 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 @@ -93,7 +105,9 @@ func newLintCmd(out io.Writer) *cobra.Command { } for _, msg := range result.Messages { - fmt.Fprintf(&message, "%s\n", msg) + if !client.Quiet || msg.Severity > support.InfoSev { + fmt.Fprintf(&message, "%s\n", msg) + } } if len(result.Errors) != 0 { @@ -112,7 +126,9 @@ func newLintCmd(out io.Writer) *cobra.Command { if failed > 0 { return errors.New(summary) } - fmt.Fprintln(out, summary) + if !client.Quiet || errorsOrWarnings > 0 { + fmt.Fprintln(out, summary) + } return nil }, } @@ -120,6 +136,7 @@ func newLintCmd(out io.Writer) *cobra.Command { f := cmd.Flags() f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings") f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts") + f.BoolVar(&client.Quiet, "quiet", false, "print only warnings and errors") addValueOptionsFlags(f, valueOpts) return cmd diff --git a/cmd/helm/lint_test.go b/cmd/helm/lint_test.go index 3501ccf87..314b54c35 100644 --- a/cmd/helm/lint_test.go +++ b/cmd/helm/lint_test.go @@ -37,6 +37,32 @@ func TestLintCmdWithSubchartsFlag(t *testing.T) { runTestCmd(t, tests) } +func TestLintCmdWithQuietFlag(t *testing.T) { + testChart1 := "testdata/testcharts/alpine" + testChart2 := "testdata/testcharts/chart-bad-requirements" + tests := []cmdTestCase{{ + name: "lint good chart using --quiet flag", + cmd: fmt.Sprintf("lint --quiet %s", testChart1), + golden: "output/lint-quiet.txt", + }, { + name: "lint two charts, one with error using --quiet flag", + cmd: fmt.Sprintf("lint --quiet %s %s", testChart1, testChart2), + golden: "output/lint-quiet-with-error.txt", + wantError: true, + }, { + name: "lint chart with warning using --quiet flag", + cmd: "lint --quiet testdata/testcharts/chart-with-only-crds", + golden: "output/lint-quiet-with-warning.txt", + }, { + name: "lint non-existent chart using --quiet flag", + cmd: "lint --quiet thischartdoesntexist/", + golden: "", + wantError: true, + }} + runTestCmd(t, tests) + +} + 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 f8be65b17..5ca3de18e 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -83,8 +83,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } if client.Short { - - names := make([]string, 0) + names := make([]string, 0, len(results)) for _, res := range results { names = append(names, res.Name) } @@ -103,17 +102,16 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { fmt.Fprintln(out, res.Name) } return nil - default: - return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat)) } } - return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat)) + return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders)) }, } f := cmd.Flags() f.BoolVarP(&client.Short, "short", "q", false, "output short (quiet) listing format") + f.BoolVarP(&client.NoHeaders, "no-headers", "", false, "don't print headers when using the default output 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") @@ -145,10 +143,11 @@ type releaseElement struct { } type releaseListWriter struct { - releases []releaseElement + releases []releaseElement + noHeaders bool } -func newReleaseListWriter(releases []*release.Release, timeFormat string) *releaseListWriter { +func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders bool) *releaseListWriter { // Initialize the array so no results returns an empty array instead of null elements := make([]releaseElement, 0, len(releases)) for _, r := range releases { @@ -157,8 +156,8 @@ func newReleaseListWriter(releases []*release.Release, timeFormat string) *relea 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, + Chart: formatChartname(r.Chart), + AppVersion: formatAppVersion(r.Chart), } t := "-" @@ -173,12 +172,14 @@ func newReleaseListWriter(releases []*release.Release, timeFormat string) *relea elements = append(elements, element) } - return &releaseListWriter{elements} + return &releaseListWriter{elements, noHeaders} } func (r *releaseListWriter) WriteTable(out io.Writer) error { table := uitable.New() - table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION") + if !r.noHeaders { + 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) } @@ -224,7 +225,14 @@ func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *acti client := action.NewList(cfg) client.All = true client.Limit = 0 - client.Filter = fmt.Sprintf("^%s", toComplete) + // Do not filter so as to get the entire list of releases. + // This will allow zsh and fish to match completion choices + // on other criteria then prefix. For example: + // helm status ingress + // can match + // helm status nginx-ingress + // + // client.Filter = fmt.Sprintf("^%s", toComplete) client.SetStateMask() releases, err := client.Run() diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index b3b29356e..97a1e284f 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -148,6 +148,11 @@ func TestListCmd(t *testing.T) { cmd: "list", golden: "output/list.txt", rels: releaseFixture, + }, { + name: "list without headers", + cmd: "list --no-headers", + golden: "output/list-no-headers.txt", + rels: releaseFixture, }, { name: "list all releases", cmd: "list --all", diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go index 70002b0b0..001a084ed 100644 --- a/cmd/helm/load_plugins.go +++ b/cmd/helm/load_plugins.go @@ -19,7 +19,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "log" "os" "os/exec" @@ -129,7 +128,8 @@ func callPluginExecutable(pluginName string, main string, argv []string, out io. env = append(env, fmt.Sprintf("%s=%s", k, v)) } - prog := exec.Command(main, argv...) + mainCmdExp := os.ExpandEnv(main) + prog := exec.Command(mainCmdExp, argv...) prog.Env = env prog.Stdin = os.Stdin prog.Stdout = out @@ -154,7 +154,7 @@ func callPluginExecutable(pluginName string, main string, argv []string, out io. func manuallyProcessArgs(args []string) ([]string, []string) { known := []string{} unknown := []string{} - 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"} + 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", "--insecure-skip-tls-verify", "--tls-server-name"} knownArg := func(a string) bool { for _, pre := range kvargs { if strings.HasPrefix(a, pre+"=") { @@ -311,9 +311,9 @@ func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *plug // 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) + b, err := os.ReadFile(path) if err != nil { - return cmds, errors.New(fmt.Sprintf("File (%s) not provided by plugin. No plugin auto-completion possible.", path)) + return cmds, fmt.Errorf("file (%s) not provided by plugin. No plugin auto-completion possible", path) } err = yaml.Unmarshal(b, cmds) diff --git a/cmd/helm/package.go b/cmd/helm/package.go index 7134a8784..822d3d56a 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -19,7 +19,6 @@ package main import ( "fmt" "io" - "io/ioutil" "os" "path/filepath" @@ -48,7 +47,7 @@ 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 { +func newPackageCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewPackage() valueOpts := &values.Options{} @@ -87,11 +86,12 @@ func newPackageCmd(out io.Writer) *cobra.Command { if client.DependencyUpdate { downloadManager := &downloader.Manager{ - Out: ioutil.Discard, + Out: io.Discard, ChartPath: path, Keyring: client.Keyring, Getters: p, Debug: settings.Debug, + RegistryClient: cfg.RegistryClient, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, } diff --git a/cmd/helm/package_test.go b/cmd/helm/package_test.go index ecb3ee36c..9093b510c 100644 --- a/cmd/helm/package_test.go +++ b/cmd/helm/package_test.go @@ -16,15 +16,13 @@ limitations under the License. package main import ( - "bytes" + "fmt" "os" "path/filepath" "regexp" + "strings" "testing" - "github.com/spf13/cobra" - - "helm.sh/helm/v3/internal/test/ensure" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" ) @@ -112,21 +110,18 @@ func TestPackage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cachePath := ensure.TempDir(t) + cachePath := t.TempDir() defer testChdir(t, cachePath)() if err := os.MkdirAll("toot", 0777); err != nil { t.Fatal(err) } - var buf bytes.Buffer - c := newPackageCmd(&buf) // 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) } - setFlags(c, tt.flags) re := regexp.MustCompile(tt.expect) adjustedArgs := make([]string, len(tt.args)) @@ -134,7 +129,16 @@ func TestPackage(t *testing.T) { adjustedArgs[i] = filepath.Join(origDir, f) } - err := c.RunE(c, adjustedArgs) + cmd := []string{"package"} + if len(adjustedArgs) > 0 { + cmd = append(cmd, adjustedArgs...) + } + for k, v := range tt.flags { + if v != "0" { + cmd = append(cmd, fmt.Sprintf("--%s=%s", k, v)) + } + } + _, _, err = executeActionCommand(strings.Join(cmd, " ")) if err != nil { if tt.err && re.MatchString(err.Error()) { return @@ -142,10 +146,6 @@ func TestPackage(t *testing.T) { 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) @@ -168,26 +168,21 @@ func TestPackage(t *testing.T) { func TestSetAppVersion(t *testing.T) { var ch *chart.Chart expectedAppVersion := "app-version-foo" - - dir := ensure.TempDir(t) - - c := newPackageCmd(&bytes.Buffer{}) - flags := map[string]string{ - "destination": dir, - "app-version": expectedAppVersion, - } - setFlags(c, flags) - if err := c.RunE(c, []string{"testdata/testcharts/alpine"}); err != nil { - t.Errorf("unexpected error %q", err) + chartToPackage := "testdata/testcharts/alpine" + dir := t.TempDir() + cmd := fmt.Sprintf("package %s --destination=%s --app-version=%s", chartToPackage, dir, expectedAppVersion) + _, output, err := executeActionCommand(cmd) + if err != nil { + t.Logf("Output: %s", output) + t.Fatal(err) } - 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 := loader.Load(chartPath) + ch, err = loader.Load(chartPath) if err != nil { t.Fatalf("unexpected error loading packaged chart: %v", err) } @@ -196,13 +191,6 @@ func TestSetAppVersion(t *testing.T) { } } -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_list.go b/cmd/helm/plugin_list.go index 6c3926a9d..ddf01f6f2 100644 --- a/cmd/helm/plugin_list.go +++ b/cmd/helm/plugin_list.go @@ -18,7 +18,6 @@ package main import ( "fmt" "io" - "strings" "github.com/gosuri/uitable" "github.com/spf13/cobra" @@ -82,9 +81,7 @@ func compListPlugins(toComplete string, ignoredPluginNames []string) []string { 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)) - } + pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage)) } } return pNames diff --git a/cmd/helm/plugin_test.go b/cmd/helm/plugin_test.go index 87fd17681..e13ad26fb 100644 --- a/cmd/helm/plugin_test.go +++ b/cmd/helm/plugin_test.go @@ -161,6 +161,81 @@ func TestLoadPlugins(t *testing.T) { } } +func TestLoadPluginsWithSpace(t *testing.T) { + settings.PluginsDirectory = "testdata/helm home with space/helm/plugins" + settings.RepositoryConfig = "testdata/helm home with space/helm/repositories.yaml" + settings.RepositoryCache = "testdata/helm home with space/helm/repository" + + var ( + out bytes.Buffer + cmd cobra.Command + ) + loadPlugins(&cmd, &out) + + envs := strings.Join([]string{ + "fullenv", + "testdata/helm home with space/helm/plugins/fullenv", + "testdata/helm home with space/helm/plugins", + "testdata/helm home with space/helm/repositories.yaml", + "testdata/helm home with space/helm/repository", + os.Args[0], + }, "\n") + + // Test that the YAML file was correctly converted to a command. + tests := []struct { + use string + short string + long string + expect string + args []string + code int + }{ + {"fullenv", "show env vars", "show all env vars", envs + "\n", []string{}, 0}, + } + + plugins := cmd.Commands() + + if len(plugins) != len(tests) { + t.Fatalf("Expected %d plugins, got %d", len(tests), len(plugins)) + } + + for i := 0; i < len(plugins); i++ { + out.Reset() + tt := tests[i] + pp := plugins[i] + if pp.Use != tt.use { + t.Errorf("%d: Expected Use=%q, got %q", i, tt.use, pp.Use) + } + if pp.Short != tt.short { + t.Errorf("%d: Expected Use=%q, got %q", i, tt.short, pp.Short) + } + if pp.Long != tt.long { + t.Errorf("%d: Expected Use=%q, got %q", i, tt.long, pp.Long) + } + + // Currently, plugins assume a Linux subsystem. Skip the execution + // tests until this is fixed + if runtime.GOOS != "windows" { + if err := pp.RunE(pp, tt.args); err != nil { + 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()) + } + } + } +} + type staticCompletionDetails struct { use string validArgs []string @@ -277,11 +352,6 @@ func TestPluginDynamicCompletion(t *testing.T) { 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" @@ -312,6 +382,11 @@ func TestPluginCmdsCompletion(t *testing.T) { cmd: "__complete plugin update ''", golden: "output/plugin_list_comp.txt", rels: []*release.Release{}, + }, { + name: "completion for plugin update, no filter", + cmd: "__complete plugin update full", + golden: "output/plugin_list_comp.txt", + rels: []*release.Release{}, }, { name: "completion for plugin update repetition", cmd: "__complete plugin update args ''", @@ -322,6 +397,11 @@ func TestPluginCmdsCompletion(t *testing.T) { cmd: "__complete plugin uninstall ''", golden: "output/plugin_list_comp.txt", rels: []*release.Release{}, + }, { + name: "completion for plugin uninstall, no filter", + cmd: "__complete plugin uninstall full", + golden: "output/plugin_list_comp.txt", + rels: []*release.Release{}, }, { name: "completion for plugin uninstall repetition", cmd: "__complete plugin uninstall args ''", diff --git a/cmd/helm/pull.go b/cmd/helm/pull.go index 7711320f1..af3092aff 100644 --- a/cmd/helm/pull.go +++ b/cmd/helm/pull.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "log" - "strings" "github.com/spf13/cobra" @@ -65,11 +64,12 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.Version = ">0.0.0-0" } - if strings.HasPrefix(args[0], "oci://") { - if !FeatureGateOCI.IsEnabled() { - return FeatureGateOCI.Error() - } + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSverify, client.PlainHTTP) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) } + client.SetRegistryClient(registryClient) for i := 0; i < len(args); i++ { output, err := client.Run(args[i]) @@ -87,7 +87,7 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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") + f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and untardir are specified, untardir is appended to this") addChartPathOptionsFlags(f, &client.ChartPathOptions) err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/helm/pull_test.go b/cmd/helm/pull_test.go index 4d86a5029..41ac237f4 100644 --- a/cmd/helm/pull_test.go +++ b/cmd/helm/pull_test.go @@ -34,7 +34,6 @@ func TestPullCmd(t *testing.T) { } defer srv.Stop() - os.Setenv("HELM_EXPERIMENTAL_OCI", "1") ociSrv, err := repotest.NewOCIServer(t, srv.Root()) if err != nil { t.Fatal(err) @@ -371,6 +370,10 @@ func TestPullVersionCompletion(t *testing.T) { 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, no filter", + cmd: fmt.Sprintf("%s __complete pull testing/alpine --version 0.3", repoSetup), + golden: "output/version-comp.txt", }, { name: "completion for pull version flag too few args", cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup), diff --git a/cmd/helm/push.go b/cmd/helm/push.go new file mode 100644 index 000000000..3375155ed --- /dev/null +++ b/cmd/helm/push.go @@ -0,0 +1,101 @@ +/* +Copyright The 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" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/pusher" +) + +const pushDesc = ` +Upload a chart to a registry. + +If the chart has an associated provenance file, +it will also be uploaded. +` + +type registryPushOptions struct { + certFile string + keyFile string + caFile string + insecureSkipTLSverify bool + plainHTTP bool +} + +func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + o := ®istryPushOptions{} + + cmd := &cobra.Command{ + Use: "push [chart] [remote]", + Short: "push a chart to remote", + Long: pushDesc, + Args: require.MinimumNArgs(2), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + // Do file completion for the chart file to push + return nil, cobra.ShellCompDirectiveDefault + } + if len(args) == 1 { + providers := []pusher.Provider(pusher.All(settings)) + var comps []string + for _, p := range providers { + for _, scheme := range p.Schemes { + comps = append(comps, fmt.Sprintf("%s://", scheme)) + } + } + return comps, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace + } + return nil, cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(cmd *cobra.Command, args []string) error { + registryClient, err := newRegistryClient(o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSverify, o.plainHTTP) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + cfg.RegistryClient = registryClient + chartRef := args[0] + remote := args[1] + client := action.NewPushWithOpts(action.WithPushConfig(cfg), + action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile), + action.WithInsecureSkipTLSVerify(o.insecureSkipTLSverify), + action.WithPlainHTTP(o.plainHTTP), + action.WithPushOptWriter(out)) + client.Settings = settings + output, err := client.Run(chartRef, remote) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + f := cmd.Flags() + f.StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file") + f.StringVar(&o.keyFile, "key-file", "", "identify registry 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 chart upload") + f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload") + + return cmd +} diff --git a/internal/experimental/registry/constants_test.go b/cmd/helm/push_test.go similarity index 69% rename from internal/experimental/registry/constants_test.go rename to cmd/helm/push_test.go index 9f078e632..8e56d99dc 100644 --- a/internal/experimental/registry/constants_test.go +++ b/cmd/helm/push_test.go @@ -14,16 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package registry +package main import ( "testing" - - "github.com/stretchr/testify/assert" ) -func TestConstants(t *testing.T) { - knownMediaTypes := KnownMediaTypes() - assert.Contains(t, knownMediaTypes, HelmChartConfigMediaType) - assert.Contains(t, knownMediaTypes, HelmChartContentLayerMediaType) +func TestPushFileCompletion(t *testing.T) { + checkFileCompletion(t, "push", true) + checkFileCompletion(t, "push package.tgz", false) + checkFileCompletion(t, "push package.tgz oci://localhost:5000", false) } diff --git a/cmd/helm/registry.go b/cmd/helm/registry.go index d13c308b2..b2b24cd14 100644 --- a/cmd/helm/registry.go +++ b/cmd/helm/registry.go @@ -29,11 +29,9 @@ 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(), + Use: "registry", + Short: "login to or logout from a registry", + Long: registryHelp, } cmd.AddCommand( newRegistryLoginCmd(cfg, out), diff --git a/cmd/helm/registry_login.go b/cmd/helm/registry_login.go index 43228f90a..112e06a95 100644 --- a/cmd/helm/registry_login.go +++ b/cmd/helm/registry_login.go @@ -21,11 +21,10 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "strings" - "github.com/docker/docker/pkg/term" + "github.com/moby/term" "github.com/spf13/cobra" "helm.sh/helm/v3/cmd/helm/require" @@ -36,45 +35,61 @@ const registryLoginDesc = ` Authenticate to a remote registry. ` +type registryLoginOptions struct { + username string + password string + passwordFromStdinOpt bool + certFile string + keyFile string + caFile string + insecure bool +} + func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - var usernameOpt, passwordOpt string - var passwordFromStdinOpt, insecureOpt bool + o := ®istryLoginOptions{} cmd := &cobra.Command{ - Use: "login [host]", - Short: "login to a registry", - Long: registryLoginDesc, - Args: require.MinimumNArgs(1), - Hidden: !FeatureGateOCI.IsEnabled(), + Use: "login [host]", + Short: "login to a registry", + Long: registryLoginDesc, + Args: require.MinimumNArgs(1), + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { hostname := args[0] - username, password, err := getUsernamePassword(usernameOpt, passwordOpt, passwordFromStdinOpt) + username, password, err := getUsernamePassword(o.username, o.password, o.passwordFromStdinOpt) if err != nil { return err } - return action.NewRegistryLogin(cfg).Run(out, hostname, username, password, insecureOpt) + return action.NewRegistryLogin(cfg).Run(out, hostname, username, password, + action.WithCertFile(o.certFile), + action.WithKeyFile(o.keyFile), + action.WithCAFile(o.caFile), + action.WithInsecure(o.insecure)) }, } 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") + f.StringVarP(&o.username, "username", "u", "", "registry username") + f.StringVarP(&o.password, "password", "p", "", "registry password or identity token") + f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read password or identity token from stdin") + f.BoolVarP(&o.insecure, "insecure", "", false, "allow connections to TLS registry without certs") + f.StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file") + f.StringVar(&o.keyFile, "key-file", "", "identify registry client using this SSL key file") + f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") return cmd } -// Adapted from https://github.com/deislabs/oras +// Adapted from https://github.com/oras-project/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) + passwordFromStdin, err := io.ReadAll(os.Stdin) if err != nil { return "", "", err } @@ -110,7 +125,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd return username, password, nil } -// Copied/adapted from https://github.com/deislabs/oras +// Copied/adapted from https://github.com/oras-project/oras func readLine(prompt string, silent bool) (string, error) { fmt.Print(prompt) if silent { diff --git a/internal/experimental/registry/authorizer.go b/cmd/helm/registry_login_test.go similarity index 73% rename from internal/experimental/registry/authorizer.go rename to cmd/helm/registry_login_test.go index 918a999ba..517fe08e1 100644 --- a/internal/experimental/registry/authorizer.go +++ b/cmd/helm/registry_login_test.go @@ -14,15 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package registry // import "helm.sh/helm/v3/internal/experimental/registry" +package main import ( - "github.com/deislabs/oras/pkg/auth" + "testing" ) -type ( - // Authorizer handles registry auth operations - Authorizer struct { - auth.Client - } -) +func TestRegistryLoginFileCompletion(t *testing.T) { + checkFileCompletion(t, "registry login", false) +} diff --git a/cmd/helm/registry_logout.go b/cmd/helm/registry_logout.go index e7e1a24fe..0084f8c09 100644 --- a/cmd/helm/registry_logout.go +++ b/cmd/helm/registry_logout.go @@ -31,11 +31,11 @@ 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(), + Use: "logout [host]", + Short: "logout from a registry", + Long: registryLogoutDesc, + Args: require.MinimumNArgs(1), + ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { hostname := args[0] return action.NewRegistryLogout(cfg).Run(out, hostname) diff --git a/internal/experimental/registry/resolver.go b/cmd/helm/registry_logout_test.go similarity index 72% rename from internal/experimental/registry/resolver.go rename to cmd/helm/registry_logout_test.go index ff8a82633..31f716725 100644 --- a/internal/experimental/registry/resolver.go +++ b/cmd/helm/registry_logout_test.go @@ -14,15 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package registry // import "helm.sh/helm/v3/internal/experimental/registry" +package main import ( - "github.com/containerd/containerd/remotes" + "testing" ) -type ( - // Resolver provides remotes based on a locator - Resolver struct { - remotes.Resolver - } -) +func TestRegistryLogoutFileCompletion(t *testing.T) { + checkFileCompletion(t, "registry logout", false) +} diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go index 2637cbb9f..548ae2b8a 100644 --- a/cmd/helm/release_testing.go +++ b/cmd/helm/release_testing.go @@ -59,9 +59,9 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command 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=")) + client.Filters[action.IncludeNameFilter] = append(client.Filters[action.IncludeNameFilter], strings.TrimPrefix(f, "name=")) } else if notName.MatchString(f) { - client.Filters["!name"] = append(client.Filters["!name"], notName.ReplaceAllLiteralString(f, "")) + client.Filters[action.ExcludeNameFilter] = append(client.Filters[action.ExcludeNameFilter], notName.ReplaceAllLiteralString(f, "")) } } rel, runErr := client.Run(args[0]) @@ -72,7 +72,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command return runErr } - if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}); err != nil { + if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}); err != nil { return err } diff --git a/cmd/helm/repo_add.go b/cmd/helm/repo_add.go index beafb31cf..2deda3f4f 100644 --- a/cmd/helm/repo_add.go +++ b/cmd/helm/repo_add.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -48,6 +47,7 @@ type repoAddOptions struct { url string username string password string + passwordFromStdinOpt bool passCredentialsAll bool forceUpdate bool allowDeprecatedRepos bool @@ -85,6 +85,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command { f := cmd.Flags() f.StringVar(&o.username, "username", "", "chart repository username") f.StringVar(&o.password, "password", "", "chart repository password") + f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin") 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") @@ -117,7 +118,7 @@ func (o *repoAddOptions) run(out io.Writer) error { repoFileExt := filepath.Ext(o.repoFile) var lockPath string if len(repoFileExt) > 0 && len(repoFileExt) < len(o.repoFile) { - lockPath = strings.Replace(o.repoFile, repoFileExt, ".lock", 1) + lockPath = strings.TrimSuffix(o.repoFile, repoFileExt) + ".lock" } else { lockPath = o.repoFile + ".lock" } @@ -132,7 +133,7 @@ func (o *repoAddOptions) run(out io.Writer) error { return err } - b, err := ioutil.ReadFile(o.repoFile) + b, err := os.ReadFile(o.repoFile) if err != nil && !os.IsNotExist(err) { return err } @@ -143,14 +144,24 @@ func (o *repoAddOptions) run(out io.Writer) error { } 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 + if o.passwordFromStdinOpt { + passwordFromStdin, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + password := strings.TrimSuffix(string(passwordFromStdin), "\n") + password = strings.TrimSuffix(password, "\r") + o.password = password + } else { + 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) } - o.password = string(password) } c := repo.Entry{ @@ -165,6 +176,11 @@ func (o *repoAddOptions) run(out io.Writer) error { InsecureSkipTLSverify: o.insecureSkipTLSverify, } + // Check if the repo name is legal + if strings.Contains(o.name, "/") { + return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", o.name) + } + // 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 @@ -196,7 +212,7 @@ func (o *repoAddOptions) run(out io.Writer) error { f.Update(&c) - if err := f.WriteFile(o.repoFile, 0644); err != nil { + if err := f.WriteFile(o.repoFile, 0600); err != nil { return err } fmt.Fprintf(out, "%q has been added to your repositories\n", o.name) diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go index 739173ee7..2386bb01f 100644 --- a/cmd/helm/repo_add_test.go +++ b/cmd/helm/repo_add_test.go @@ -18,15 +18,15 @@ package main import ( "fmt" - "io/ioutil" + "io" "os" "path/filepath" + "strings" "sync" "testing" "sigs.k8s.io/yaml" - "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" @@ -47,7 +47,11 @@ func TestRepoAddCmd(t *testing.T) { } defer srv2.Stop() - tmpdir := ensure.TempDir(t) + tmpdir := filepath.Join(t.TempDir(), "path-component.yaml/data") + err = os.MkdirAll(tmpdir, 0777) + if err != nil { + t.Fatal(err) + } repoFile := filepath.Join(tmpdir, "repositories.yaml") tests := []cmdTestCase{ @@ -83,7 +87,7 @@ func TestRepoAdd(t *testing.T) { } defer ts.Stop() - rootDir := ensure.TempDir(t) + rootDir := t.TempDir() repoFile := filepath.Join(rootDir, "repositories.yaml") const testRepoName = "test-name" @@ -97,7 +101,7 @@ func TestRepoAdd(t *testing.T) { } os.Setenv(xdg.CacheHomeEnvVar, rootDir) - if err := o.run(ioutil.Discard); err != nil { + if err := o.run(io.Discard); err != nil { t.Error(err) } @@ -121,36 +125,69 @@ func TestRepoAdd(t *testing.T) { o.forceUpdate = true - if err := o.run(ioutil.Discard); err != nil { + if err := o.run(io.Discard); err != nil { t.Errorf("Repository was not updated: %s", err) } - if err := o.run(ioutil.Discard); err != nil { + if err := o.run(io.Discard); err != nil { t.Errorf("Duplicate repository name was added") } } +func TestRepoAddCheckLegalName(t *testing.T) { + ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + defer ts.Stop() + defer resetEnv()() + + const testRepoName = "test-hub/test-name" + + rootDir := t.TempDir() + repoFile := filepath.Join(t.TempDir(), "repositories.yaml") + + o := &repoAddOptions{ + name: testRepoName, + url: ts.URL(), + forceUpdate: false, + deprecatedNoUpdate: true, + repoFile: repoFile, + } + os.Setenv(xdg.CacheHomeEnvVar, rootDir) + + wantErrorMsg := fmt.Sprintf("repository name (%s) contains '/', please specify a different name without '/'", testRepoName) + + if err := o.run(io.Discard); err != nil { + if wantErrorMsg != err.Error() { + t.Fatalf("Actual error %s, not equal to expected error %s", err, wantErrorMsg) + } + } else { + t.Fatalf("expect reported an error.") + } +} + func TestRepoAddConcurrentGoRoutines(t *testing.T) { const testName = "test-name" - repoFile := filepath.Join(ensure.TempDir(t), "repositories.yaml") + repoFile := filepath.Join(t.TempDir(), "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") + repoFile := filepath.Join(t.TempDir(), "foo", "repositories.yaml") repoAddConcurrent(t, testName, repoFile) } func TestRepoAddConcurrentNoFileExtension(t *testing.T) { const testName = "test-name-3" - repoFile := filepath.Join(ensure.TempDir(t), "repositories") + repoFile := filepath.Join(t.TempDir(), "repositories") repoAddConcurrent(t, testName, repoFile) } func TestRepoAddConcurrentHiddenFile(t *testing.T) { const testName = "test-name-4" - repoFile := filepath.Join(ensure.TempDir(t), ".repositories") + repoFile := filepath.Join(t.TempDir(), ".repositories") repoAddConcurrent(t, testName, repoFile) } @@ -173,14 +210,14 @@ func repoAddConcurrent(t *testing.T, testName, repoFile string) { forceUpdate: false, repoFile: repoFile, } - if err := o.run(ioutil.Discard); err != nil { + if err := o.run(io.Discard); err != nil { t.Error(err) } }(fmt.Sprintf("%s-%d", testName, i)) } wg.Wait() - b, err := ioutil.ReadFile(repoFile) + b, err := os.ReadFile(repoFile) if err != nil { t.Error(err) } @@ -204,3 +241,33 @@ func TestRepoAddFileCompletion(t *testing.T) { checkFileCompletion(t, "repo add reponame", false) checkFileCompletion(t, "repo add reponame https://example.com", false) } + +func TestRepoAddWithPasswordFromStdin(t *testing.T) { + srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/testserver/*.*") + defer srv.Stop() + + defer resetEnv()() + + in, err := os.Open("testdata/password") + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + tmpdir := t.TempDir() + repoFile := filepath.Join(tmpdir, "repositories.yaml") + + store := storageFixture() + + const testName = "test-name" + const username = "username" + cmd := fmt.Sprintf("repo add %s %s --repository-config %s --repository-cache %s --username %s --password-stdin", testName, srv.URL(), repoFile, tmpdir, username) + var result string + _, result, err = executeActionCommandStdinC(store, in, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + if !strings.Contains(result, fmt.Sprintf("\"%s\" has been added to your repositories", testName)) { + t.Errorf("Repo was not successfully added. Output: %s", result) + } +} diff --git a/cmd/helm/repo_index.go b/cmd/helm/repo_index.go index 917acd442..3960380d1 100644 --- a/cmd/helm/repo_index.go +++ b/cmd/helm/repo_index.go @@ -43,6 +43,7 @@ type repoIndexOptions struct { dir string url string merge string + json bool } func newRepoIndexCmd(out io.Writer) *cobra.Command { @@ -70,6 +71,7 @@ func newRepoIndexCmd(out io.Writer) *cobra.Command { f := cmd.Flags() f.StringVar(&o.url, "url", "", "url of chart repository") f.StringVar(&o.merge, "merge", "", "merge the generated index into the given index") + f.BoolVar(&o.json, "json", false, "output in JSON format") return cmd } @@ -80,10 +82,10 @@ func (i *repoIndexOptions) run(out io.Writer) error { return err } - return index(path, i.url, i.merge) + return index(path, i.url, i.merge, i.json) } -func index(dir, url, mergeTo string) error { +func index(dir, url, mergeTo string, json bool) error { out := filepath.Join(dir, "index.yaml") i, err := repo.IndexDirectory(dir, url) @@ -95,7 +97,7 @@ 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, 0644) + writeIndexFile(i2, mergeTo, json) } else { i2, err = repo.LoadIndexFile(mergeTo) if err != nil { @@ -105,5 +107,12 @@ func index(dir, url, mergeTo string) error { i.Merge(i2) } i.SortEntries() + return writeIndexFile(i, out, json) +} + +func writeIndexFile(i *repo.IndexFile, out string, json bool) error { + if json { + return i.WriteJSONFile(out, 0644) + } return i.WriteFile(out, 0644) } diff --git a/cmd/helm/repo_index_test.go b/cmd/helm/repo_index_test.go index ae3390154..554a3dadf 100644 --- a/cmd/helm/repo_index_test.go +++ b/cmd/helm/repo_index_test.go @@ -18,18 +18,18 @@ package main import ( "bytes" + "encoding/json" "io" "os" "path/filepath" "testing" - "helm.sh/helm/v3/internal/test/ensure" "helm.sh/helm/v3/pkg/repo" ) func TestRepoIndexCmd(t *testing.T) { - dir := ensure.TempDir(t) + dir := t.TempDir() comp := filepath.Join(dir, "compressedchart-0.1.0.tgz") if err := linkOrCopy("testdata/testcharts/compressedchart-0.1.0.tgz", comp); err != nil { @@ -68,6 +68,28 @@ func TestRepoIndexCmd(t *testing.T) { t.Errorf("expected %q, got %q", expectedVersion, vs[0].Version) } + b, err := os.ReadFile(destIndex) + if err != nil { + t.Fatal(err) + } + if json.Valid(b) { + t.Error("did not expect index file to be valid json") + } + + // Test with `--json` + + c.ParseFlags([]string{"--json", "true"}) + if err := c.RunE(c, []string{dir}); err != nil { + t.Error(err) + } + + if b, err = os.ReadFile(destIndex); err != nil { + t.Fatal(err) + } + if !json.Valid(b) { + t.Error("index file is not valid json") + } + // Test with `--merge` // Remove first two charts. diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go index fc53ba75a..c9b952fee 100644 --- a/cmd/helm/repo_list.go +++ b/cmd/helm/repo_list.go @@ -17,8 +17,8 @@ limitations under the License. package main import ( + "fmt" "io" - "strings" "github.com/gosuri/uitable" "github.com/pkg/errors" @@ -38,8 +38,8 @@ func newRepoListCmd(out io.Writer) *cobra.Command { Args: require.NoArgs, ValidArgsFunction: noCompletions, RunE: func(cmd *cobra.Command, args []string) error { - f, err := repo.LoadFile(settings.RepositoryConfig) - if isNotExist(err) || (len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML)) { + f, _ := repo.LoadFile(settings.RepositoryConfig) + if len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML) { return errors.New("no repositories to show") } @@ -130,9 +130,7 @@ func compListRepos(prefix string, ignoredRepoNames []string) []string { 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) - } + rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL)) } } return rNames diff --git a/cmd/helm/repo_remove.go b/cmd/helm/repo_remove.go index e6e9cb681..0c1ad2cd5 100644 --- a/cmd/helm/repo_remove.go +++ b/cmd/helm/repo_remove.go @@ -67,7 +67,7 @@ func (o *repoRemoveOptions) run(out io.Writer) error { if !r.Remove(name) { return errors.Errorf("no repo named %q found", name) } - if err := r.WriteFile(o.repoFile, 0644); err != nil { + if err := r.WriteFile(o.repoFile, 0600); err != nil { return err } diff --git a/cmd/helm/repo_remove_test.go b/cmd/helm/repo_remove_test.go index cb5c6e9ab..e2795e738 100644 --- a/cmd/helm/repo_remove_test.go +++ b/cmd/helm/repo_remove_test.go @@ -18,12 +18,12 @@ package main import ( "bytes" + "fmt" "os" "path/filepath" "strings" "testing" - "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" @@ -36,7 +36,7 @@ func TestRepoRemove(t *testing.T) { } defer ts.Stop() - rootDir := ensure.TempDir(t) + rootDir := t.TempDir() repoFile := filepath.Join(rootDir, "repositories.yaml") const testRepoName = "test-name" @@ -161,6 +161,55 @@ func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string, } } +func TestRepoRemoveCompletion(t *testing.T) { + ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + defer ts.Stop() + + rootDir := t.TempDir() + repoFile := filepath.Join(rootDir, "repositories.yaml") + repoCache := filepath.Join(rootDir, "cache/") + + var testRepoNames = []string{"foo", "bar", "baz"} + + // 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) + } + } + + repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache) + + // In the following tests, we turn off descriptions for completions by using __completeNoDesc. + // We have to do this because the description will contain the port used by the webserver, + // and that port changes each time we run the test. + tests := []cmdTestCase{{ + name: "completion for repo remove", + cmd: fmt.Sprintf("%s __completeNoDesc repo remove ''", repoSetup), + golden: "output/repo_list_comp.txt", + }, { + name: "completion for repo remove, no filter", + cmd: fmt.Sprintf("%s __completeNoDesc repo remove fo", repoSetup), + golden: "output/repo_list_comp.txt", + }, { + name: "completion for repo remove repetition", + cmd: fmt.Sprintf("%s __completeNoDesc repo remove foo ''", repoSetup), + golden: "output/repo_repeat_comp.txt", + }} + for _, test := range tests { + runTestCmd(t, []cmdTestCase{test}) + } +} + func TestRepoRemoveFileCompletion(t *testing.T) { checkFileCompletion(t, "repo remove", false) checkFileCompletion(t, "repo remove repo1", false) diff --git a/cmd/helm/repo_update.go b/cmd/helm/repo_update.go index 23dca194a..27661674c 100644 --- a/cmd/helm/repo_update.go +++ b/cmd/helm/repo_update.go @@ -32,32 +32,48 @@ import ( 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'. + +You can optionally specify a list of repositories you want to update. + $ helm repo update ... +To update all the repositories, use 'helm repo update'. ` var errNoRepositories = errors.New("no repositories found. You must add one before updating") type repoUpdateOptions struct { - update func([]*repo.ChartRepository, io.Writer) - repoFile string - repoCache string + update func([]*repo.ChartRepository, io.Writer, bool) error + repoFile string + repoCache string + names []string + failOnRepoUpdateFail bool } func newRepoUpdateCmd(out io.Writer) *cobra.Command { o := &repoUpdateOptions{update: updateCharts} cmd := &cobra.Command{ - Use: "update", - Aliases: []string{"up"}, - Short: "update information of available charts locally from chart repositories", - Long: updateDesc, - Args: require.NoArgs, - ValidArgsFunction: noCompletions, + Use: "update [REPO1 [REPO2 ...]]", + Aliases: []string{"up"}, + Short: "update information of available charts locally from chart repositories", + Long: updateDesc, + Args: require.MinimumNArgs(0), + 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 { o.repoFile = settings.RepositoryConfig o.repoCache = settings.RepositoryCache + o.names = args return o.run(out) }, } + + f := cmd.Flags() + + // Adding this flag for Helm 3 as stop gap functionality for https://github.com/helm/helm/issues/10016. + // This should be deprecated in Helm 4 by update to the behaviour of `helm repo update` command. + f.BoolVar(&o.failOnRepoUpdateFail, "fail-on-repo-update-fail", false, "update fails if any of the repository updates fail") + return cmd } @@ -73,35 +89,79 @@ func (o *repoUpdateOptions) run(out io.Writer) error { } var repos []*repo.ChartRepository - for _, cfg := range f.Repositories { - r, err := repo.NewChartRepository(cfg, getter.All(settings)) - if err != nil { + updateAllRepos := len(o.names) == 0 + + if !updateAllRepos { + // Fail early if the user specified an invalid repo to update + if err := checkRequestedRepos(o.names, f.Repositories); err != nil { return err } - if o.repoCache != "" { - r.CachePath = o.repoCache + } + + for _, cfg := range f.Repositories { + if updateAllRepos || isRepoRequested(cfg.Name, o.names) { + r, err := repo.NewChartRepository(cfg, getter.All(settings)) + if err != nil { + return err + } + if o.repoCache != "" { + r.CachePath = o.repoCache + } + repos = append(repos, r) } - repos = append(repos, r) } - o.update(repos, out) - return nil + return o.update(repos, out, o.failOnRepoUpdateFail) } -func updateCharts(repos []*repo.ChartRepository, out io.Writer) { +func updateCharts(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error { fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") var wg sync.WaitGroup + var repoFailList []string for _, re := range repos { wg.Add(1) go func(re *repo.ChartRepository) { defer wg.Done() 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) + repoFailList = append(repoFailList, re.Config.URL) } else { fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name) } }(re) } wg.Wait() + + if len(repoFailList) > 0 && failOnRepoUpdateFail { + return fmt.Errorf("Failed to update the following repositories: %s", + repoFailList) + } + fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") + return nil +} + +func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error { + for _, requestedRepo := range requestedRepos { + found := false + for _, repo := range validRepos { + if requestedRepo == repo.Name { + found = true + break + } + } + if !found { + return errors.Errorf("no repositories found matching '%s'. Nothing will be updated", requestedRepo) + } + } + return nil +} + +func isRepoRequested(repoName string, requestedRepos []string) bool { + for _, requestedRepo := range requestedRepos { + if repoName == requestedRepo { + return true + } + } + return false } diff --git a/cmd/helm/repo_update_test.go b/cmd/helm/repo_update_test.go index 83ef24349..645c68cfe 100644 --- a/cmd/helm/repo_update_test.go +++ b/cmd/helm/repo_update_test.go @@ -19,7 +19,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -35,10 +34,11 @@ func TestUpdateCmd(t *testing.T) { 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) { + updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error { for _, re := range repos { fmt.Fprintln(out, re.Config.Name) } + return nil } o := &repoUpdateOptions{ update: updater, @@ -48,16 +48,63 @@ func TestUpdateCmd(t *testing.T) { t.Fatal(err) } - if got := out.String(); !strings.Contains(got, "charts") { - t.Errorf("Expected 'charts' got %q", got) + if got := out.String(); !strings.Contains(got, "charts") || + !strings.Contains(got, "firstexample") || + !strings.Contains(got, "secondexample") { + t.Errorf("Expected 'charts', 'firstexample' and 'secondexample' but got %q", got) + } +} + +func TestUpdateCmdMultiple(t *testing.T) { + 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, failOnRepoUpdateFail bool) error { + for _, re := range repos { + fmt.Fprintln(out, re.Config.Name) + } + return nil + } + o := &repoUpdateOptions{ + update: updater, + repoFile: "testdata/repositories.yaml", + names: []string{"firstexample", "charts"}, + } + if err := o.run(&out); err != nil { + t.Fatal(err) + } + + if got := out.String(); !strings.Contains(got, "charts") || + !strings.Contains(got, "firstexample") || + strings.Contains(got, "secondexample") { + t.Errorf("Expected 'charts' and 'firstexample' but not 'secondexample' but got %q", got) + } +} + +func TestUpdateCmdInvalid(t *testing.T) { + 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, failOnRepoUpdateFail bool) error { + for _, re := range repos { + fmt.Fprintln(out, re.Config.Name) + } + return nil + } + o := &repoUpdateOptions{ + update: updater, + repoFile: "testdata/repositories.yaml", + names: []string{"firstexample", "invalid"}, + } + if err := o.run(&out); err == nil { + t.Fatal("expected error but did not get one") } } func TestUpdateCustomCacheCmd(t *testing.T) { - rootDir := ensure.TempDir(t) + rootDir := t.TempDir() cachePath := filepath.Join(rootDir, "updcustomcache") os.Mkdir(cachePath, os.ModePerm) - defer os.RemoveAll(cachePath) ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") if err != nil { @@ -70,7 +117,7 @@ func TestUpdateCustomCacheCmd(t *testing.T) { repoFile: filepath.Join(ts.Root(), "repositories.yaml"), repoCache: cachePath, } - b := ioutil.Discard + b := io.Discard if err := o.run(b); err != nil { t.Fatal(err) } @@ -81,7 +128,7 @@ func TestUpdateCustomCacheCmd(t *testing.T) { func TestUpdateCharts(t *testing.T) { defer resetEnv()() - defer ensure.HelmHome(t)() + ensure.HelmHome(t) ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") if err != nil { @@ -98,7 +145,7 @@ func TestUpdateCharts(t *testing.T) { } b := bytes.NewBuffer(nil) - updateCharts([]*repo.ChartRepository{r}, b) + updateCharts([]*repo.ChartRepository{r}, b, false) got := b.String() if strings.Contains(got, "Unable to get an update") { @@ -111,4 +158,81 @@ func TestUpdateCharts(t *testing.T) { func TestRepoUpdateFileCompletion(t *testing.T) { checkFileCompletion(t, "repo update", false) + checkFileCompletion(t, "repo update repo1", false) +} + +func TestUpdateChartsFail(t *testing.T) { + defer resetEnv()() + ensure.HelmHome(t) + + ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + defer ts.Stop() + + var invalidURL = ts.URL() + "55" + r, err := repo.NewChartRepository(&repo.Entry{ + Name: "charts", + URL: invalidURL, + }, getter.All(settings)) + if err != nil { + t.Error(err) + } + + b := bytes.NewBuffer(nil) + if err := updateCharts([]*repo.ChartRepository{r}, b, false); err != nil { + t.Error("Repo update should not return error if update of repository fails") + } + + got := b.String() + if !strings.Contains(got, "Unable to get an update") { + t.Errorf("Repo should have failed update but instead got: %q", got) + } + if !strings.Contains(got, "Update Complete.") { + t.Error("Update was not successful") + } +} + +func TestUpdateChartsFailWithError(t *testing.T) { + defer resetEnv()() + ensure.HelmHome(t) + + ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") + if err != nil { + t.Fatal(err) + } + defer ts.Stop() + + var invalidURL = ts.URL() + "55" + r, err := repo.NewChartRepository(&repo.Entry{ + Name: "charts", + URL: invalidURL, + }, getter.All(settings)) + if err != nil { + t.Error(err) + } + + b := bytes.NewBuffer(nil) + err = updateCharts([]*repo.ChartRepository{r}, b, true) + if err == nil { + t.Error("Repo update should return error because update of repository fails and 'fail-on-repo-update-fail' flag set") + return + } + var expectedErr = "Failed to update the following repositories" + var receivedErr = err.Error() + if !strings.Contains(receivedErr, expectedErr) { + t.Errorf("Expected error (%s) but got (%s) instead", expectedErr, receivedErr) + } + if !strings.Contains(receivedErr, invalidURL) { + t.Errorf("Expected invalid URL (%s) in error message but got (%s) instead", invalidURL, receivedErr) + } + + got := b.String() + if !strings.Contains(got, "Unable to get an update") { + t.Errorf("Repo should have failed update but instead got: %q", got) + } + if strings.Contains(got, "Update Complete.") { + t.Error("Update was not successful and should return error message because 'fail-on-repo-update-fail' flag set") + } } diff --git a/cmd/helm/require/args_test.go b/cmd/helm/require/args_test.go index c8d5c3110..5a84a42d0 100644 --- a/cmd/helm/require/args_test.go +++ b/cmd/helm/require/args_test.go @@ -17,7 +17,7 @@ package require import ( "fmt" - "io/ioutil" + "io" "strings" "testing" @@ -71,7 +71,7 @@ func runTestCases(t *testing.T, testCases []testCase) { Args: tc.validateFunc, } cmd.SetArgs(tc.args) - cmd.SetOutput(ioutil.Discard) + cmd.SetOutput(io.Discard) err := cmd.Execute() if tc.wantError == "" { diff --git a/cmd/helm/rollback.go b/cmd/helm/rollback.go index ea4b75cb1..7de98e404 100644 --- a/cmd/helm/rollback.go +++ b/cmd/helm/rollback.go @@ -32,8 +32,8 @@ 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. If this argument is omitted, it will -roll back to the previous release. +second is a revision (version) number. If this argument is omitted or set to +0, it will roll back to the previous release. To see revision numbers, run 'helm history RELEASE'. ` diff --git a/cmd/helm/rollback_test.go b/cmd/helm/rollback_test.go index 9ca921557..6d38e16eb 100644 --- a/cmd/helm/rollback_test.go +++ b/cmd/helm/rollback_test.go @@ -64,6 +64,12 @@ func TestRollbackCmd(t *testing.T) { cmd: "rollback funny-honey", golden: "output/rollback-no-revision.txt", rels: rels, + }, { + name: "rollback a release with non-existent version", + cmd: "rollback funny-honey 3", + golden: "output/rollback-non-existent-version.txt", + rels: rels, + wantError: true, }, { name: "rollback a release without release name", cmd: "rollback", diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 285c80021..dd95b1df2 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -29,8 +29,8 @@ import ( 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/registry" "helm.sh/helm/v3/pkg/repo" ) @@ -45,28 +45,31 @@ Common actions for Helm: 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. | +| 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, sql. | +| $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_KUBEINSECURE_SKIP_TLS_VERIFY | indicate if the Kubernetes API server's certificate validation should be skipped (insecure) | +| $HELM_KUBETLS_SERVER_NAME | set the server name used to validate the Kubernetes API server certificate | +| $HELM_BURST_LIMIT | set the default burst limit in the case the server contains many CRDs (default 100, -1 to disable)| Helm stores cache, configuration, and data based on the following configuration order: @@ -106,9 +109,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string 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) - } + nsNames = append(nsNames, ns.Name) } return nsNames, cobra.ShellCompDirectiveNoFileComp } @@ -133,9 +134,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string &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)) - } + comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster)) } return comps, cobra.ShellCompDirectiveNoFileComp } @@ -153,11 +152,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string flags.ParseErrorsWhitelist.UnknownFlags = true flags.Parse(args) - registryClient, err := registry.NewClient( - registry.ClientOptDebug(settings.Debug), - registry.ClientOptWriter(out), - registry.ClientOptCredentialsFile(settings.RegistryConfig), - ) + registryClient, err := newDefaultRegistryClient(false) if err != nil { return nil, err } @@ -169,9 +164,9 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string newCreateCmd(out), newDependencyCmd(actionConfig, out), newPullCmd(actionConfig, out), - newShowCmd(out), + newShowCmd(actionConfig, out), newLintCmd(out), - newPackageCmd(out), + newPackageCmd(actionConfig, out), newRepoCmd(out), newSearchCmd(out), newVerifyCmd(out), @@ -197,10 +192,9 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string newDocsCmd(out), ) - // Add *experimental* subcommands cmd.AddCommand( newRegistryCmd(actionConfig, out), - newChartCmd(actionConfig, out), + newPushCmd(actionConfig, out), ) // Find and add plugins @@ -262,3 +256,48 @@ func checkForExpiredRepos(repofile string) { } } + +func newRegistryClient(certFile, keyFile, caFile string, insecureSkipTLSverify, plainHTTP bool) (*registry.Client, error) { + if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSverify { + registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSverify) + if err != nil { + return nil, err + } + return registryClient, nil + } + registryClient, err := newDefaultRegistryClient(plainHTTP) + if err != nil { + return nil, err + } + return registryClient, nil +} + +func newDefaultRegistryClient(plainHTTP bool) (*registry.Client, error) { + opts := []registry.ClientOption{ + registry.ClientOptDebug(settings.Debug), + registry.ClientOptEnableCache(true), + registry.ClientOptWriter(os.Stderr), + registry.ClientOptCredentialsFile(settings.RegistryConfig), + } + if plainHTTP { + opts = append(opts, registry.ClientOptPlainHTTP()) + } + + // Create a new registry client + registryClient, err := registry.NewClient(opts...) + if err != nil { + return nil, err + } + return registryClient, nil +} + +func newRegistryClientWithTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*registry.Client, error) { + // Create a new registry client + registryClient, err := registry.NewRegistryClientWithTLS(os.Stderr, certFile, keyFile, caFile, insecureSkipTLSverify, + settings.RegistryConfig, settings.Debug, + ) + if err != nil { + return nil, err + } + return registryClient, nil +} diff --git a/cmd/helm/root_test.go b/cmd/helm/root_test.go index 075544971..65e6d66c7 100644 --- a/cmd/helm/root_test.go +++ b/cmd/helm/root_test.go @@ -77,7 +77,7 @@ func TestRootCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) for k, v := range tt.envvars { os.Setenv(k, v) diff --git a/cmd/helm/root_unix.go b/cmd/helm/root_unix.go index 3df801e4c..92fa1b59d 100644 --- a/cmd/helm/root_unix.go +++ b/cmd/helm/root_unix.go @@ -1,4 +1,4 @@ -// +build !windows +//go:build !windows /* Copyright The Helm Authors. diff --git a/cmd/helm/root_unix_test.go b/cmd/helm/root_unix_test.go index c62776c2a..f7466a93d 100644 --- a/cmd/helm/root_unix_test.go +++ b/cmd/helm/root_unix_test.go @@ -1,4 +1,4 @@ -// +build !windows +//go:build !windows /* Copyright The Helm Authors. @@ -21,7 +21,6 @@ package main import ( "bytes" "io" - "io/ioutil" "os" "path/filepath" "strings" @@ -49,11 +48,7 @@ func checkPermsStderr() (string, error) { } func TestCheckPerms(t *testing.T) { - tdir, err := ioutil.TempDir("", "helmtest") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tdir) + tdir := t.TempDir() tfile := filepath.Join(tdir, "testconfig") fh, err := os.OpenFile(tfile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0440) if err != nil { diff --git a/cmd/helm/search/search.go b/cmd/helm/search/search.go index fc7f30596..ac29b27c2 100644 --- a/cmd/helm/search/search.go +++ b/cmd/helm/search/search.go @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package search provides client-side repository searching. +/* +Package search provides client-side repository searching. This supports building an in-memory search index based on the contents of multiple repositories, and then using string matching or regular expressions @@ -146,11 +147,10 @@ func (i *Index) SearchLiteral(term string, threshold int) []*Result { term = strings.ToLower(term) buf := []*Result{} for k, v := range i.lines { - lk := strings.ToLower(k) lv := strings.ToLower(v) res := strings.Index(lv, term) if score := i.calcScore(res, lv); res != -1 && score < threshold { - parts := strings.Split(lk, verSep) // Remove version, if it is there. + parts := strings.Split(k, verSep) // Remove version, if it is there. buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) } } diff --git a/cmd/helm/search/search_test.go b/cmd/helm/search/search_test.go index 9c1859d77..dc82ca3d9 100644 --- a/cmd/helm/search/search_test.go +++ b/cmd/helm/search/search_test.go @@ -105,11 +105,11 @@ func loadTestIndex(t *testing.T, all bool) *Index { i := NewIndex() i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all) i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ - "pinta": { + "Pinta": { { URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"}, Metadata: &chart.Metadata{ - Name: "pinta", + Name: "Pinta", Version: "2.0.0", Description: "Two ship, version two", }, @@ -170,14 +170,14 @@ func TestSearchByName(t *testing.T) { query: "pinta", expect: []*Result{ {Name: "testing/pinta"}, - {Name: "ztesting/pinta"}, + {Name: "ztesting/Pinta"}, }, }, { name: "repo-specific search for one result", query: "ztesting/pinta", expect: []*Result{ - {Name: "ztesting/pinta"}, + {Name: "ztesting/Pinta"}, }, }, { @@ -199,7 +199,15 @@ func TestSearchByName(t *testing.T) { query: "two", expect: []*Result{ {Name: "testing/pinta"}, - {Name: "ztesting/pinta"}, + {Name: "ztesting/Pinta"}, + }, + }, + { + name: "search mixedCase and result should be mixedCase too", + query: "pinta", + expect: []*Result{ + {Name: "testing/pinta"}, + {Name: "ztesting/Pinta"}, }, }, { @@ -207,7 +215,7 @@ func TestSearchByName(t *testing.T) { query: "TWO", expect: []*Result{ {Name: "testing/pinta"}, - {Name: "ztesting/pinta"}, + {Name: "ztesting/Pinta"}, }, }, { diff --git a/cmd/helm/search_hub.go b/cmd/helm/search_hub.go index 82b555788..1618a4c9f 100644 --- a/cmd/helm/search_hub.go +++ b/cmd/helm/search_hub.go @@ -53,6 +53,8 @@ type searchHubOptions struct { searchEndpoint string maxColWidth uint outputFormat output.Format + listRepoURL bool + failOnNoResult bool } func newSearchHubCmd(out io.Writer) *cobra.Command { @@ -70,6 +72,9 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { 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") + f.BoolVar(&o.listRepoURL, "list-repo-url", false, "print charts repository URL") + f.BoolVar(&o.failOnNoResult, "fail-on-no-result", false, "search fails if no results are found") + bindOutputFlag(cmd, &o.outputFormat) return cmd @@ -88,22 +93,30 @@ func (o *searchHubOptions) run(out io.Writer, args []string) error { return fmt.Errorf("unable to perform search against %q", o.searchEndpoint) } - return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth)) + return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth, o.listRepoURL, o.failOnNoResult)) +} + +type hubChartRepo struct { + URL string `json:"url"` + Name string `json:"name"` } type hubChartElement struct { - URL string `json:"url"` - Version string `json:"version"` - AppVersion string `json:"app_version"` - Description string `json:"description"` + URL string `json:"url"` + Version string `json:"version"` + AppVersion string `json:"app_version"` + Description string `json:"description"` + Repository hubChartRepo `json:"repository"` } type hubSearchWriter struct { - elements []hubChartElement - columnWidth uint + elements []hubChartElement + columnWidth uint + listRepoURL bool + failOnNoResult bool } -func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint) *hubSearchWriter { +func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint, listRepoURL, failOnNoResult bool) *hubSearchWriter { var elements []hubChartElement for _, r := range results { // Backwards compatibility for Monocular @@ -114,13 +127,18 @@ func newHubSearchWriter(results []monocular.SearchResult, endpoint string, colum url = r.ArtifactHub.PackageURL } - elements = append(elements, hubChartElement{url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description}) + elements = append(elements, hubChartElement{url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description, hubChartRepo{URL: r.Attributes.Repo.URL, Name: r.Attributes.Repo.Name}}) } - return &hubSearchWriter{elements, columnWidth} + return &hubSearchWriter{elements, columnWidth, listRepoURL, failOnNoResult} } func (h *hubSearchWriter) WriteTable(out io.Writer) error { if len(h.elements) == 0 { + // Fail if no results found and --fail-on-no-result is enabled + if h.failOnNoResult { + return fmt.Errorf("no results found") + } + _, err := out.Write([]byte("No results found\n")) if err != nil { return fmt.Errorf("unable to write results: %s", err) @@ -129,9 +147,19 @@ func (h *hubSearchWriter) WriteTable(out io.Writer) error { } table := uitable.New() table.MaxColWidth = h.columnWidth - table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION") + + if h.listRepoURL { + table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION", "REPO URL") + } else { + table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION") + } + for _, r := range h.elements { - table.AddRow(r.URL, r.Version, r.AppVersion, r.Description) + if h.listRepoURL { + table.AddRow(r.URL, r.Version, r.AppVersion, r.Description, r.Repository.URL) + } else { + table.AddRow(r.URL, r.Version, r.AppVersion, r.Description) + } } return output.EncodeTable(out, table) } @@ -145,11 +173,16 @@ func (h *hubSearchWriter) WriteYAML(out io.Writer) error { } func (h *hubSearchWriter) encodeByFormat(out io.Writer, format output.Format) error { + // Fail if no results found and --fail-on-no-result is enabled + if len(h.elements) == 0 && h.failOnNoResult { + return fmt.Errorf("no results found") + } + // 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}) + chartList = append(chartList, hubChartElement{r.URL, r.Version, r.AppVersion, r.Description, r.Repository}) } switch format { diff --git a/cmd/helm/search_hub_test.go b/cmd/helm/search_hub_test.go index ae51b6a3e..89ce2b3e5 100644 --- a/cmd/helm/search_hub_test.go +++ b/cmd/helm/search_hub_test.go @@ -33,6 +33,8 @@ func TestSearchHubCmd(t *testing.T) { defer ts.Close() // The expected output has the URL to the mocked search service in it + // Trailing spaces are necessary to preserve in "expected" as the uitable package adds + // them during printing. 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 @@ -51,6 +53,36 @@ func TestSearchHubCmd(t *testing.T) { } } +func TestSearchHubListRepoCmd(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 + // Trailing spaces are necessary to preserve in "expected" as the uitable package adds + // them during printing. + var expected = fmt.Sprintf(`URL CHART VERSION APP VERSION DESCRIPTION REPO URL +%s/charts/stable/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend https://charts.helm.sh/stable +%s/charts/bitnami/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend https://charts.bitnami.com +`, ts.URL, ts.URL) + + testcmd := "search hub --list-repo-url --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") } @@ -58,3 +90,98 @@ func TestSearchHubOutputCompletion(t *testing.T) { func TestSearchHubFileCompletion(t *testing.T) { checkFileCompletion(t, "search hub", true) // File completion may be useful when inputting a keyword } + +func TestSearchHubCmd_FailOnNoResponseTests(t *testing.T) { + var ( + searchResult = `{"data":[]}` + noResultFoundErr = "Error: no results found\n" + noResultFoundWarn = "No results found\n" + noResultFoundWarnInList = "[]\n" + ) + + type testCase struct { + name string + cmd string + response string + expected string + wantErr bool + } + + var tests = []testCase{ + { + name: "Search hub with no results in response", + cmd: `search hub maria`, + response: searchResult, + expected: noResultFoundWarn, + wantErr: false, + }, + { + name: "Search hub with no results in response and output JSON", + cmd: `search hub maria --output json`, + response: searchResult, + expected: noResultFoundWarnInList, + wantErr: false, + }, + { + name: "Search hub with no results in response and output YAML", + cmd: `search hub maria --output yaml`, + response: searchResult, + expected: noResultFoundWarnInList, + wantErr: false, + }, + { + name: "Search hub with no results in response and --fail-on-no-result enabled, expected failure", + cmd: `search hub maria --fail-on-no-result`, + response: searchResult, + expected: noResultFoundErr, + wantErr: true, + }, + { + name: "Search hub with no results in response, output JSON and --fail-on-no-result enabled, expected failure", + cmd: `search hub maria --fail-on-no-result --output json`, + response: searchResult, + expected: noResultFoundErr, + wantErr: true, + }, + { + name: "Search hub with no results in response, output YAML and --fail-on-no-result enabled, expected failure", + cmd: `search hub maria --fail-on-no-result --output yaml`, + response: searchResult, + expected: noResultFoundErr, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup a mock search service + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, tt.response) + })) + defer ts.Close() + + // Add mock server URL to command + tt.cmd += " --endpoint " + ts.URL + + storage := storageFixture() + + _, out, err := executeActionCommandC(storage, tt.cmd) + if tt.wantErr { + if err == nil { + t.Errorf("expected error due to no record in response, got nil") + } + } else { + if err != nil { + t.Errorf("unexpected error, got %q", err) + } + } + + if out != tt.expected { + t.Errorf("expected and actual output did not match\n"+ + "expected: %q\n"+ + "actual : %q", + tt.expected, out) + } + }) + } +} diff --git a/cmd/helm/search_repo.go b/cmd/helm/search_repo.go index ba692a2e7..2c6f17094 100644 --- a/cmd/helm/search_repo.go +++ b/cmd/helm/search_repo.go @@ -21,7 +21,7 @@ import ( "bytes" "fmt" "io" - "io/ioutil" + "os" "path/filepath" "strings" @@ -63,14 +63,15 @@ Repositories are managed with 'helm repo' commands. const searchMaxScore = 25 type searchRepoOptions struct { - versions bool - regexp bool - devel bool - version string - maxColWidth uint - repoFile string - repoCacheDir string - outputFormat output.Format + versions bool + regexp bool + devel bool + version string + maxColWidth uint + repoFile string + repoCacheDir string + outputFormat output.Format + failOnNoResult bool } func newSearchRepoCmd(out io.Writer) *cobra.Command { @@ -93,6 +94,8 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command { 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") + f.BoolVar(&o.failOnNoResult, "fail-on-no-result", false, "search fails if no results are found") + bindOutputFlag(cmd, &o.outputFormat) return cmd @@ -123,7 +126,7 @@ func (o *searchRepoOptions) run(out io.Writer, args []string) error { return err } - return o.outputFormat.Write(out, &repoSearchWriter{data, o.maxColWidth}) + return o.outputFormat.Write(out, &repoSearchWriter{data, o.maxColWidth, o.failOnNoResult}) } func (o *searchRepoOptions) setupSearchedVersion() { @@ -204,12 +207,18 @@ type repoChartElement struct { } type repoSearchWriter struct { - results []*search.Result - columnWidth uint + results []*search.Result + columnWidth uint + failOnNoResult bool } func (r *repoSearchWriter) WriteTable(out io.Writer) error { if len(r.results) == 0 { + // Fail if no results found and --fail-on-no-result is enabled + if r.failOnNoResult { + return fmt.Errorf("no results found") + } + _, err := out.Write([]byte("No results found\n")) if err != nil { return fmt.Errorf("unable to write results: %s", err) @@ -234,6 +243,11 @@ func (r *repoSearchWriter) WriteYAML(out io.Writer) error { } func (r *repoSearchWriter) encodeByFormat(out io.Writer, format output.Format) error { + // Fail if no results found and --fail-on-no-result is enabled + if len(r.results) == 0 && r.failOnNoResult { + return fmt.Errorf("no results found") + } + // Initialize the array so no results returns an empty array instead of null chartList := make([]repoChartElement, 0, len(r.results)) @@ -258,7 +272,7 @@ func compListChartsOfRepo(repoName string, prefix string) []string { var charts []string path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName)) - content, err := ioutil.ReadFile(path) + content, err := os.ReadFile(path) if err == nil { scanner := bufio.NewScanner(bytes.NewReader(content)) for scanner.Scan() { @@ -301,23 +315,31 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell // First check completions for repos repos := compListRepos("", nil) - for _, repo := range repos { + for _, repoInfo := range repos { + // Split name from description + repoInfo := strings.Split(repoInfo, "\t") + repo := repoInfo[0] + repoDesc := "" + if len(repoInfo) > 1 { + repoDesc = repoInfo[1] + } repoWithSlash := fmt.Sprintf("%s/", repo) if strings.HasPrefix(toComplete, repoWithSlash) { - // Must complete with charts within the specified repo - completions = append(completions, compListChartsOfRepo(repo, toComplete)...) + // Must complete with charts within the specified repo. + // Don't filter on toComplete to allow for shell fuzzy matching + completions = append(completions, compListChartsOfRepo(repo, "")...) noSpace = false break } else if strings.HasPrefix(repo, toComplete) { - // Must complete the repo name - completions = append(completions, repoWithSlash) + // Must complete the repo name with the slash, followed by the description + completions = append(completions, fmt.Sprintf("%s\t%s", repoWithSlash, repoDesc)) 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://"} { + for _, url := range []string{"oci://\tChart OCI prefix", "https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} { 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 @@ -340,7 +362,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell // 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 { + if files, err := os.ReadDir("."); err == nil { for _, file := range files { if strings.HasPrefix(file.Name(), toComplete) { // We are completing a file prefix @@ -354,7 +376,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell // 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, "./", "/") + completions = append(completions, "./\tRelative path prefix to local chart", "/\tAbsolute path prefix to local chart") } cobra.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions), settings.Debug) diff --git a/cmd/helm/search_repo_test.go b/cmd/helm/search_repo_test.go index 58ba3a715..9039842f0 100644 --- a/cmd/helm/search_repo_test.go +++ b/cmd/helm/search_repo_test.go @@ -56,6 +56,20 @@ func TestSearchRepositoriesCmd(t *testing.T) { name: "search for 'syzygy', expect no matches", cmd: "search repo syzygy", golden: "output/search-not-found.txt", + }, { + name: "search for 'syzygy' with --fail-on-no-result, expect failure for no results", + cmd: "search repo syzygy --fail-on-no-result", + golden: "output/search-not-found-error.txt", + wantError: true, + }, {name: "search for 'syzygy' with json output and --fail-on-no-result, expect failure for no results", + cmd: "search repo syzygy --output json --fail-on-no-result", + golden: "output/search-not-found-error.txt", + wantError: true, + }, { + name: "search for 'syzygy' with yaml output --fail-on-no-result, expect failure for no results", + cmd: "search repo syzygy --output yaml --fail-on-no-result", + golden: "output/search-not-found-error.txt", + wantError: true, }, { name: "search for 'alp[a-z]+', expect two matches", cmd: "search repo alp[a-z]+ --regexp", diff --git a/cmd/helm/show.go b/cmd/helm/show.go index 888d2d3f3..28eb9756d 100644 --- a/cmd/helm/show.go +++ b/cmd/helm/show.go @@ -33,7 +33,7 @@ This command consists of multiple subcommands to display information about a cha const showAllDesc = ` This command inspects a chart (directory, file, or URL) and displays all its content -(values.yaml, Charts.yaml, README) +(values.yaml, Chart.yaml, README) ` const showValuesDesc = ` @@ -43,7 +43,7 @@ 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 +of the Chart.yaml file ` const readmeChartDesc = ` @@ -51,8 +51,13 @@ This command inspects a chart (directory, file, or URL) and displays the content of the README file ` -func newShowCmd(out io.Writer) *cobra.Command { - client := action.NewShow(action.ShowAll) +const showCRDsDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the CustomResourceDefinition files +` + +func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewShowWithConfig(action.ShowAll, cfg) showCommand := &cobra.Command{ Use: "show", @@ -79,6 +84,10 @@ func newShowCmd(out io.Writer) *cobra.Command { ValidArgsFunction: validArgsFunc, RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowAll + err := addRegistryClient(client) + if err != nil { + return err + } output, err := runShow(args, client) if err != nil { return err @@ -96,6 +105,10 @@ func newShowCmd(out io.Writer) *cobra.Command { ValidArgsFunction: validArgsFunc, RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowValues + err := addRegistryClient(client) + if err != nil { + return err + } output, err := runShow(args, client) if err != nil { return err @@ -113,6 +126,10 @@ func newShowCmd(out io.Writer) *cobra.Command { ValidArgsFunction: validArgsFunc, RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowChart + err := addRegistryClient(client) + if err != nil { + return err + } output, err := runShow(args, client) if err != nil { return err @@ -130,6 +147,31 @@ func newShowCmd(out io.Writer) *cobra.Command { ValidArgsFunction: validArgsFunc, RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowReadme + err := addRegistryClient(client) + if err != nil { + return err + } + output, err := runShow(args, client) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + crdsSubCmd := &cobra.Command{ + Use: "crds [CHART]", + Short: "show the chart's CRDs", + Long: showCRDsDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: validArgsFunc, + RunE: func(cmd *cobra.Command, args []string) error { + client.OutputFormat = action.ShowCRDs + err := addRegistryClient(client) + if err != nil { + return err + } output, err := runShow(args, client) if err != nil { return err @@ -139,7 +181,7 @@ func newShowCmd(out io.Writer) *cobra.Command { }, } - cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd} + cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd, crdsSubCmd} for _, subCmd := range cmds { addShowFlags(subCmd, client) showCommand.AddCommand(subCmd) @@ -182,3 +224,13 @@ func runShow(args []string, client *action.Show) (string, error) { } return client.Run(cp) } + +func addRegistryClient(client *action.Show) error { + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSverify, client.PlainHTTP) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + client.SetRegistryClient(registryClient) + return nil +} diff --git a/cmd/helm/show_test.go b/cmd/helm/show_test.go index 9781a3de4..93ec08d0f 100644 --- a/cmd/helm/show_test.go +++ b/cmd/helm/show_test.go @@ -47,14 +47,14 @@ func TestShowPreReleaseChart(t *testing.T) { name: "show pre-release chart", args: "test/pre-release-chart", fail: true, - expectedErr: "failed to download \"test/pre-release-chart\"", + expectedErr: "chart \"pre-release-chart\" matching not found in test index. (try 'helm repo update'): no chart version found for 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\"", + expectedErr: "chart \"pre-release-chart\" matching 1.0.0 not found in test index. (try 'helm repo update'): no chart version found for pre-release-chart-1.0.0", }, { name: "show pre-release chart with 'devel' flag", @@ -98,6 +98,10 @@ func TestShowVersionCompletion(t *testing.T) { 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, no filter", + cmd: fmt.Sprintf("%s __complete show chart testing/alpine --version 0.3", repoSetup), + golden: "output/version-comp.txt", }, { name: "completion for show version flag too few args", cmd: fmt.Sprintf("%s __complete show chart --version ''", repoSetup), @@ -145,3 +149,7 @@ func TestShowReadmeFileCompletion(t *testing.T) { func TestShowValuesFileCompletion(t *testing.T) { checkFileCompletion(t, "show values", true) } + +func TestShowCRDsFileCompletion(t *testing.T) { + checkFileCompletion(t, "show crds", true) +} diff --git a/cmd/helm/status.go b/cmd/helm/status.go index 6085251d5..850862cd5 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "bytes" "fmt" "io" "log" @@ -25,6 +26,8 @@ import ( "github.com/spf13/cobra" + "k8s.io/kubectl/pkg/cmd/get" + "helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chartutil" @@ -41,7 +44,7 @@ The status consists of: - 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 +- list of resources that this release consists of (need to enable --show-resources) - details on last test suite run, if applicable - additional notes provided by the chart ` @@ -62,6 +65,13 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return compListReleases(toComplete, args, cfg) }, RunE: func(cmd *cobra.Command, args []string) error { + + // When the output format is a table the resources should be fetched + // and displayed as a table. When YAML or JSON the resources will be + // returned. This mirrors the handling in kubectl. + if outfmt == output.Table { + client.ShowResourcesTable = true + } rel, err := client.Run(args[0]) if err != nil { return err @@ -70,7 +80,7 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { // strip chart metadata from the output rel.Chart = nil - return outfmt.Write(out, &statusPrinter{rel, false, client.ShowDescription}) + return outfmt.Write(out, &statusPrinter{rel, false, client.ShowDescription, client.ShowResources, false}) }, } @@ -92,6 +102,8 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { bindOutputFlag(cmd, &outfmt) f.BoolVar(&client.ShowDescription, "show-desc", false, "if set, display the description message of the named release") + f.BoolVar(&client.ShowResources, "show-resources", false, "if set, display the resources of the named release") + return cmd } @@ -99,6 +111,8 @@ type statusPrinter struct { release *release.Release debug bool showDescription bool + showResources bool + showMetadata bool } func (s statusPrinter) WriteJSON(out io.Writer) error { @@ -113,27 +127,59 @@ func (s statusPrinter) WriteTable(out io.Writer) error { if s.release == nil { return nil } - fmt.Fprintf(out, "NAME: %s\n", s.release.Name) + _, _ = 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, "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.showMetadata { + _, _ = fmt.Fprintf(out, "CHART: %s\n", s.release.Chart.Metadata.Name) + _, _ = fmt.Fprintf(out, "VERSION: %s\n", s.release.Chart.Metadata.Version) + _, _ = fmt.Fprintf(out, "APP_VERSION: %s\n", s.release.Chart.Metadata.AppVersion) } - 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) + _, _ = fmt.Fprintf(out, "DESCRIPTION: %s\n", s.release.Info.Description) + } + + if s.showResources && s.release.Info.Resources != nil && len(s.release.Info.Resources) > 0 { + buf := new(bytes.Buffer) + printFlags := get.NewHumanPrintFlags() + typePrinter, _ := printFlags.ToPrinter("") + printer := &get.TablePrinter{Delegate: typePrinter} + + var keys []string + for key := range s.release.Info.Resources { + keys = append(keys, key) + } + + for _, t := range keys { + _, _ = fmt.Fprintf(buf, "==> %s\n", t) + + vk := s.release.Info.Resources[t] + for _, resource := range vk { + if err := printer.PrintObj(resource, buf); err != nil { + _, _ = fmt.Fprintf(buf, "failed to print object type %s: %v\n", t, err) + } + } + + buf.WriteString("\n") + } + + _, _ = fmt.Fprintf(out, "RESOURCES:\n%s\n", buf.String()) } executions := executionsByHookEvent(s.release) if tests, ok := executions[release.HookTest]; !ok || len(tests) == 0 { - fmt.Fprintln(out, "TEST SUITE: None") + _, _ = 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", + _, _ = 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)), @@ -143,38 +189,38 @@ func (s statusPrinter) WriteTable(out io.Writer) error { } if s.debug { - fmt.Fprintln(out, "USER-SUPPLIED VALUES:") + _, _ = fmt.Fprintln(out, "USER-SUPPLIED VALUES:") err := output.EncodeYAML(out, s.release.Config) if err != nil { return err } // Print an extra newline - fmt.Fprintln(out) + _, _ = fmt.Fprintln(out) cfg, err := chartutil.CoalesceValues(s.release.Chart, s.release.Config) if err != nil { return err } - fmt.Fprintln(out, "COMPUTED VALUES:") + _, _ = fmt.Fprintln(out, "COMPUTED VALUES:") err = output.EncodeYAML(out, cfg.AsMap()) if err != nil { return err } // Print an extra newline - fmt.Fprintln(out) + _, _ = fmt.Fprintln(out) } if strings.EqualFold(s.release.Info.Description, "Dry run complete") || s.debug { - fmt.Fprintln(out, "HOOKS:") + _, _ = 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, "---\n# Source: %s\n%s\n", h.Path, h.Manifest) } - fmt.Fprintf(out, "MANIFEST:\n%s\n", s.release.Manifest) + _, _ = fmt.Fprintf(out, "MANIFEST:\n%s\n", s.release.Manifest) } if len(s.release.Info.Notes) > 0 { - fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes)) + _, _ = fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes)) } return nil } diff --git a/cmd/helm/status_test.go b/cmd/helm/status_test.go index 7f305d56b..6722bf949 100644 --- a/cmd/helm/status_test.go +++ b/cmd/helm/status_test.go @@ -32,7 +32,7 @@ func TestStatusCmd(t *testing.T) { Name: "flummoxed-chickadee", Namespace: "default", Info: info, - Chart: &chart.Chart{}, + Chart: &chart.Chart{Metadata: &chart.Metadata{Name: "name", Version: "1.2.3", AppVersion: "3.2.1"}}, Hooks: hooks, }} } @@ -68,6 +68,24 @@ func TestStatusCmd(t *testing.T) { Status: release.StatusDeployed, Notes: "release notes", }), + }, { + name: "get status of a deployed release with resources", + cmd: "status --show-resources flummoxed-chickadee", + golden: "output/status-with-resources.txt", + rels: releasesMockWithStatus( + &release.Info{ + Status: release.StatusDeployed, + }, + ), + }, { + name: "get status of a deployed release with resources in json", + cmd: "status --show-resources flummoxed-chickadee -o json", + golden: "output/status-with-resources.json", + rels: releasesMockWithStatus( + &release.Info{ + Status: release.StatusDeployed, + }, + ), }, { name: "get status of a deployed release with test suite", cmd: "status flummoxed-chickadee", diff --git a/cmd/helm/template.go b/cmd/helm/template.go index e3c1d421f..a16cbc76e 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -73,8 +73,21 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.KubeVersion = parsedKubeVersion } + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSverify, client.PlainHTTP) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + client.SetRegistryClient(registryClient) + + // This is for the case where "" is specifically passed in as a + // value. When there is no value passed in NoOptDefVal will be used + // and it is set to client. See addInstallFlags. + if client.DryRunOption == "" { + client.DryRunOption = "true" + } client.DryRun = true - client.ReleaseName = "RELEASE-NAME" + client.ReleaseName = "release-name" client.Replace = true // Skip the name check client.ClientOnly = !validate client.APIVersions = chartutil.VersionSet(extraAPIs) @@ -106,11 +119,15 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if client.UseReleaseName { newDir = filepath.Join(client.OutputDir, client.ReleaseName) } + _, err := os.Stat(filepath.Join(newDir, m.Path)) + if err == nil { + fileWritten[m.Path] = true + } + err = writeToFile(newDir, m.Path, m.Manifest, fileWritten[m.Path]) if err != nil { return err } - fileWritten[m.Path] = true } } @@ -181,7 +198,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion") - f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions") + f.StringSliceVarP(&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) diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go index 30ee2722b..e5b939879 100644 --- a/cmd/helm/template_test.go +++ b/cmd/helm/template_test.go @@ -25,6 +25,8 @@ import ( var chartPath = "testdata/testcharts/subchart" func TestTemplateCmd(t *testing.T) { + deletevalchart := "testdata/testcharts/issue-9027" + tests := []cmdTestCase{ { name: "check name", @@ -43,7 +45,7 @@ func TestTemplateCmd(t *testing.T) { }, { name: "check name template", - cmd: fmt.Sprintf(`template '%s' --name-template='foobar-{{ b64enc "abc" }}-baz'`, chartPath), + cmd: fmt.Sprintf(`template '%s' --name-template='foobar-{{ b64enc "abc" | lower }}-baz'`, chartPath), golden: "output/template-name-template.txt", }, { @@ -62,7 +64,7 @@ func TestTemplateCmd(t *testing.T) { name: "check chart bad type", cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-bad-type"), wantError: true, - golden: "output/install-chart-bad-type.txt", + golden: "output/template-chart-bad-type.txt", }, { name: "check chart with dependency which is an app chart acting as a library chart", @@ -131,6 +133,34 @@ func TestTemplateCmd(t *testing.T) { cmd: fmt.Sprintf(`template '%s' --skip-tests`, chartPath), golden: "output/template-skip-tests.txt", }, + { + // This test case is to ensure the case where specified dependencies + // in the Chart.yaml and those where the Chart.yaml don't have them + // specified are the same. + name: "ensure nil/null values pass to subcharts delete values", + cmd: fmt.Sprintf("template '%s'", deletevalchart), + golden: "output/issue-9027.txt", + }, + { + // Ensure that parent chart values take precedence over imported values + name: "template with imported subchart values ensuring import", + cmd: fmt.Sprintf("template '%s' --set configmap.enabled=true --set subchartb.enabled=true", chartPath), + golden: "output/template-subchart-cm.txt", + }, + { + // Ensure that user input values take precedence over imported + // values from sub-charts. + name: "template with imported subchart values set with --set", + cmd: fmt.Sprintf("template '%s' --set configmap.enabled=true --set subchartb.enabled=true --set configmap.value=baz", chartPath), + golden: "output/template-subchart-cm-set.txt", + }, + { + // Ensure that user input values take precedence over imported + // values from sub-charts when passed by file + name: "template with imported subchart values set with --set", + cmd: fmt.Sprintf("template '%s' -f %s/extra_values.yaml", chartPath, chartPath), + golden: "output/template-subchart-cm-set-file.txt", + }, } runTestCmd(t, tests) } diff --git a/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/completion.yaml b/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/completion.yaml new file mode 100644 index 000000000..e0b161c69 --- /dev/null +++ b/cmd/helm/testdata/helm home with space/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/helm home with space/helm/plugins/fullenv/fullenv.sh b/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/fullenv.sh new file mode 100755 index 000000000..2efad9b3c --- /dev/null +++ b/cmd/helm/testdata/helm home with space/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/helm home with space/helm/plugins/fullenv/plugin.yaml b/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml new file mode 100644 index 000000000..63f2f12db --- /dev/null +++ b/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml @@ -0,0 +1,4 @@ +name: fullenv +usage: "show env vars" +description: "show all env vars" +command: "$HELM_PLUGIN_DIR/fullenv.sh" diff --git a/cmd/helm/testdata/helm home with space/helm/repositories.yaml b/cmd/helm/testdata/helm home with space/helm/repositories.yaml new file mode 100644 index 000000000..e9de487d6 --- /dev/null +++ b/cmd/helm/testdata/helm home with space/helm/repositories.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +generated: 2016-10-03T16:03:10.640376913-06:00 +repositories: + - cache: testing-index.yaml + name: testing + url: http://example.com/charts diff --git a/cmd/helm/testdata/helm home with space/helm/repository/test-name-charts.txt b/cmd/helm/testdata/helm home with space/helm/repository/test-name-charts.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/helm/testdata/helm home with space/helm/repository/test-name-index.yaml b/cmd/helm/testdata/helm home with space/helm/repository/test-name-index.yaml new file mode 100644 index 000000000..d5ab620ad --- /dev/null +++ b/cmd/helm/testdata/helm home with space/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/helm home with space/helm/repository/testing-index.yaml b/cmd/helm/testdata/helm home with space/helm/repository/testing-index.yaml new file mode 100644 index 000000000..91e4d463f --- /dev/null +++ b/cmd/helm/testdata/helm home with space/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/helm/plugins/echo/plugin.complete b/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete index 6bc73d130..63569aada 100755 --- a/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete +++ b/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete @@ -7,8 +7,7 @@ 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" + echo ":0" # else # Don't include the directive, to test it is really optional fi diff --git a/cmd/helm/testdata/output/env-comp.txt b/cmd/helm/testdata/output/env-comp.txt index b7befd69e..b7d93c12e 100644 --- a/cmd/helm/testdata/output/env-comp.txt +++ b/cmd/helm/testdata/output/env-comp.txt @@ -1,4 +1,5 @@ HELM_BIN +HELM_BURST_LIMIT HELM_CACHE_HOME HELM_CONFIG_HOME HELM_DATA_HOME @@ -8,6 +9,8 @@ HELM_KUBEASGROUPS HELM_KUBEASUSER HELM_KUBECAFILE HELM_KUBECONTEXT +HELM_KUBEINSECURE_SKIP_TLS_VERIFY +HELM_KUBETLS_SERVER_NAME HELM_KUBETOKEN HELM_MAX_HISTORY HELM_NAMESPACE diff --git a/cmd/helm/testdata/output/get-metadata-args.txt b/cmd/helm/testdata/output/get-metadata-args.txt new file mode 100644 index 000000000..acd3f4c15 --- /dev/null +++ b/cmd/helm/testdata/output/get-metadata-args.txt @@ -0,0 +1,3 @@ +Error: "helm get metadata" requires 1 argument + +Usage: helm get metadata RELEASE_NAME [flags] diff --git a/cmd/helm/testdata/output/get-metadata.json b/cmd/helm/testdata/output/get-metadata.json new file mode 100644 index 000000000..1d5152b24 --- /dev/null +++ b/cmd/helm/testdata/output/get-metadata.json @@ -0,0 +1 @@ +{"name":"thomas-guide","chart":"foo","version":"0.1.0-beta.1","appVersion":"1.0","namespace":"default","revision":1,"status":"deployed","deployedAt":"1977-09-02T22:04:05Z"} diff --git a/cmd/helm/testdata/output/get-metadata.txt b/cmd/helm/testdata/output/get-metadata.txt new file mode 100644 index 000000000..b91f1b86a --- /dev/null +++ b/cmd/helm/testdata/output/get-metadata.txt @@ -0,0 +1,8 @@ +NAME: thomas-guide +CHART: foo +VERSION: 0.1.0-beta.1 +APP_VERSION: 1.0 +NAMESPACE: default +REVISION: 1 +STATUS: deployed +DEPLOYED_AT: 1977-09-02T22:04:05Z diff --git a/cmd/helm/testdata/output/get-metadata.yaml b/cmd/helm/testdata/output/get-metadata.yaml new file mode 100644 index 000000000..b6d49b038 --- /dev/null +++ b/cmd/helm/testdata/output/get-metadata.yaml @@ -0,0 +1,8 @@ +appVersion: "1.0" +chart: foo +deployedAt: "1977-09-02T22:04:05Z" +name: thomas-guide +namespace: default +revision: 1 +status: deployed +version: 0.1.0-beta.1 diff --git a/cmd/helm/testdata/output/get-release.txt b/cmd/helm/testdata/output/get-release.txt index f6c3b57eb..12b4a407a 100644 --- a/cmd/helm/testdata/output/get-release.txt +++ b/cmd/helm/testdata/output/get-release.txt @@ -3,6 +3,9 @@ LAST DEPLOYED: Fri Sep 2 22:04:05 1977 NAMESPACE: default STATUS: deployed REVISION: 1 +CHART: foo +VERSION: 0.1.0-beta.1 +APP_VERSION: 1.0 TEST SUITE: None USER-SUPPLIED VALUES: name: value diff --git a/cmd/helm/testdata/output/install-chart-bad-type.txt b/cmd/helm/testdata/output/install-chart-bad-type.txt index d8a3bf275..c482a793d 100644 --- a/cmd/helm/testdata/output/install-chart-bad-type.txt +++ b/cmd/helm/testdata/output/install-chart-bad-type.txt @@ -1 +1 @@ -Error: validation: chart.metadata.type must be application or library +Error: INSTALLATION FAILED: validation: chart.metadata.type must be application or library diff --git a/cmd/helm/testdata/output/install-lib-chart.txt b/cmd/helm/testdata/output/install-lib-chart.txt new file mode 100644 index 000000000..c482a793d --- /dev/null +++ b/cmd/helm/testdata/output/install-lib-chart.txt @@ -0,0 +1 @@ +Error: INSTALLATION FAILED: 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 index 67e06d92b..19952e3c2 100644 --- a/cmd/helm/testdata/output/install-name-template.txt +++ b/cmd/helm/testdata/output/install-name-template.txt @@ -1,4 +1,4 @@ -NAME: FOOBAR +NAME: foobar LAST DEPLOYED: Fri Sep 2 22:04:05 1977 NAMESPACE: default STATUS: deployed diff --git a/cmd/helm/testdata/output/issue-9027.txt b/cmd/helm/testdata/output/issue-9027.txt new file mode 100644 index 000000000..eb19fc383 --- /dev/null +++ b/cmd/helm/testdata/output/issue-9027.txt @@ -0,0 +1,32 @@ +--- +# Source: issue-9027/charts/subchart/templates/values.yaml +global: + hash: + key3: 13 + key4: 4 + key5: 5 + key6: 6 +hash: + key3: 13 + key4: 4 + key5: 5 + key6: 6 +--- +# Source: issue-9027/templates/values.yaml +global: + hash: + key1: null + key2: null + key3: 13 +subchart: + global: + hash: + key3: 13 + key4: 4 + key5: 5 + key6: 6 + hash: + key3: 13 + key4: 4 + key5: 5 + key6: 6 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 index e77aa387f..d43c7c361 100644 --- 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 @@ -1,6 +1,6 @@ ==> Linting testdata/testcharts/chart-with-bad-subcharts [INFO] Chart.yaml: icon is recommended -[WARNING] templates/: directory not found +[ERROR] templates/: error unpacking bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required [ERROR] : unable to load chart error unpacking bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required @@ -9,12 +9,11 @@ [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] templates/: validation: chart.metadata.name is required [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 index 265e555f7..7c898b89f 100644 --- a/cmd/helm/testdata/output/lint-chart-with-bad-subcharts.txt +++ b/cmd/helm/testdata/output/lint-chart-with-bad-subcharts.txt @@ -1,6 +1,6 @@ ==> Linting testdata/testcharts/chart-with-bad-subcharts [INFO] Chart.yaml: icon is recommended -[WARNING] templates/: directory not found +[ERROR] templates/: error unpacking bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required [ERROR] : unable to load chart error unpacking bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required diff --git a/cmd/helm/testdata/output/lint-quiet-with-error.txt b/cmd/helm/testdata/output/lint-quiet-with-error.txt new file mode 100644 index 000000000..e3d29a5a3 --- /dev/null +++ b/cmd/helm/testdata/output/lint-quiet-with-error.txt @@ -0,0 +1,8 @@ +==> Linting testdata/testcharts/chart-bad-requirements +[ERROR] Chart.yaml: unable to parse YAML + error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator +[ERROR] templates/: cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator +[ERROR] : unable to load chart + cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator + +Error: 2 chart(s) linted, 1 chart(s) failed diff --git a/cmd/helm/testdata/output/lint-quiet-with-warning.txt b/cmd/helm/testdata/output/lint-quiet-with-warning.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/helm/testdata/output/lint-quiet.txt b/cmd/helm/testdata/output/lint-quiet.txt new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/helm/testdata/output/list-no-headers.txt b/cmd/helm/testdata/output/list-no-headers.txt new file mode 100644 index 000000000..9d11d0caf --- /dev/null +++ b/cmd/helm/testdata/output/list-no-headers.txt @@ -0,0 +1,4 @@ +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/plugin_echo_bad_directive.txt b/cmd/helm/testdata/output/plugin_echo_bad_directive.txt deleted file mode 100644 index 9f2802581..000000000 --- a/cmd/helm/testdata/output/plugin_echo_bad_directive.txt +++ /dev/null @@ -1,6 +0,0 @@ -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/repo_list_comp.txt b/cmd/helm/testdata/output/repo_list_comp.txt new file mode 100644 index 000000000..289e0d2e1 --- /dev/null +++ b/cmd/helm/testdata/output/repo_list_comp.txt @@ -0,0 +1,5 @@ +foo +bar +baz +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/docs-type-filtered-comp.txt b/cmd/helm/testdata/output/repo_repeat_comp.txt similarity index 87% rename from cmd/helm/testdata/output/docs-type-filtered-comp.txt rename to cmd/helm/testdata/output/repo_repeat_comp.txt index 55104f32e..ed8ed89fa 100644 --- a/cmd/helm/testdata/output/docs-type-filtered-comp.txt +++ b/cmd/helm/testdata/output/repo_repeat_comp.txt @@ -1,3 +1,4 @@ -markdown +bar +baz :4 Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/rollback-non-existent-version.txt b/cmd/helm/testdata/output/rollback-non-existent-version.txt new file mode 100644 index 000000000..9c2e10e17 --- /dev/null +++ b/cmd/helm/testdata/output/rollback-non-existent-version.txt @@ -0,0 +1 @@ +Error: release has no 3 version diff --git a/cmd/helm/testdata/output/schema-negative-cli.txt b/cmd/helm/testdata/output/schema-negative-cli.txt index d6f096e14..c4a5cc516 100644 --- a/cmd/helm/testdata/output/schema-negative-cli.txt +++ b/cmd/helm/testdata/output/schema-negative-cli.txt @@ -1,4 +1,4 @@ -Error: values don't meet the specifications of the schema(s) in the following chart(s): +Error: INSTALLATION FAILED: 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 index f7c89dd56..929af5518 100644 --- a/cmd/helm/testdata/output/schema-negative.txt +++ b/cmd/helm/testdata/output/schema-negative.txt @@ -1,4 +1,4 @@ -Error: values don't meet the specifications of the schema(s) in the following chart(s): +Error: INSTALLATION FAILED: 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/search-not-found-error.txt b/cmd/helm/testdata/output/search-not-found-error.txt new file mode 100644 index 000000000..8b586bea3 --- /dev/null +++ b/cmd/helm/testdata/output/search-not-found-error.txt @@ -0,0 +1 @@ +Error: no results found diff --git a/cmd/helm/testdata/output/status-comp.txt b/cmd/helm/testdata/output/status-comp.txt index 4f56ab30a..4c408c974 100644 --- a/cmd/helm/testdata/output/status-comp.txt +++ b/cmd/helm/testdata/output/status-comp.txt @@ -1,4 +1,5 @@ aramis Aramis-chart-0.0.0 -> uninstalled athos Athos-chart-1.2.3 -> deployed +porthos Porthos-chart-111.222.333 -> failed :4 Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/cmd/helm/testdata/output/status-with-resources.json b/cmd/helm/testdata/output/status-with-resources.json new file mode 100644 index 000000000..275e0cfc6 --- /dev/null +++ b/cmd/helm/testdata/output/status-with-resources.json @@ -0,0 +1 @@ +{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed"},"namespace":"default"} diff --git a/cmd/helm/testdata/output/status-with-resources.txt b/cmd/helm/testdata/output/status-with-resources.txt new file mode 100644 index 000000000..a326c3db0 --- /dev/null +++ b/cmd/helm/testdata/output/status-with-resources.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 index c0883a8e8..7396b4bfe 100644 --- a/cmd/helm/testdata/output/subchart-schema-cli-negative.txt +++ b/cmd/helm/testdata/output/subchart-schema-cli-negative.txt @@ -1,4 +1,4 @@ -Error: values don't meet the specifications of the schema(s) in the following chart(s): +Error: INSTALLATION FAILED: 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-negative.txt b/cmd/helm/testdata/output/subchart-schema-negative.txt index 5a84170fd..7b1f654a2 100644 --- a/cmd/helm/testdata/output/subchart-schema-negative.txt +++ b/cmd/helm/testdata/output/subchart-schema-negative.txt @@ -1,4 +1,4 @@ -Error: values don't meet the specifications of the schema(s) in the following chart(s): +Error: INSTALLATION FAILED: 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: diff --git a/cmd/helm/testdata/output/template-chart-bad-type.txt b/cmd/helm/testdata/output/template-chart-bad-type.txt new file mode 100644 index 000000000..d8a3bf275 --- /dev/null +++ b/cmd/helm/testdata/output/template-chart-bad-type.txt @@ -0,0 +1 @@ +Error: validation: chart.metadata.type must be application or library 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 index dc1aa2907..c954b8e14 100644 --- 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 @@ -7,7 +7,7 @@ metadata: app: chart-with-template-lib-archive-dep chart: chart-with-template-lib-archive-dep-0.1.0 heritage: Helm - release: RELEASE-NAME + release: release-name name: release-name-chart-with-template-lib-archive-dep spec: ports: @@ -16,30 +16,30 @@ spec: targetPort: http selector: app: chart-with-template-lib-archive-dep - release: RELEASE-NAME + 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 + 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 + release: release-name heritage: Helm spec: replicas: 1 selector: matchLabels: app: chart-with-template-lib-archive-dep - release: RELEASE-NAME + release: release-name template: metadata: labels: app: chart-with-template-lib-archive-dep - release: RELEASE-NAME + release: release-name spec: containers: - name: chart-with-template-lib-archive-dep 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 index 12adeb28b..74a2a2df8 100644 --- a/cmd/helm/testdata/output/template-chart-with-template-lib-dep.txt +++ b/cmd/helm/testdata/output/template-chart-with-template-lib-dep.txt @@ -7,7 +7,7 @@ metadata: app: chart-with-template-lib-dep chart: chart-with-template-lib-dep-0.1.0 heritage: Helm - release: RELEASE-NAME + release: release-name name: release-name-chart-with-template-lib-dep spec: ports: @@ -16,30 +16,30 @@ spec: targetPort: http selector: app: chart-with-template-lib-dep - release: RELEASE-NAME + 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 + 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 + release: release-name heritage: Helm spec: replicas: 1 selector: matchLabels: app: chart-with-template-lib-dep - release: RELEASE-NAME + release: release-name template: metadata: labels: app: chart-with-template-lib-dep - release: RELEASE-NAME + release: release-name spec: containers: - name: chart-with-template-lib-dep diff --git a/cmd/helm/testdata/output/template-name-template.txt b/cmd/helm/testdata/output/template-name-template.txt index b9e7cbbe4..9406048dd 100644 --- a/cmd/helm/testdata/output/template-name-template.txt +++ b/cmd/helm/testdata/output/template-name-template.txt @@ -70,7 +70,7 @@ metadata: name: subchart labels: helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "foobar-YWJj-baz" + app.kubernetes.io/instance: "foobar-ywjj-baz" kube-version/major: "1" kube-version/minor: "20" kube-version/version: "v1.20.0" @@ -88,7 +88,7 @@ spec: apiVersion: v1 kind: ConfigMap metadata: - name: "foobar-YWJj-baz-testconfig" + name: "foobar-ywjj-baz-testconfig" annotations: "helm.sh/hook": test data: @@ -98,7 +98,7 @@ data: apiVersion: v1 kind: Pod metadata: - name: "foobar-YWJj-baz-test" + name: "foobar-ywjj-baz-test" annotations: "helm.sh/hook": test spec: @@ -107,7 +107,7 @@ spec: image: "alpine:latest" envFrom: - configMapRef: - name: "foobar-YWJj-baz-testconfig" + name: "foobar-ywjj-baz-testconfig" command: - echo - "$message" diff --git a/cmd/helm/testdata/output/template-set.txt b/cmd/helm/testdata/output/template-set.txt index 177d8e58c..4040991cf 100644 --- a/cmd/helm/testdata/output/template-set.txt +++ b/cmd/helm/testdata/output/template-set.txt @@ -70,7 +70,7 @@ metadata: name: subchart labels: helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" kube-version/major: "1" kube-version/minor: "20" kube-version/version: "v1.20.0" @@ -88,7 +88,7 @@ spec: apiVersion: v1 kind: ConfigMap metadata: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" annotations: "helm.sh/hook": test data: @@ -98,7 +98,7 @@ data: apiVersion: v1 kind: Pod metadata: - name: "RELEASE-NAME-test" + name: "release-name-test" annotations: "helm.sh/hook": test spec: @@ -107,7 +107,7 @@ spec: image: "alpine:latest" envFrom: - configMapRef: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" command: - echo - "$message" diff --git a/cmd/helm/testdata/output/template-show-only-multiple.txt b/cmd/helm/testdata/output/template-show-only-multiple.txt index 81a1d8e3c..1aac3081a 100644 --- a/cmd/helm/testdata/output/template-show-only-multiple.txt +++ b/cmd/helm/testdata/output/template-show-only-multiple.txt @@ -6,7 +6,7 @@ metadata: name: subchart labels: helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" kube-version/major: "1" kube-version/minor: "20" kube-version/version: "v1.20.0" diff --git a/cmd/helm/testdata/output/template-show-only-one.txt b/cmd/helm/testdata/output/template-show-only-one.txt index a354bc0cc..9cc34f515 100644 --- a/cmd/helm/testdata/output/template-show-only-one.txt +++ b/cmd/helm/testdata/output/template-show-only-one.txt @@ -6,7 +6,7 @@ metadata: name: subchart labels: helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" kube-version/major: "1" kube-version/minor: "20" kube-version/version: "v1.20.0" diff --git a/cmd/helm/testdata/output/template-skip-tests.txt b/cmd/helm/testdata/output/template-skip-tests.txt index 8e8181474..5c907b563 100644 --- a/cmd/helm/testdata/output/template-skip-tests.txt +++ b/cmd/helm/testdata/output/template-skip-tests.txt @@ -70,7 +70,7 @@ metadata: name: subchart labels: helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" kube-version/major: "1" kube-version/minor: "20" kube-version/version: "v1.20.0" diff --git a/cmd/helm/testdata/output/template-subchart-cm-set-file.txt b/cmd/helm/testdata/output/template-subchart-cm-set-file.txt new file mode 100644 index 000000000..56844e292 --- /dev/null +++ b/cmd/helm/testdata/output/template-subchart-cm-set-file.txt @@ -0,0 +1,122 @@ +--- +# Source: subchart/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart-sa +--- +# Source: subchart/templates/subdir/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: subchart-cm +data: + value: qux +--- +# 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/template-subchart-cm-set.txt b/cmd/helm/testdata/output/template-subchart-cm-set.txt new file mode 100644 index 000000000..e52f7c234 --- /dev/null +++ b/cmd/helm/testdata/output/template-subchart-cm-set.txt @@ -0,0 +1,122 @@ +--- +# Source: subchart/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart-sa +--- +# Source: subchart/templates/subdir/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: subchart-cm +data: + value: baz +--- +# 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/template-subchart-cm.txt b/cmd/helm/testdata/output/template-subchart-cm.txt new file mode 100644 index 000000000..9cc9e2296 --- /dev/null +++ b/cmd/helm/testdata/output/template-subchart-cm.txt @@ -0,0 +1,122 @@ +--- +# Source: subchart/templates/subdir/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: subchart-sa +--- +# Source: subchart/templates/subdir/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: subchart-cm +data: + value: foo +--- +# 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/template-values-files.txt b/cmd/helm/testdata/output/template-values-files.txt index 177d8e58c..4040991cf 100644 --- a/cmd/helm/testdata/output/template-values-files.txt +++ b/cmd/helm/testdata/output/template-values-files.txt @@ -70,7 +70,7 @@ metadata: name: subchart labels: helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" kube-version/major: "1" kube-version/minor: "20" kube-version/version: "v1.20.0" @@ -88,7 +88,7 @@ spec: apiVersion: v1 kind: ConfigMap metadata: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" annotations: "helm.sh/hook": test data: @@ -98,7 +98,7 @@ data: apiVersion: v1 kind: Pod metadata: - name: "RELEASE-NAME-test" + name: "release-name-test" annotations: "helm.sh/hook": test spec: @@ -107,7 +107,7 @@ spec: image: "alpine:latest" envFrom: - configMapRef: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" command: - echo - "$message" diff --git a/cmd/helm/testdata/output/template-with-api-version.txt b/cmd/helm/testdata/output/template-with-api-version.txt index 4b2d4ee84..7e1c35001 100644 --- a/cmd/helm/testdata/output/template-with-api-version.txt +++ b/cmd/helm/testdata/output/template-with-api-version.txt @@ -70,7 +70,7 @@ metadata: name: subchart labels: helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" kube-version/major: "1" kube-version/minor: "20" kube-version/version: "v1.20.0" @@ -89,7 +89,7 @@ spec: apiVersion: v1 kind: ConfigMap metadata: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" annotations: "helm.sh/hook": test data: @@ -99,7 +99,7 @@ data: apiVersion: v1 kind: Pod metadata: - name: "RELEASE-NAME-test" + name: "release-name-test" annotations: "helm.sh/hook": test spec: @@ -108,7 +108,7 @@ spec: image: "alpine:latest" envFrom: - configMapRef: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" command: - echo - "$message" diff --git a/cmd/helm/testdata/output/template-with-crds.txt b/cmd/helm/testdata/output/template-with-crds.txt index 758e2e39f..256fc7c3b 100644 --- a/cmd/helm/testdata/output/template-with-crds.txt +++ b/cmd/helm/testdata/output/template-with-crds.txt @@ -1,5 +1,5 @@ --- -# Source: crds/crdA.yaml +# Source: subchart/crds/crdA.yaml apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: @@ -87,7 +87,7 @@ metadata: name: subchart labels: helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" kube-version/major: "1" kube-version/minor: "20" kube-version/version: "v1.20.0" @@ -105,7 +105,7 @@ spec: apiVersion: v1 kind: ConfigMap metadata: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" annotations: "helm.sh/hook": test data: @@ -115,7 +115,7 @@ data: apiVersion: v1 kind: Pod metadata: - name: "RELEASE-NAME-test" + name: "release-name-test" annotations: "helm.sh/hook": test spec: @@ -124,7 +124,7 @@ spec: image: "alpine:latest" envFrom: - configMapRef: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" command: - echo - "$message" diff --git a/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt b/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt index c1f51185c..909c543d3 100644 --- a/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt +++ b/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt @@ -3,7 +3,7 @@ apiVersion: v1 kind: Pod metadata: - name: "RELEASE-NAME-my-alpine" + name: "release-name-my-alpine" spec: containers: - name: waiter diff --git a/cmd/helm/testdata/output/template-with-kube-version.txt b/cmd/helm/testdata/output/template-with-kube-version.txt index 836905268..9d326f328 100644 --- a/cmd/helm/testdata/output/template-with-kube-version.txt +++ b/cmd/helm/testdata/output/template-with-kube-version.txt @@ -70,7 +70,7 @@ metadata: name: subchart labels: helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" kube-version/major: "1" kube-version/minor: "16" kube-version/version: "v1.16.0" @@ -88,7 +88,7 @@ spec: apiVersion: v1 kind: ConfigMap metadata: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" annotations: "helm.sh/hook": test data: @@ -98,7 +98,7 @@ data: apiVersion: v1 kind: Pod metadata: - name: "RELEASE-NAME-test" + name: "release-name-test" annotations: "helm.sh/hook": test spec: @@ -107,7 +107,7 @@ spec: image: "alpine:latest" envFrom: - configMapRef: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" command: - echo - "$message" diff --git a/cmd/helm/testdata/output/template.txt b/cmd/helm/testdata/output/template.txt index 4146a0749..58c480b47 100644 --- a/cmd/helm/testdata/output/template.txt +++ b/cmd/helm/testdata/output/template.txt @@ -70,7 +70,7 @@ metadata: name: subchart labels: helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" kube-version/major: "1" kube-version/minor: "20" kube-version/version: "v1.20.0" @@ -88,7 +88,7 @@ spec: apiVersion: v1 kind: ConfigMap metadata: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" annotations: "helm.sh/hook": test data: @@ -98,7 +98,7 @@ data: apiVersion: v1 kind: Pod metadata: - name: "RELEASE-NAME-test" + name: "release-name-test" annotations: "helm.sh/hook": test spec: @@ -107,7 +107,7 @@ spec: image: "alpine:latest" envFrom: - configMapRef: - name: "RELEASE-NAME-testconfig" + name: "release-name-testconfig" command: - echo - "$message" diff --git a/cmd/helm/testdata/output/uninstall-wait.txt b/cmd/helm/testdata/output/uninstall-wait.txt new file mode 100644 index 000000000..f5454b88d --- /dev/null +++ b/cmd/helm/testdata/output/uninstall-wait.txt @@ -0,0 +1 @@ +release "aeneas" uninstalled diff --git a/cmd/helm/testdata/output/upgrade-with-missing-dependencies.txt b/cmd/helm/testdata/output/upgrade-with-missing-dependencies.txt index de62e1d2a..adf2ae899 100644 --- a/cmd/helm/testdata/output/upgrade-with-missing-dependencies.txt +++ b/cmd/helm/testdata/output/upgrade-with-missing-dependencies.txt @@ -1 +1 @@ -Error: found in Chart.yaml, but missing in charts/ directory: reqsubchart2 +Error: An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: found in Chart.yaml, but missing in charts/ directory: reqsubchart2 diff --git a/cmd/helm/testdata/output/version-client-shorthand.txt b/cmd/helm/testdata/output/version-client-shorthand.txt index 02e210fb7..9a42dcba7 100644 --- a/cmd/helm/testdata/output/version-client-shorthand.txt +++ b/cmd/helm/testdata/output/version-client-shorthand.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.6", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v3.13", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/output/version-client.txt b/cmd/helm/testdata/output/version-client.txt index 02e210fb7..9a42dcba7 100644 --- a/cmd/helm/testdata/output/version-client.txt +++ b/cmd/helm/testdata/output/version-client.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.6", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v3.13", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/output/version-short.txt b/cmd/helm/testdata/output/version-short.txt index cde1fbc45..588b5b7c5 100644 --- a/cmd/helm/testdata/output/version-short.txt +++ b/cmd/helm/testdata/output/version-short.txt @@ -1 +1 @@ -v3.6 +v3.13 diff --git a/cmd/helm/testdata/output/version-template.txt b/cmd/helm/testdata/output/version-template.txt index 84b18182b..b6f541d94 100644 --- a/cmd/helm/testdata/output/version-template.txt +++ b/cmd/helm/testdata/output/version-template.txt @@ -1 +1 @@ -Version: v3.6 \ No newline at end of file +Version: v3.13 \ No newline at end of file diff --git a/cmd/helm/testdata/output/version.txt b/cmd/helm/testdata/output/version.txt index 02e210fb7..9a42dcba7 100644 --- a/cmd/helm/testdata/output/version.txt +++ b/cmd/helm/testdata/output/version.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v3.6", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v3.13", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/cmd/helm/testdata/password b/cmd/helm/testdata/password new file mode 100644 index 000000000..f3097ab13 --- /dev/null +++ b/cmd/helm/testdata/password @@ -0,0 +1 @@ +password diff --git a/cmd/helm/testdata/repositories.yaml b/cmd/helm/testdata/repositories.yaml index 423b9f195..6be26b771 100644 --- a/cmd/helm/testdata/repositories.yaml +++ b/cmd/helm/testdata/repositories.yaml @@ -2,3 +2,8 @@ apiVersion: v1 repositories: - name: charts url: "https://charts.helm.sh/stable" + - name: firstexample + url: "http://firstexample.com" + - name: secondexample + url: "http://secondexample.com" + 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 index ca0459474..0e06414d6 100755 --- 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 @@ -13,7 +13,7 @@ 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. +In this document, we use `release-name` as the name of the release. ## Resource Kinds @@ -733,7 +733,7 @@ metadata: labels: app: metadata heritage: "Tiller" - release: "RELEASE-NAME" + release: "release-name" chart: metadata-0.1.0 first: "matt" last: "butcher" @@ -748,7 +748,7 @@ metadata: labels: app: metadata heritage: "Tiller" - release: "RELEASE-NAME" + release: "release-name" chart: metadata-0.1.0 annotations: ``` @@ -791,7 +791,7 @@ Example output: ```yaml app: labelizer heritage: "Tiller" -release: "RELEASE-NAME" +release: "release-name" chart: labelizer-0.1.0 ``` diff --git a/cmd/helm/testdata/testcharts/issue-9027/Chart.yaml b/cmd/helm/testdata/testcharts/issue-9027/Chart.yaml new file mode 100644 index 000000000..ea6761a1c --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-9027/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: issue-9027 +version: 0.1.0 +dependencies: + - name: subchart + version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/Chart.yaml b/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/Chart.yaml new file mode 100644 index 000000000..0639b1806 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: subchart +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/templates/values.yaml b/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/templates/values.yaml new file mode 100644 index 000000000..fe0018e1a --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/templates/values.yaml @@ -0,0 +1 @@ +{{ .Values | toYaml }} diff --git a/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/values.yaml b/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/values.yaml new file mode 100644 index 000000000..0da524211 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/values.yaml @@ -0,0 +1,17 @@ +global: + hash: + key1: 1 + key2: 2 + key3: 3 + key4: 4 + key5: 5 + key6: 6 + + +hash: + key1: 1 + key2: 2 + key3: 3 + key4: 4 + key5: 5 + key6: 6 diff --git a/cmd/helm/testdata/testcharts/issue-9027/templates/values.yaml b/cmd/helm/testdata/testcharts/issue-9027/templates/values.yaml new file mode 100644 index 000000000..fe0018e1a --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-9027/templates/values.yaml @@ -0,0 +1 @@ +{{ .Values | toYaml }} diff --git a/cmd/helm/testdata/testcharts/issue-9027/values.yaml b/cmd/helm/testdata/testcharts/issue-9027/values.yaml new file mode 100644 index 000000000..22577e4f8 --- /dev/null +++ b/cmd/helm/testdata/testcharts/issue-9027/values.yaml @@ -0,0 +1,11 @@ +global: + hash: + key1: null + key2: null + key3: 13 + +subchart: + hash: + key1: null + key2: null + key3: 13 diff --git a/cmd/helm/testdata/testcharts/lib-chart/README.md b/cmd/helm/testdata/testcharts/lib-chart/README.md index aca257924..87b753f25 100644 --- a/cmd/helm/testdata/testcharts/lib-chart/README.md +++ b/cmd/helm/testdata/testcharts/lib-chart/README.md @@ -13,7 +13,7 @@ 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. +In this document, we use `release-name` as the name of the release. ## Resource Kinds @@ -733,7 +733,7 @@ metadata: labels: app.kubernetes.io/name: metadata app.kubernetes.io/managed-by: "Helm" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" helm.sh/chart: metadata-0.1.0 first: "matt" last: "butcher" @@ -748,7 +748,7 @@ metadata: labels: app.kubernetes.io/name: metadata app.kubernetes.io/managed-by: "Helm" - app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/instance: "release-name" helm.sh/chart: metadata-0.1.0 annotations: ``` @@ -791,7 +791,7 @@ Example output: ```yaml app.kubernetes.io/name: labelizer app.kubernetes.io/managed-by: "Tiller" -app.kubernetes.io/instance: "RELEASE-NAME" +app.kubernetes.io/instance: "release-name" helm.sh/chart: labelizer-0.1.0 ``` diff --git a/cmd/helm/testdata/testcharts/subchart/Chart.yaml b/cmd/helm/testdata/testcharts/subchart/Chart.yaml index b03ea3cd3..ae844c349 100644 --- a/cmd/helm/testdata/testcharts/subchart/Chart.yaml +++ b/cmd/helm/testdata/testcharts/subchart/Chart.yaml @@ -29,6 +29,9 @@ dependencies: parent: imported-chartA-B - child: exports.SCBexported2 parent: exports.SCBexported2 + # - child: exports.configmap + # parent: configmap + - configmap - SCBexported1 tags: diff --git a/cmd/helm/testdata/testcharts/subchart/charts/subchartB/values.yaml b/cmd/helm/testdata/testcharts/subchart/charts/subchartB/values.yaml index 774fdd75c..0ada0aadc 100644 --- a/cmd/helm/testdata/testcharts/subchart/charts/subchartB/values.yaml +++ b/cmd/helm/testdata/testcharts/subchart/charts/subchartB/values.yaml @@ -20,6 +20,10 @@ exports: SCBexported2: SCBexported2A: "blaster" + + configmap: + configmap: + value: "bar" global: kolla: diff --git a/cmd/helm/testdata/testcharts/subchart/extra_values.yaml b/cmd/helm/testdata/testcharts/subchart/extra_values.yaml new file mode 100644 index 000000000..5976bd178 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/extra_values.yaml @@ -0,0 +1,5 @@ +# This file is used to test values passed by file at the command line + +configmap: + enabled: true + value: "qux" \ No newline at end of file diff --git a/cmd/helm/testdata/testcharts/subchart/templates/subdir/configmap.yaml b/cmd/helm/testdata/testcharts/subchart/templates/subdir/configmap.yaml new file mode 100644 index 000000000..e404a6cb2 --- /dev/null +++ b/cmd/helm/testdata/testcharts/subchart/templates/subdir/configmap.yaml @@ -0,0 +1,8 @@ +{{ if .Values.configmap.enabled -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Chart.Name }}-cm +data: + value: {{ .Values.configmap.value }} +{{- end }} \ No newline at end of file diff --git a/cmd/helm/testdata/testcharts/subchart/values.yaml b/cmd/helm/testdata/testcharts/subchart/values.yaml index 8a3ab6c64..bcbebb5c0 100644 --- a/cmd/helm/testdata/testcharts/subchart/values.yaml +++ b/cmd/helm/testdata/testcharts/subchart/values.yaml @@ -53,3 +53,7 @@ exports: SC1exported2: all: SC1exported3: "SC1expstr" + +configmap: + enabled: false + value: "foo" diff --git a/cmd/helm/uninstall.go b/cmd/helm/uninstall.go index f4f5d87e8..9ced8fef0 100644 --- a/cmd/helm/uninstall.go +++ b/cmd/helm/uninstall.go @@ -51,6 +51,10 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return compListReleases(toComplete, args, cfg) }, RunE: func(cmd *cobra.Command, args []string) error { + validationErr := validateCascadeFlag(client) + if validationErr != nil { + return validationErr + } for i := 0; i < len(args); i++ { res, err := client.Run(args[i]) @@ -70,9 +74,19 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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.IgnoreNotFound, "ignore-not-found", false, `Treat "release not found" as a successful uninstall`) f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history") + f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout") + f.StringVar(&client.DeletionPropagation, "cascade", "background", "Must be \"background\", \"orphan\", or \"foreground\". Selects the deletion cascading strategy for the dependents. Defaults to background.") 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 } + +func validateCascadeFlag(client *action.Uninstall) error { + if client.DeletionPropagation != "background" && client.DeletionPropagation != "foreground" && client.DeletionPropagation != "orphan" { + return fmt.Errorf("invalid cascade value (%s). Must be \"background\", \"foreground\", or \"orphan\"", client.DeletionPropagation) + } + return nil +} diff --git a/cmd/helm/uninstall_test.go b/cmd/helm/uninstall_test.go index 1a33458c4..23b61058e 100644 --- a/cmd/helm/uninstall_test.go +++ b/cmd/helm/uninstall_test.go @@ -57,6 +57,12 @@ func TestUninstall(t *testing.T) { golden: "output/uninstall-keep-history.txt", rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, }, + { + name: "wait", + cmd: "uninstall aeneas --wait", + golden: "output/uninstall-wait.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "aeneas"})}, + }, { name: "uninstall without release", cmd: "uninstall", diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 3bd392d1d..328497d7e 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -17,9 +17,13 @@ limitations under the License. package main import ( + "context" "fmt" "io" "log" + "os" + "os/signal" + "syscall" "time" "github.com/pkg/errors" @@ -45,9 +49,10 @@ 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'. 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. +values, use '--set-string'. You can use '--set-file' to set individual +values from a file when the value itself is too long for the command line +or is dynamically generated. You can also use '--set-json' to set json values +(scalars/objects/arrays) from the command line. 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 @@ -60,6 +65,13 @@ last (right-most) set specified. For example, if both 'bar' and 'newbar' values set for a key called 'foo', the 'newbar' value would take precedence: $ helm upgrade --set foo=bar --set foo=newbar redis ./redis + +You can update the values for an existing release with this command as well via the +'--reuse-values' flag. The 'RELEASE' and 'CHART' arguments should be set to the original +parameters, and existing values will be merged with any values set via '--values'/'-f' +or '--set' flags. Priority is given to new values. + + $ helm upgrade --reuse-values --set foo=bar --set foo=newbar redis ./redis ` func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { @@ -85,6 +97,19 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { client.Namespace = settings.Namespace() + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSverify, client.PlainHTTP) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + client.SetRegistryClient(registryClient) + + // This is for the case where "" is specifically passed in as a + // value. When there is no value passed in NoOptDefVal will be used + // and it is set to client. See addInstallFlags. + if client.DryRunOption == "" { + client.DryRunOption = "none" + } // 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 { @@ -99,7 +124,9 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient := action.NewInstall(cfg) instClient.CreateNamespace = createNamespace instClient.ChartPathOptions = client.ChartPathOptions + instClient.Force = client.Force instClient.DryRun = client.DryRun + instClient.DryRunOption = client.DryRunOption instClient.DisableHooks = client.DisableHooks instClient.SkipCRDs = client.SkipCRDs instClient.Timeout = client.Timeout @@ -112,12 +139,15 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.DisableOpenAPIValidation = client.DisableOpenAPIValidation instClient.SubNotes = client.SubNotes instClient.Description = client.Description + instClient.DependencyUpdate = client.DependencyUpdate + instClient.Labels = client.Labels + instClient.EnableDNS = client.EnableDNS rel, err := runInstall(args, instClient, valueOpts, out) if err != nil { return err } - return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}) } else if err != nil { return err } @@ -132,6 +162,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } + // Validate dry-run flag value is one of the allowed values + if err := validateDryRunOptionFlag(client.DryRunOption); err != nil { + return err + } p := getter.All(settings) vals, err := valueOpts.MergeValues(p) @@ -146,6 +180,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } if req := ch.Metadata.Dependencies; req != nil { if err := action.CheckDependencies(ch, req); err != nil { + err = errors.Wrap(err, "An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies") if client.DependencyUpdate { man := &downloader.Manager{ Out: out, @@ -174,7 +209,22 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { warning("This chart is deprecated") } - rel, err := client.Run(args[0], ch, vals) + // Create context and prepare the handle of SIGTERM + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + // Set up channel on which to send signal notifications. + // We must use a buffered channel or risk missing the signal + // if we're not ready to receive when the signal is sent. + cSignal := make(chan os.Signal, 2) + signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) + go func() { + <-cSignal + fmt.Fprintf(out, "Release %s has been cancelled.\n", args[0]) + cancel() + }() + + rel, err := client.RunWithContext(ctx, args[0], ch, vals) if err != nil { return errors.Wrap(err, "UPGRADE FAILED") } @@ -183,7 +233,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0]) } - return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}) }, } @@ -191,7 +241,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.") + f.Lookup("dry-run").NoOptDefVal = "client" 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") @@ -207,8 +258,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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.StringToStringVarP(&client.Labels, "labels", "l", nil, "Labels that would be added to release metadata. Should be separated by comma. Original release labels will be merged with upgrade labels. You can unset label using null.") f.StringVar(&client.Description, "description", "", "add a custom description") f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart") + f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates") addChartPathOptionsFlags(f, &client.ChartPathOptions) addValueOptionsFlags(f, valueOpts) bindOutputFlag(cmd, &outfmt) diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index fc2a22d7d..485267d1d 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -18,13 +18,12 @@ package main import ( "fmt" - "io/ioutil" "os" "path/filepath" + "reflect" "strings" "testing" - "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" @@ -33,7 +32,7 @@ import ( func TestUpgradeCmd(t *testing.T) { - tmpChart := ensure.TempDir(t) + tmpChart := t.TempDir() cfile := &chart.Chart{ Metadata: &chart.Metadata{ APIVersion: chart.APIVersionV1, @@ -358,8 +357,8 @@ func TestUpgradeInstallWithValuesFromStdin(t *testing.T) { } 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") + tmpChart := t.TempDir() + configmapData, err := os.ReadFile("testdata/testcharts/upgradetest/templates/configmap.yaml") if err != nil { t.Fatalf("Error loading template yaml %v", err) } @@ -406,6 +405,10 @@ func TestUpgradeVersionCompletion(t *testing.T) { 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, no filter", + cmd: fmt.Sprintf("%s __complete upgrade releasename testing/alpine --version 0.3", repoSetup), + golden: "output/version-comp.txt", }, { name: "completion for upgrade version flag too few args", cmd: fmt.Sprintf("%s __complete upgrade releasename --version ''", repoSetup), @@ -427,3 +430,31 @@ func TestUpgradeFileCompletion(t *testing.T) { checkFileCompletion(t, "upgrade myrelease", true) checkFileCompletion(t, "upgrade myrelease repo/chart", false) } + +func TestUpgradeInstallWithLabels(t *testing.T) { + releaseName := "funny-bunny-labels" + _, _, chartPath := prepareMockRelease(releaseName, t) + + defer resetEnv()() + + store := storageFixture() + + expectedLabels := map[string]string{ + "key1": "val1", + "key2": "val2", + } + cmd := fmt.Sprintf("upgrade %s --install --labels key1=val1,key2=val2 '%s'", releaseName, chartPath) + _, _, err := executeActionCommandC(store, 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 !reflect.DeepEqual(updatedRel.Labels, expectedLabels) { + t.Errorf("Expected {%v}, got {%v}", expectedLabels, updatedRel.Labels) + } +} diff --git a/go.mod b/go.mod index 12f211004..c169a6283 100644 --- a/go.mod +++ b/go.mod @@ -1,50 +1,164 @@ module helm.sh/helm/v3 -go 1.16 +go 1.19 require ( - github.com/BurntSushi/toml v0.3.1 + github.com/BurntSushi/toml v1.3.2 github.com/DATA-DOG/go-sqlmock v1.5.0 - 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/Masterminds/semver/v3 v3.2.1 + github.com/Masterminds/sprig/v3 v3.2.3 + github.com/Masterminds/squirrel v1.5.4 + github.com/Masterminds/vcs v1.13.3 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.11.1 - github.com/docker/distribution v2.7.1+incompatible - github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible - github.com/docker/go-units v0.4.0 - github.com/evanphx/json-patch v4.9.0+incompatible + github.com/containerd/containerd v1.7.6 + github.com/cyphar/filepath-securejoin v0.2.4 + github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 + github.com/evanphx/json-patch v5.6.0+incompatible + github.com/foxcpp/go-mockdns v1.0.0 github.com/gobwas/glob v0.2.3 - github.com/gofrs/flock v0.8.0 + github.com/gofrs/flock v0.8.1 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/hashicorp/go-multierror v1.1.1 + github.com/jmoiron/sqlx v1.3.5 + github.com/lib/pq v1.10.9 + github.com/mattn/go-shellwords v1.0.12 + github.com/mitchellh/copystructure v1.2.0 + github.com/moby/term v0.5.0 + github.com/opencontainers/image-spec v1.1.0-rc5 + github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 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/rubenv/sql-migrate v1.5.2 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.8.4 github.com/xeipuuv/gojsonschema v1.2.0 - golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 - golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d - k8s.io/api v0.21.0 - k8s.io/apiextensions-apiserver v0.21.0 - k8s.io/apimachinery v0.21.0 - k8s.io/apiserver v0.21.0 - k8s.io/cli-runtime v0.21.0 - k8s.io/client-go v0.21.0 - k8s.io/klog/v2 v2.8.0 - k8s.io/kubectl v0.21.0 - sigs.k8s.io/yaml v1.2.0 + golang.org/x/crypto v0.13.0 + golang.org/x/term v0.12.0 + golang.org/x/text v0.13.0 + k8s.io/api v0.28.2 + k8s.io/apiextensions-apiserver v0.28.2 + k8s.io/apimachinery v0.28.2 + k8s.io/apiserver v0.28.2 + k8s.io/cli-runtime v0.28.2 + k8s.io/client-go v0.28.2 + k8s.io/klog/v2 v2.100.1 + k8s.io/kubectl v0.28.2 + oras.land/oras-go v1.2.4 + sigs.k8s.io/yaml v1.3.0 ) -replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d +require ( + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Microsoft/hcsshim v0.11.0 // indirect + github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect + github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd // indirect + github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b // indirect + github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v24.0.6+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.6+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect + github.com/emicklei/go-restful/v3 v3.10.1 // indirect + github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/fvbommel/sortorder v1.1.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/gomodule/redigo v1.8.2 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/handlers v1.5.1 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/miekg/dns v1.1.25 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 // indirect + github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 // indirect + github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f // indirect + go.opentelemetry.io/otel v1.14.0 // indirect + go.opentelemetry.io/otel/trace v1.14.0 // indirect + go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect + golang.org/x/net v0.13.0 // indirect + golang.org/x/oauth2 v0.8.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect + google.golang.org/grpc v1.54.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/component-base v0.28.2 // indirect + k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect + k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect + sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect +) diff --git a/go.sum b/go.sum index b5268076e..974a2c68d 100644 --- a/go.sum +++ b/go.sum @@ -1,472 +1,224 @@ -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.12 h1:gI8ytXbxMfI+IVbI9mP2JGCTXIuhHLgRlvQ9X4PsnHE= -github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -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.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/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 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/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 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/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= 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/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -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/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE= +github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/hcsshim v0.11.0 h1:7EFNIY4igHEXUdj1zXgAyU3fLc7QfOKHbkldRVTBdiM= +github.com/Microsoft/hcsshim v0.11.0/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM= 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/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= 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/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -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-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 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/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/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= 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.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/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/containerd v1.7.6 h1:oNAVsnhPoy4BTPQivLgTzI9Oleml9l/+eYIDYXRCYo8= +github.com/containerd/containerd v1.7.6/go.mod h1:SY6lrkkuJT40BVNO37tlYTSnKJnP5AXBc0fhx0q+TJ4= +github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -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/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 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.11.1 h1:oo2J/3vXdcti8cjFi8ghMOkx0OacONxHC8dhJ17NdJ0= -github.com/deislabs/oras v0.11.1/go.mod h1:39lCtf8Q6WDC7ul9cnyWXONNzKvabEKk+AX+L0ImnQk= -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.5+incompatible h1:bjflayQbWg+xOkF2WPEAOi4Y7zWhR7ptoPhV/VqLVDE= -github.com/docker/cli v20.10.5+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 v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8= -github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -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/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= +github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= +github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY= +github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= +github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= 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.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -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/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.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/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/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= +github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 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.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -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/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.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/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 v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -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/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= +github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= +github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= 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/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= -github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= -github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= -github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= -github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= -github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= -github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= -github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -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.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -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/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= -github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= -github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= -github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= -github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= -github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.19.5 h1:Xm0Ao53uqnk9QE/LlYV5DEU09UAgpliA85QoT9LzqPw= -github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= -github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= -github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= -github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= -github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= -github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= -github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -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-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= -github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= -github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= -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-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.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/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= -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/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= +github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= +github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= 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/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 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/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 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/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 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/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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 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/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -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.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 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/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 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/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 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/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -475,684 +227,331 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtB 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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/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-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -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/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= -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/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= 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/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg= +github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= 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/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= -github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 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/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -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/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= -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/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= 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/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= 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/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= 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/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= 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_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 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/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= 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/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= 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/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= +github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -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/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 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/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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -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/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 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.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -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 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -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 v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -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/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/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/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= -github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -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.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= -go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= -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= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= +go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= +go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= 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-20190320223903-b7391e95e576/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-20190617133340-57b3e21c3d56/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-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= 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/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 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/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= 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-20181005035420-146acd28ed58/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-20190320064053-1272bf9dcd53/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-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 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-20190923162816-aa69164e4478/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210224082022-3d97a244fca7 h1:OgUuv8lsRpBibGNbSizVwKWlysjaNzmC9gYMhPVfqFM= -golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 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/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= 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-20201020160332-67f06af15bc9/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/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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-20190321052220-f7bb7a8bee54/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-20191002063906-3421d5a6bb1c/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-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 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-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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-20190125232054-d66bd3c5d5a6/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-20190617190820-da514acc4774/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-20190907020128-2ca718005c18/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-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= 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/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 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/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= 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/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= 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= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.7/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/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= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= 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.21.0 h1:gu5iGF4V6tfVCQ/R+8Hc0h7H1JuEhzyEi9S4R5LM8+Y= -k8s.io/api v0.21.0/go.mod h1:+YbrhBBGgsxbF6o6Kj4KJPJnBmAKuXDeS3E18bgHNVU= -k8s.io/apiextensions-apiserver v0.21.0 h1:Nd4uBuweg6ImzbxkC1W7xUNZcCV/8Vt10iTdTIVF3hw= -k8s.io/apiextensions-apiserver v0.21.0/go.mod h1:gsQGNtGkc/YoDG9loKI0V+oLZM4ljRPjc/sql5tmvzc= -k8s.io/apimachinery v0.21.0 h1:3Fx+41if+IRavNcKOz09FwEXDBG6ORh6iMsTSelhkMA= -k8s.io/apimachinery v0.21.0/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= -k8s.io/apiserver v0.21.0 h1:1hWMfsz+cXxB77k6/y0XxWxwl6l9OF26PC9QneUVn1Q= -k8s.io/apiserver v0.21.0/go.mod h1:w2YSn4/WIwYuxG5zJmcqtRdtqgW/J2JRgFAqps3bBpg= -k8s.io/cli-runtime v0.21.0 h1:/V2Kkxtf6x5NI2z+Sd/mIrq4FQyQ8jzZAUD6N5RnN7Y= -k8s.io/cli-runtime v0.21.0/go.mod h1:XoaHP93mGPF37MkLbjGVYqg3S1MnsFdKtiA/RZzzxOo= -k8s.io/client-go v0.21.0 h1:n0zzzJsAQmJngpC0IhgFcApZyoGXPrDIAD601HD09ag= -k8s.io/client-go v0.21.0/go.mod h1:nNBytTF9qPFDEhoqgEPaarobC8QPae13bElIVHzIglA= -k8s.io/code-generator v0.21.0/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= -k8s.io/component-base v0.21.0 h1:tLLGp4BBjQaCpS/KiuWh7m2xqvAdsxLm4ATxHSe5Zpg= -k8s.io/component-base v0.21.0/go.mod h1:qvtjz6X0USWXbgmbfXR+Agik4RZ3jv2Bgr5QnZzdPYw= -k8s.io/component-helpers v0.21.0/go.mod h1:tezqefP7lxfvJyR+0a+6QtVrkZ/wIkyMLK4WcQ3Cj8U= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/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.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-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0= -k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= -k8s.io/kubectl v0.21.0 h1:WZXlnG/yjcE4LWO2g6ULjFxtzK6H1TKzsfaBFuVIhNg= -k8s.io/kubectl v0.21.0/go.mod h1:EU37NukZRXn1TpAkMUoy8Z/B2u6wjHDS4aInsDzVvks= -k8s.io/metrics v0.21.0/go.mod h1:L3Ji9EGPP1YBbfm9sPfEXSpnj8i24bfQbAFAsW0NueQ= -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.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/kustomize/api v0.8.5 h1:bfCXGXDAbFbb/Jv5AhMj2BB8a5VAJuuQ5/KU69WtDjQ= -sigs.k8s.io/kustomize/api v0.8.5/go.mod h1:M377apnKT5ZHJS++6H4rQoCHmWtt6qTpp3mbe7p6OLY= -sigs.k8s.io/kustomize/cmd/config v0.9.7/go.mod h1:MvXCpHs77cfyxRmCNUQjIqCmZyYsbn5PyQpWiq44nW0= -sigs.k8s.io/kustomize/kustomize/v4 v4.0.5/go.mod h1:C7rYla7sI8EnxHE/xEhRBSHMNfcL91fx0uKmUlUhrBk= -sigs.k8s.io/kustomize/kyaml v0.10.15 h1:dSLgG78KyaxN4HylPXdK+7zB3k7sW6q3IcCmcfKA+aI= -sigs.k8s.io/kustomize/kyaml v0.10.15/go.mod h1:mlQFagmkm1P+W4lZJbJ/yaxMd8PqMRSC4cPcfUVt5Hg= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.0 h1:C4r9BgJ98vrKnnVCjwCSXcWjWe0NKcUQkmzDXZXGwH8= -sigs.k8s.io/structured-merge-diff/v4 v4.1.0/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= +k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw= +k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg= +k8s.io/apiextensions-apiserver v0.28.2 h1:J6/QRWIKV2/HwBhHRVITMLYoypCoPY1ftigDM0Kn+QU= +k8s.io/apiextensions-apiserver v0.28.2/go.mod h1:5tnkxLGa9nefefYzWuAlWZ7RZYuN/765Au8cWLA6SRg= +k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ= +k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU= +k8s.io/apiserver v0.28.2 h1:rBeYkLvF94Nku9XfXyUIirsVzCzJBs6jMn3NWeHieyI= +k8s.io/apiserver v0.28.2/go.mod h1:f7D5e8wH8MWcKD7azq6Csw9UN+CjdtXIVQUyUhrtb+E= +k8s.io/cli-runtime v0.28.2 h1:64meB2fDj10/ThIMEJLO29a1oujSm0GQmKzh1RtA/uk= +k8s.io/cli-runtime v0.28.2/go.mod h1:bTpGOvpdsPtDKoyfG4EG041WIyFZLV9qq4rPlkyYfDA= +k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY= +k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY= +k8s.io/component-base v0.28.2 h1:Yc1yU+6AQSlpJZyvehm/NkJBII72rzlEsd6MkBQ+G0E= +k8s.io/component-base v0.28.2/go.mod h1:4IuQPQviQCg3du4si8GpMrhAIegxpsgPngPRR/zWpzc= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= +k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/kubectl v0.28.2 h1:fOWOtU6S0smdNjG1PB9WFbqEIMlkzU5ahyHkc7ESHgM= +k8s.io/kubectl v0.28.2/go.mod h1:6EQWTPySF1fn7yKoQZHYf9TPwIl2AygHEcJoxFekr64= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go v1.2.4 h1:djpBY2/2Cs1PV87GSJlxv4voajVOMZxqqtq9AB8YNvY= +oras.land/oras-go v1.2.4/go.mod h1:DYcGfb3YF1nKjcezfX2SNlDAeQFKSXmf+qrFmrh4324= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= +sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= +sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U= +sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/internal/experimental/registry/cache.go b/internal/experimental/registry/cache.go deleted file mode 100644 index 5aca63668..000000000 --- a/internal/experimental/registry/cache.go +++ /dev/null @@ -1,368 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index 6851ae807..000000000 --- a/internal/experimental/registry/cache_opts.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index 26556533c..000000000 --- a/internal/experimental/registry/client.go +++ /dev/null @@ -1,341 +0,0 @@ -/* -Copyright The 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 - columnWidth uint - } -) - -// 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 - } - - if client.columnWidth == 0 { - client.columnWidth = 60 - } - 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 = c.columnWidth - 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 deleted file mode 100644 index 266cf3f6f..000000000 --- a/internal/experimental/registry/client_opts.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright The 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 - } -} - -// ClientOptColumnWidth returns a function that sets the column width on a client options set -func ClientOptColumnWidth(columnWidth uint) ClientOption { - return func(client *Client) { - client.columnWidth = columnWidth - } -} diff --git a/internal/experimental/registry/client_test.go b/internal/experimental/registry/client_test.go deleted file mode 100644 index a9936ba13..000000000 --- a/internal/experimental/registry/client_test.go +++ /dev/null @@ -1,294 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index dafb3c9e5..000000000 --- a/internal/experimental/registry/constants.go +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright The 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/reference.go b/internal/experimental/registry/reference.go deleted file mode 100644 index f0e91d4ba..000000000 --- a/internal/experimental/registry/reference.go +++ /dev/null @@ -1,146 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index aae03ad99..000000000 --- a/internal/experimental/registry/reference_test.go +++ /dev/null @@ -1,126 +0,0 @@ -/* -Copyright The 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/util.go b/internal/experimental/registry/util.go deleted file mode 100644 index 697a890e3..000000000 --- a/internal/experimental/registry/util.go +++ /dev/null @@ -1,66 +0,0 @@ -/* -Copyright The 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 index 739093f3b..4ea09cca4 100644 --- a/internal/fileutil/fileutil.go +++ b/internal/fileutil/fileutil.go @@ -18,7 +18,6 @@ package fileutil import ( "io" - "io/ioutil" "os" "path/filepath" @@ -28,7 +27,7 @@ import ( // 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)) + tempFile, err := os.CreateTemp(filepath.Split(filename)) if err != nil { return err } diff --git a/internal/fileutil/fileutil_test.go b/internal/fileutil/fileutil_test.go index 9a4bc32c9..92920d3c4 100644 --- a/internal/fileutil/fileutil_test.go +++ b/internal/fileutil/fileutil_test.go @@ -18,30 +18,25 @@ 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) + dir := t.TempDir() testpath := filepath.Join(dir, "test") stringContent := "Test content" reader := bytes.NewReader([]byte(stringContent)) mode := os.FileMode(0644) - err = AtomicWriteFile(testpath, reader, mode) + err := AtomicWriteFile(testpath, reader, mode) if err != nil { t.Errorf("AtomicWriteFile error: %s", err) } - got, err := ioutil.ReadFile(testpath) + got, err := os.ReadFile(testpath) if err != nil { t.Fatal(err) } diff --git a/internal/ignore/doc.go b/internal/ignore/doc.go index e6a6a6c7b..a1f0fcfc8 100644 --- a/internal/ignore/doc.go +++ b/internal/ignore/doc.go @@ -14,7 +14,8 @@ 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). +/* +Package ignore provides tools for writing ignore files (a la .gitignore). This provides both an ignore parser and a file-aware processor. @@ -23,19 +24,19 @@ 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). + - 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: @@ -58,10 +59,10 @@ Example: 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. + - 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/internal/monocular/search.go b/internal/monocular/search.go index 3082ff361..4e7e8c002 100644 --- a/internal/monocular/search.go +++ b/internal/monocular/search.go @@ -114,7 +114,7 @@ func (c *Client) Search(term string) ([]SearchResult, error) { p.RawQuery = "q=" + url.QueryEscape(term) // Create request - req, err := http.NewRequest("GET", p.String(), nil) + req, err := http.NewRequest(http.MethodGet, p.String(), nil) if err != nil { return nil, err } diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go index de0634093..5e8921f96 100644 --- a/internal/resolver/resolver.go +++ b/internal/resolver/resolver.go @@ -18,6 +18,7 @@ package resolver import ( "bytes" "encoding/json" + "fmt" "os" "path/filepath" "strings" @@ -28,25 +29,25 @@ import ( "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/registry" "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 + chartpath string + cachepath string + registryClient *registry.Client } -// New creates a new resolver for a given chart and a given helm home. -func New(chartpath, cachepath string) *Resolver { +// New creates a new resolver for a given chart, helm home and registry client. +func New(chartpath, cachepath string, registryClient *registry.Client) *Resolver { return &Resolver{ - chartpath: chartpath, - cachepath: cachepath, + chartpath: chartpath, + cachepath: cachepath, + registryClient: registryClient, } } @@ -57,6 +58,11 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string locked := make([]*chart.Dependency, len(reqs)) missing := []string{} for i, d := range reqs { + constraint, err := semver.NewConstraint(d.Version) + if err != nil { + return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name) + } + if d.Repository == "" { // Local chart subfolder if _, err := GetLocalPath(filepath.Join("charts", d.Name), r.chartpath); err != nil { @@ -77,13 +83,22 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string 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 } + v, err := semver.NewVersion(ch.Metadata.Version) + if err != nil { + // Not a legit entry. + continue + } + + if !constraint.Check(v) { + missing = append(missing, d.Name) + continue + } + locked[i] = &chart.Dependency{ Name: d.Name, Repository: d.Repository, @@ -92,11 +107,6 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string 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 != "" { @@ -112,7 +122,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string var version string var ok bool found := true - if !strings.HasPrefix(d.Repository, "oci://") { + if !registry.IsOCI(d.Repository) { 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) @@ -125,9 +135,36 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string found = false } else { version = d.Version - if !FeatureGateOCI.IsEnabled() { - return nil, errors.Wrapf(FeatureGateOCI.Error(), - "repository %s is an OCI registry", d.Repository) + + // Check to see if an explicit version has been provided + _, err := semver.NewVersion(version) + + // Use an explicit version, otherwise search for tags + if err == nil { + vs = []*repo.ChartVersion{{ + Metadata: &chart.Metadata{ + Version: version, + }, + }} + + } else { + // Retrieve list of tags for repository + ref := fmt.Sprintf("%s/%s", strings.TrimPrefix(d.Repository, fmt.Sprintf("%s://", registry.OCIScheme)), d.Name) + tags, err := r.registryClient.Tags(ref) + if err != nil { + return nil, errors.Wrapf(err, "could not retrieve list of tags for repository %s", d.Repository) + } + + vs = make(repo.ChartVersions, len(tags)) + for ti, t := range tags { + // Mock chart version objects + version := &repo.ChartVersion{ + Metadata: &chart.Metadata{ + Version: t, + }, + } + vs[ti] = version + } } } @@ -139,7 +176,8 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string // 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 { + // OCI does not need URLs + if err != nil || (!registry.IsOCI(d.Repository) && len(ver.URLs) == 0) { // Not a legit entry. continue } diff --git a/internal/resolver/resolver_test.go b/internal/resolver/resolver_test.go index 204a72622..a79852175 100644 --- a/internal/resolver/resolver_test.go +++ b/internal/resolver/resolver_test.go @@ -16,9 +16,11 @@ limitations under the License. package resolver import ( + "runtime" "testing" "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/registry" ) func TestResolve(t *testing.T) { @@ -28,6 +30,18 @@ func TestResolve(t *testing.T) { expect *chart.Lock err bool }{ + { + name: "repo from invalid version", + req: []*chart.Dependency{ + {Name: "base", Repository: "file://base", Version: "1.1.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: "base", Repository: "file://base", Version: "0.1.0"}, + }, + }, + err: true, + }, { name: "version failure", req: []*chart.Dependency{ @@ -126,7 +140,8 @@ func TestResolve(t *testing.T) { } repoNames := map[string]string{"alpine": "kubernetes-charts", "redis": "kubernetes-charts"} - r := New("testdata/chartpath", "testdata/repository") + registryClient, _ := registry.NewClient() + r := New("testdata/chartpath", "testdata/repository", registryClient) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l, err := r.Resolve(tt.req, repoNames) @@ -234,24 +249,28 @@ func TestGetLocalPath(t *testing.T) { repo string chartpath string expect string + winExpect string err bool }{ { - name: "absolute path", - repo: "file:////tmp", - expect: "/tmp", + name: "absolute path", + repo: "file:////", + expect: "/", + winExpect: "\\", }, { name: "relative path", repo: "file://../../testdata/chartpath/base", chartpath: "foo/bar", expect: "testdata/chartpath/base", + winExpect: "testdata\\chartpath\\base", }, { name: "current directory path", repo: "../charts/localdependency", chartpath: "testdata/chartpath/charts", expect: "testdata/chartpath/charts/localdependency", + winExpect: "testdata\\chartpath\\charts\\localdependency", }, { name: "invalid local path", @@ -279,8 +298,12 @@ func TestGetLocalPath(t *testing.T) { 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) + expect := tt.expect + if runtime.GOOS == "windows" { + expect = tt.winExpect + } + if p != expect { + t.Errorf("%q: expected %q, got %q", tt.name, expect, p) } }) } diff --git a/internal/sympath/walk.go b/internal/sympath/walk.go index 752526fe9..a276cfeff 100644 --- a/internal/sympath/walk.go +++ b/internal/sympath/walk.go @@ -71,7 +71,7 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error { if err != nil { return errors.Wrapf(err, "error evaluating symlink %s", path) } - log.Printf("found symbolic link in path: %s resolves to %s", path, resolved) + log.Printf("found symbolic link in path: %s resolves to %s. Contents of linked file included and used", path, resolved) if info, err = os.Lstat(resolved); err != nil { return err } diff --git a/internal/test/ensure/ensure.go b/internal/test/ensure/ensure.go index 3c0e4575c..ff2d180fe 100644 --- a/internal/test/ensure/ensure.go +++ b/internal/test/ensure/ensure.go @@ -17,7 +17,6 @@ limitations under the License. package ensure import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -27,43 +26,29 @@ import ( ) // HelmHome sets up a Helm Home in a temp dir. -func HelmHome(t *testing.T) func() { +func HelmHome(t *testing.T) { t.Helper() - base := TempDir(t) + base := t.TempDir() 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. +// The returned directory is automatically removed when the test and all its subtests complete. // -// tempdir := TempFile(t, "foo", []byte("bar")) -// defer os.RemoveAll(tempdir) -// filename := filepath.Join(tempdir, "foo") +// tempdir := TempFile(t, "foo", []byte("bar")) +// filename := filepath.Join(tempdir, "foo") func TempFile(t *testing.T, name string, data []byte) string { - path := TempDir(t) + path := t.TempDir() filename := filepath.Join(path, name) - if err := ioutil.WriteFile(filename, data, 0755); err != nil { + if err := os.WriteFile(filename, data, 0755); err != nil { t.Fatal(err) } return path diff --git a/internal/test/test.go b/internal/test/test.go index 646037606..e6821282c 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -19,7 +19,7 @@ package test import ( "bytes" "flag" - "io/ioutil" + "os" "path/filepath" "github.com/pkg/errors" @@ -40,21 +40,12 @@ 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) + t.Fatalf("%v\n", err) } } @@ -62,11 +53,11 @@ func AssertGoldenString(t TestingT, actual, filename string) { func AssertGoldenFile(t TestingT, actualFileName string, expectedFilename string) { t.Helper() - actual, err := ioutil.ReadFile(actualFileName) + actual, err := os.ReadFile(actualFileName) if err != nil { t.Fatalf("%v", err) } - AssertGoldenBytes(t, actual, expectedFilename) + AssertGoldenString(t, string(actual), expectedFilename) } func path(filename string) string { @@ -82,13 +73,13 @@ func compare(actual []byte, filename string) error { return err } - expected, err := ioutil.ReadFile(filename) + expected, err := os.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 errors.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'", filename, expected, actual) } return nil } @@ -97,7 +88,7 @@ func update(filename string, in []byte) error { if !*updateGolden { return nil } - return ioutil.WriteFile(filename, normalize(in), 0666) + return os.WriteFile(filename, normalize(in), 0666) } func normalize(in []byte) []byte { diff --git a/internal/third_party/dep/fs/fs.go b/internal/third_party/dep/fs/fs.go index 832592197..4e4eacc60 100644 --- a/internal/third_party/dep/fs/fs.go +++ b/internal/third_party/dep/fs/fs.go @@ -33,7 +33,6 @@ package fs import ( "io" - "io/ioutil" "os" "path/filepath" "runtime" @@ -119,7 +118,7 @@ func CopyDir(src, dst string) error { return errors.Wrapf(err, "cannot mkdir %s", dst) } - entries, err := ioutil.ReadDir(src) + entries, err := os.ReadDir(src) if err != nil { return errors.Wrapf(err, "cannot read directory %s", dst) } diff --git a/internal/third_party/dep/fs/fs_test.go b/internal/third_party/dep/fs/fs_test.go index 98a31aec6..d42c3f110 100644 --- a/internal/third_party/dep/fs/fs_test.go +++ b/internal/third_party/dep/fs/fs_test.go @@ -32,7 +32,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package fs import ( - "io/ioutil" "os" "os/exec" "path/filepath" @@ -46,13 +45,9 @@ var ( ) func TestRenameWithFallback(t *testing.T) { - dir, err := ioutil.TempDir("", "helm-tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() - if err = RenameWithFallback(filepath.Join(dir, "does_not_exists"), filepath.Join(dir, "dst")); err == nil { + 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") } @@ -64,31 +59,27 @@ func TestRenameWithFallback(t *testing.T) { srcf.Close() } - if err = RenameWithFallback(srcpath, filepath.Join(dir, "dst")); err != nil { + 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 { + if err := os.MkdirAll(srcpath, 0777); err != nil { t.Fatal(err) } dstpath := filepath.Join(dir, "b") - if err = os.MkdirAll(dstpath, 0777); err != nil { + if err := os.MkdirAll(dstpath, 0777); err != nil { t.Fatal(err) } - if err = RenameWithFallback(srcpath, dstpath); err == nil { + 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) + dir := t.TempDir() srcdir := filepath.Join(dir, "src") if err := os.MkdirAll(srcdir, 0755); err != nil { @@ -108,7 +99,7 @@ func TestCopyDir(t *testing.T) { for i, file := range files { fn := filepath.Join(srcdir, file.path) dn := filepath.Dir(fn) - if err = os.MkdirAll(dn, 0755); err != nil { + if err := os.MkdirAll(dn, 0755); err != nil { t.Fatal(err) } @@ -145,7 +136,7 @@ func TestCopyDir(t *testing.T) { t.Fatalf("expected %s to be a directory", dn) } - got, err := ioutil.ReadFile(fn) + got, err := os.ReadFile(fn) if err != nil { t.Fatal(err) } @@ -189,14 +180,10 @@ func TestCopyDirFail_SrcInaccessible(t *testing.T) { }) defer cleanup() - dir, err := ioutil.TempDir("", "helm-tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() dstdir = filepath.Join(dir, "dst") - if err = CopyDir(srcdir, dstdir); err == nil { + if err := CopyDir(srcdir, dstdir); err == nil { t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir) } } @@ -218,14 +205,10 @@ func TestCopyDirFail_DstInaccessible(t *testing.T) { var srcdir, dstdir string - dir, err := ioutil.TempDir("", "helm-tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() srcdir = filepath.Join(dir, "src") - if err = os.MkdirAll(srcdir, 0755); err != nil { + if err := os.MkdirAll(srcdir, 0755); err != nil { t.Fatal(err) } @@ -242,12 +225,9 @@ func TestCopyDirFail_DstInaccessible(t *testing.T) { func TestCopyDirFail_SrcIsNotDir(t *testing.T) { var srcdir, dstdir string + var err error - dir, err := ioutil.TempDir("", "helm-tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() srcdir = filepath.Join(dir, "src") if _, err = os.Create(srcdir); err != nil { @@ -268,12 +248,9 @@ func TestCopyDirFail_SrcIsNotDir(t *testing.T) { func TestCopyDirFail_DstExists(t *testing.T) { var srcdir, dstdir string + var err error - dir, err := ioutil.TempDir("", "helm-tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() srcdir = filepath.Join(dir, "src") if err = os.MkdirAll(srcdir, 0755); err != nil { @@ -314,14 +291,10 @@ func TestCopyDirFailOpen(t *testing.T) { var srcdir, dstdir string - dir, err := ioutil.TempDir("", "helm-tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() srcdir = filepath.Join(dir, "src") - if err = os.MkdirAll(srcdir, 0755); err != nil { + if err := os.MkdirAll(srcdir, 0755); err != nil { t.Fatal(err) } @@ -345,11 +318,7 @@ func TestCopyDirFailOpen(t *testing.T) { } func TestCopyFile(t *testing.T) { - dir, err := ioutil.TempDir("", "helm-tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() srcf, err := os.Create(filepath.Join(dir, "srcfile")) if err != nil { @@ -367,7 +336,7 @@ func TestCopyFile(t *testing.T) { t.Fatal(err) } - got, err := ioutil.ReadFile(destf) + got, err := os.ReadFile(destf) if err != nil { t.Fatal(err) } @@ -405,13 +374,7 @@ func cleanUpDir(dir string) { } 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) + tempdir := t.TempDir() testcases := map[string]string{ filepath.Join("./testdata/symlinks/file-symlink"): filepath.Join(tempdir, "dst-file"), @@ -432,11 +395,11 @@ func TestCopyFileSymlink(t *testing.T) { // 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) + srcb, err := os.ReadFile(symlink) if err != nil { t.Fatalf("%+v", err) } - dstb, err := ioutil.ReadFile(dst) + dstb, err := os.ReadFile(dst) if err != nil { t.Fatalf("%+v", err) } @@ -477,11 +440,7 @@ func TestCopyFileFail(t *testing.T) { t.Skip("Skipping for root user") } - dir, err := ioutil.TempDir("", "helm-tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() srcf, err := os.Create(filepath.Join(dir, "srcfile")) if err != nil { @@ -517,11 +476,7 @@ func TestCopyFileFail(t *testing.T) { // 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 - } + dir := t.TempDir() subdir := filepath.Join(dir, "dir") @@ -529,9 +484,6 @@ func setupInaccessibleDir(t *testing.T, op func(dir string) error) 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 { @@ -617,14 +569,10 @@ func TestIsSymlink(t *testing.T) { t.Skip("Skipping for root user") } - dir, err := ioutil.TempDir("", "helm-tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() dirPath := filepath.Join(dir, "directory") - if err = os.MkdirAll(dirPath, 0777); err != nil { + if err := os.MkdirAll(dirPath, 0777); err != nil { t.Fatal(err) } diff --git a/internal/third_party/dep/fs/rename.go b/internal/third_party/dep/fs/rename.go index 0bb600949..a3e5e56a6 100644 --- a/internal/third_party/dep/fs/rename.go +++ b/internal/third_party/dep/fs/rename.go @@ -1,4 +1,4 @@ -// +build !windows +//go:build !windows /* Copyright (c) for portions of rename.go are held by The Go Authors, 2016 and are provided under diff --git a/internal/third_party/dep/fs/rename_windows.go b/internal/third_party/dep/fs/rename_windows.go index 14f017d09..a377720a6 100644 --- a/internal/third_party/dep/fs/rename_windows.go +++ b/internal/third_party/dep/fs/rename_windows.go @@ -1,4 +1,4 @@ -// +build windows +//go:build windows /* Copyright (c) for portions of rename_windows.go are held by The Go Authors, 2016 and are provided under diff --git a/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go b/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go index 103db35c4..ae62d0e6f 100644 --- a/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go +++ b/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go @@ -92,9 +92,9 @@ func FindNewReplicaSet(deployment *apps.Deployment, rsList []*apps.ReplicaSet) * // 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 +// 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() diff --git a/internal/tlsutil/tls.go b/internal/tlsutil/tls.go index ed7795dbe..dc832ed80 100644 --- a/internal/tlsutil/tls.go +++ b/internal/tlsutil/tls.go @@ -19,14 +19,16 @@ package tlsutil import ( "crypto/tls" "crypto/x509" - "io/ioutil" + "os" "github.com/pkg/errors" ) // NewClientTLS returns tls.Config appropriate for client auth. -func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) { - config := tls.Config{} +func NewClientTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*tls.Config, error) { + config := tls.Config{ + InsecureSkipVerify: insecureSkipTLSverify, + } if certFile != "" && keyFile != "" { cert, err := CertFromFilePair(certFile, keyFile) @@ -52,7 +54,7 @@ func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) { // Returns an error if the file could not be read, a certificate could not // be parsed, or if the file does not contain any certificates func CertPoolFromFile(filename string) (*x509.CertPool, error) { - b, err := ioutil.ReadFile(filename) + b, err := os.ReadFile(filename) if err != nil { return nil, errors.Errorf("can't read CA file: %v", filename) } diff --git a/internal/tlsutil/tlsutil_test.go b/internal/tlsutil/tlsutil_test.go index e660c030c..e31a873d3 100644 --- a/internal/tlsutil/tlsutil_test.go +++ b/internal/tlsutil/tlsutil_test.go @@ -65,8 +65,9 @@ func TestNewClientTLS(t *testing.T) { certFile := testfile(t, testCertFile) keyFile := testfile(t, testKeyFile) caCertFile := testfile(t, testCaCertFile) + insecureSkipTLSverify := false - cfg, err := NewClientTLS(certFile, keyFile, caCertFile) + cfg, err := NewClientTLS(certFile, keyFile, caCertFile, insecureSkipTLSverify) if err != nil { t.Error(err) } @@ -81,7 +82,7 @@ func TestNewClientTLS(t *testing.T) { t.Fatalf("mismatch tls RootCAs, expecting non-nil") } - cfg, err = NewClientTLS("", "", caCertFile) + cfg, err = NewClientTLS("", "", caCertFile, insecureSkipTLSverify) if err != nil { t.Error(err) } @@ -96,7 +97,7 @@ func TestNewClientTLS(t *testing.T) { t.Fatalf("mismatch tls RootCAs, expecting non-nil") } - cfg, err = NewClientTLS(certFile, keyFile, "") + cfg, err = NewClientTLS(certFile, keyFile, "", insecureSkipTLSverify) if err != nil { t.Error(err) } diff --git a/internal/version/version.go b/internal/version/version.go index 4db8c0549..b29891ec6 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -29,7 +29,7 @@ var ( // // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. - version = "v3.6" + version = "v3.13" // metadata is extra build time data metadata = "" diff --git a/pkg/action/action.go b/pkg/action/action.go index f093ed7f8..5693f4838 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -32,12 +32,12 @@ import ( "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/registry" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/storage" @@ -101,8 +101,9 @@ type Configuration struct { // // 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 (cfg *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) { +// +// This code has to do with writing files to disk. +func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS bool) ([]*release.Hook, *bytes.Buffer, string, error) { hs := []*release.Hook{} b := bytes.NewBuffer(nil) @@ -120,19 +121,21 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu 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 && cfg.RESTClientGetter != nil { + // A `helm template` should not talk to the remote cluster. However, commands with the flag + //`--dry-run` with the value of `false`, `none`, or `server` should try to interact with the cluster. + // It may break in interesting and exotic ways because other data (e.g. discovery) is mocked. + if interactWithRemote && cfg.RESTClientGetter != nil { restConfig, err := cfg.RESTClientGetter.ToRESTConfig() if err != nil { return hs, b, "", err } - files, err2 = engine.RenderWithClient(ch, values, restConfig) + e := engine.New(restConfig) + e.EnableDNS = enableDNS + files, err2 = e.Render(ch, values) } else { - files, err2 = engine.Render(ch, values) + var e engine.Engine + e.EnableDNS = enableDNS + files, err2 = e.Render(ch, values) } if err2 != nil { @@ -184,13 +187,13 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu if includeCrds { for _, crd := range ch.CRDObjects() { if outputDir == "" { - fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:])) + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Filename, string(crd.File.Data[:])) } else { - err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name]) + err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Filename]) if err != nil { return hs, b, "", err } - fileWritten[crd.Name] = true + fileWritten[crd.Filename] = true } } } @@ -272,6 +275,7 @@ func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) { Major: kubeVersion.Major, Minor: kubeVersion.Minor, }, + HelmVersion: chartutil.DefaultCapabilities.HelmVersion, } return cfg.Capabilities, nil } diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index fedf260fb..c4ef6c056 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -16,21 +16,16 @@ limitations under the License. package action import ( - "context" "flag" - "io/ioutil" - "net/http" - "os" - "path/filepath" + "io" "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/registry" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage" "helm.sh/helm/v3/pkg/storage/driver" @@ -42,47 +37,14 @@ 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), - ) + registryClient, err := registry.NewClient() if err != nil { t.Fatal(err) } return &Configuration{ Releases: storage.Init(driver.NewMemory()), - KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: ioutil.Discard}}, + KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}}, Capabilities: chartutil.DefaultCapabilities, RegistryClient: registryClient, Log: func(format string, v ...interface{}) { diff --git a/pkg/action/chart_export.go b/pkg/action/chart_export.go deleted file mode 100644 index 75840d8bc..000000000 --- a/pkg/action/chart_export.go +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index ba49a887d..000000000 --- a/pkg/action/chart_list.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright The 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" -) - -// ChartList performs a chart list operation. -type ChartList struct { - cfg *Configuration - ColumnWidth uint -} - -// 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 { - client := a.cfg.RegistryClient - opt := registry.ClientOptColumnWidth(a.ColumnWidth) - opt(client) - return client.PrintChartTable() -} diff --git a/pkg/action/chart_pull.go b/pkg/action/chart_pull.go deleted file mode 100644 index 896755201..000000000 --- a/pkg/action/chart_pull.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index 91ec49d38..000000000 --- a/pkg/action/chart_push.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index 3c0fc2ed7..000000000 --- a/pkg/action/chart_remove.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index 14a2d7c3c..000000000 --- a/pkg/action/chart_save.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright The 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 deleted file mode 100644 index 4fd991a4e..000000000 --- a/pkg/action/chart_save_test.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright The 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 index 6a284d762..3265f1f17 100644 --- a/pkg/action/dependency.go +++ b/pkg/action/dependency.go @@ -193,7 +193,7 @@ func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart } // printMissing prints warnings about charts that are present on disk, but are -// not in Charts.yaml. +// not in Chart.yaml. func (d *Dependency) printMissing(chartpath string, out io.Writer, reqs []*chart.Dependency) { folder := filepath.Join(chartpath, "charts/*") files, err := filepath.Glob(folder) diff --git a/pkg/action/dependency_test.go b/pkg/action/dependency_test.go index b5032a377..c29587aec 100644 --- a/pkg/action/dependency_test.go +++ b/pkg/action/dependency_test.go @@ -18,7 +18,6 @@ package action import ( "bytes" - "io/ioutil" "os" "path/filepath" "testing" @@ -60,7 +59,7 @@ func TestList(t *testing.T) { if err := NewDependency().List(tcase.chart, &buf); err != nil { t.Fatal(err) } - test.AssertGoldenBytes(t, buf.Bytes(), tcase.golden) + test.AssertGoldenString(t, buf.String(), tcase.golden) } } @@ -68,11 +67,7 @@ func TestList(t *testing.T) { // 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) + dir := t.TempDir() chartpath := filepath.Join(dir, "charts") if err := os.MkdirAll(chartpath, 0700); err != nil { @@ -81,7 +76,7 @@ func TestDependencyStatus_Dashes(t *testing.T) { // Add some fake charts first := buildChart(withName("first-chart")) - _, err = chartutil.Save(first, chartpath) + _, err := chartutil.Save(first, chartpath) if err != nil { t.Fatal(err) } @@ -106,11 +101,7 @@ func TestDependencyStatus_Dashes(t *testing.T) { func TestStatArchiveForStatus(t *testing.T) { // Make a temp dir - dir, err := ioutil.TempDir("", "helmtest-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() chartpath := filepath.Join(dir, "charts") if err := os.MkdirAll(chartpath, 0700); err != nil { diff --git a/pkg/action/get_metadata.go b/pkg/action/get_metadata.go new file mode 100644 index 000000000..ec096ae16 --- /dev/null +++ b/pkg/action/get_metadata.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 action + +import "time" + +// GetMetadata is the action for checking a given release's metadata. +// +// It provides the implementation of 'helm get metadata'. +type GetMetadata struct { + cfg *Configuration + + Version int +} + +type Metadata struct { + Name string `json:"name" yaml:"name"` + Chart string `json:"chart" yaml:"chart"` + Version string `json:"version" yaml:"version"` + AppVersion string `json:"appVersion" yaml:"appVersion"` + Namespace string `json:"namespace" yaml:"namespace"` + Revision int `json:"revision" yaml:"revision"` + Status string `json:"status" yaml:"status"` + DeployedAt string `json:"deployedAt" yaml:"deployedAt"` +} + +// NewGetMetadata creates a new GetMetadata object with the given configuration. +func NewGetMetadata(cfg *Configuration) *GetMetadata { + return &GetMetadata{ + cfg: cfg, + } +} + +// Run executes 'helm get metadata' against the given release. +func (g *GetMetadata) Run(name string) (*Metadata, 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 + } + + return &Metadata{ + Name: rel.Name, + Chart: rel.Chart.Metadata.Name, + Version: rel.Chart.Metadata.Version, + AppVersion: rel.Chart.Metadata.AppVersion, + Namespace: rel.Namespace, + Revision: rel.Version, + Status: rel.Info.Status.String(), + DeployedAt: rel.Info.LastDeployed.Format(time.RFC3339), + }, nil +} diff --git a/pkg/action/install.go b/pkg/action/install.go index 933747d26..e3538a4f5 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -18,13 +18,15 @@ package action import ( "bytes" + "context" "fmt" - "io/ioutil" + "io" "net/url" "os" "path" "path/filepath" "strings" + "sync" "text/template" "time" @@ -32,6 +34,7 @@ import ( "github.com/pkg/errors" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/resource" "sigs.k8s.io/yaml" @@ -44,6 +47,7 @@ import ( "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/registry" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/repo" @@ -51,13 +55,6 @@ import ( "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 @@ -73,8 +70,10 @@ type Install struct { ChartPathOptions ClientOnly bool + Force bool CreateNamespace bool DryRun bool + DryRunOption string DisableHooks bool Replace bool Wait bool @@ -93,6 +92,7 @@ type Install struct { SubNotes bool DisableOpenAPIValidation bool IncludeCRDs bool + Labels map[string]string // KubeVersion allows specifying a custom kubernetes version to use and // APIVersions allows a manual set of supported API Versions to be passed // (for things like templating). These are ignored if ClientOnly is false @@ -100,10 +100,14 @@ type Install struct { APIVersions chartutil.VersionSet // Used by helm template to render charts with .Release.IsUpgrade. Ignored if Dry-Run is false IsUpgrade bool + // Enable DNS lookups when rendering templates + EnableDNS bool // Used by helm template to add the release as part of OutputDir path // OutputDir/ UseReleaseName bool PostRenderer postrender.PostRenderer + // Lock to control raceconditions when the process receives a SIGTERM + Lock sync.Mutex } // ChartPathOptions captures common options used for controlling chart paths @@ -112,6 +116,7 @@ type ChartPathOptions struct { CertFile string // --cert-file KeyFile string // --key-file InsecureSkipTLSverify bool // --insecure-skip-verify + PlainHTTP bool // --plain-http Keyring string // --keyring Password string // --password PassCredentialsAll bool // --pass-credentials @@ -119,13 +124,30 @@ type ChartPathOptions struct { Username string // --username Verify bool // --verify Version string // --version + + // registryClient provides a registry client but is not added with + // options from a flag + registryClient *registry.Client } // NewInstall creates a new Install object with the given configuration. func NewInstall(cfg *Configuration) *Install { - return &Install{ + in := &Install{ cfg: cfg, } + in.ChartPathOptions.registryClient = cfg.RegistryClient + + return in +} + +// SetRegistryClient sets the registry client for the install action +func (i *Install) SetRegistryClient(registryClient *registry.Client) { + i.ChartPathOptions.registryClient = registryClient +} + +// GetRegistryClient get the registry client. +func (i *Install) GetRegistryClient() *registry.Client { + return i.ChartPathOptions.registryClient } func (i *Install) installCRDs(crds []chart.CRD) error { @@ -151,22 +173,38 @@ func (i *Install) installCRDs(crds []chart.CRD) error { 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() + // If we have already gathered the capabilities, we need to invalidate + // the cache so that the new CRDs are recognized. This should only be + // the case when an action configuration is reused for multiple actions, + // as otherwise it is later loaded by ourselves when getCapabilities + // is called later on in the installation process. + if i.cfg.Capabilities != nil { + discoveryClient, err := i.cfg.RESTClientGetter.ToDiscoveryClient() + if err != nil { + return err + } + + i.cfg.Log("Clearing discovery cache") + discoveryClient.Invalidate() + + _, _ = discoveryClient.ServerGroups() + } + + // Invalidate the REST mapper, since it will not have the new CRDs + // present. + restMapper, err := i.cfg.RESTClientGetter.ToRESTMapper() + if err != nil { + return err + } + if resettable, ok := restMapper.(meta.ResettableRESTMapper); ok { + i.cfg.Log("Clearing REST mapper cache") + resettable.Reset() + } } return nil } @@ -174,7 +212,17 @@ func (i *Install) installCRDs(crds []chart.CRD) error { // 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) { + ctx := context.Background() + return i.RunWithContext(ctx, chrt, vals) +} + +// Run executes the installation with Context +// +// When the task is cancelled through ctx, the function returns and the install +// proceeds in the background. +func (i *Install) RunWithContext(ctx context.Context, 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 { @@ -186,11 +234,20 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. return nil, err } + if err := chartutil.ProcessDependenciesWithMerge(chrt, vals); err != nil { + return nil, err + } + + var interactWithRemote bool + if !i.isDryRun() || i.DryRunOption == "server" || i.DryRunOption == "none" || i.DryRunOption == "false" { + interactWithRemote = true + } + // 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 { + if i.isDryRun() { 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 @@ -205,7 +262,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. i.cfg.Capabilities.KubeVersion = *i.KubeVersion } i.cfg.Capabilities.APIVersions = append(i.cfg.Capabilities.APIVersions, i.APIVersions...) - i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard} + i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard} mem := driver.NewMemory() mem.SetNamespace(i.Namespace) @@ -214,10 +271,6 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. 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 @@ -228,7 +281,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. } // special case for helm template --is-upgrade - isUpgrade := i.IsUpgrade && i.DryRun + isUpgrade := i.IsUpgrade && i.isDryRun() options := chartutil.ReleaseOptions{ Name: i.ReleaseName, Namespace: i.Namespace, @@ -241,10 +294,14 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. return nil, err } - rel := i.createRelease(chrt, vals) + if driver.ContainsSystemLabels(i.Labels) { + return nil, fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()) + } + + rel := i.createRelease(chrt, vals, i.Labels) 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) + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS) // Even for errors, attach this if available if manifestDoc != nil { rel.Manifest = manifestDoc.String() @@ -280,12 +337,12 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. 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") + return nil, errors.Wrap(err, "Unable to continue with install") } } // Bail out here if it is a dry run - if i.DryRun { + if i.isDryRun() { rel.Info.Description = "Dry run complete" return rel, nil } @@ -332,10 +389,47 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. return rel, err } + rel, err = i.performInstallCtx(ctx, rel, toBeAdopted, resources) + if err != nil { + rel, err = i.failRelease(rel, err) + } + return rel, err +} + +func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) { + type Msg struct { + r *release.Release + e error + } + resultChan := make(chan Msg, 1) + + go func() { + rel, err := i.performInstall(rel, toBeAdopted, resources) + resultChan <- Msg{rel, err} + }() + select { + case <-ctx.Done(): + err := ctx.Err() + return rel, err + case msg := <-resultChan: + return msg.r, msg.e + } +} + +// isDryRun returns true if Upgrade is set to run as a DryRun +func (i *Install) isDryRun() bool { + if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" { + return true + } + return false +} + +func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) { + var err error // 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)) + return rel, fmt.Errorf("failed pre-install: %s", err) } } @@ -343,30 +437,28 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. // 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) - } + _, err = i.cfg.KubeClient.Create(resources) } else if len(resources) > 0 { - if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil { - return i.failRelease(rel, err) - } + _, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force) + } + if err != nil { + return rel, err } if i.Wait { if i.WaitForJobs { - if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil { - return i.failRelease(rel, err) - } + err = i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout) } else { - if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil { - return i.failRelease(rel, err) - } + err = i.cfg.KubeClient.Wait(resources, i.Timeout) + } + if err != nil { + return 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)) + return rel, fmt.Errorf("failed post-install: %s", err) } } @@ -411,21 +503,18 @@ func (i *Install) failRelease(rel *release.Release, err error) (*release.Release // // 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 +// - 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 err := chartutil.ValidateReleaseName(start); err != nil { + return errors.Wrapf(err, "release name %q", start) } - - if i.DryRun { + // On dry run, bail here + if i.isDryRun() { return nil } @@ -443,7 +532,7 @@ func (i *Install) availableName() error { } // createRelease creates a new release object -func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}) *release.Release { +func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release { ts := i.cfg.Now() return &release.Release{ Name: i.ReleaseName, @@ -456,6 +545,7 @@ func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{ Status: release.StatusUnknown, }, Version: 1, + Labels: labels, } } @@ -632,6 +722,10 @@ OUTER: // // 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) { + if registry.IsOCI(name) && c.registryClient == nil { + return "", fmt.Errorf("unable to lookup chart %q, missing registry client", name) + } + name = strings.TrimSpace(name) version := strings.TrimSpace(c.Version) @@ -659,10 +753,17 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( getter.WithPassCredentialsAll(c.PassCredentialsAll), getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile), getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify), + getter.WithPlainHTTP(c.PlainHTTP), }, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, + RegistryClient: c.registryClient, + } + + if registry.IsOCI(name) { + dl.Options = append(dl.Options, getter.WithRegistryClient(c.registryClient)) } + if c.Verify { dl.Verify = downloader.VerifyAlways } @@ -702,19 +803,13 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( } 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 + if err != nil { + return "", err } - atVersion := "" - if version != "" { - atVersion = fmt.Sprintf(" at version %q", version) + lname, err := filepath.Abs(filename) + if err != nil { + return filename, err } - return filename, errors.Errorf("failed to download %q%s (hint: running `helm repo update` may help)", name, atVersion) + return lname, nil } diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 63383d778..bc0890115 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -17,16 +17,19 @@ limitations under the License. package action import ( + "context" "fmt" - "io/ioutil" - "log" + "io" "os" "path/filepath" "regexp" + "runtime" "strings" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "helm.sh/helm/v3/internal/test" "helm.sh/helm/v3/pkg/chart" @@ -34,7 +37,7 @@ import ( 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" + helmtime "helm.sh/helm/v3/pkg/time" ) type nameTemplateTestCase struct { @@ -54,9 +57,12 @@ func installAction(t *testing.T) *Install { func TestInstallRelease(t *testing.T) { is := assert.New(t) + req := require.New(t) + instAction := installAction(t) vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(), vals) + ctx, done := context.WithCancel(context.Background()) + res, err := instAction.RunWithContext(ctx, buildChart(), vals) if err != nil { t.Fatalf("Failed install: %s", err) } @@ -75,6 +81,14 @@ func TestInstallRelease(t *testing.T) { is.NotEqual(len(rel.Manifest), 0) is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") is.Equal(rel.Info.Description, "Install complete") + + // Detecting previous bug where context termination after successful release + // caused release to fail. + done() + time.Sleep(time.Millisecond * 100) + lastRelease, err := instAction.cfg.Releases.Last(rel.Name) + req.NoError(err) + is.Equal(lastRelease.Info.Status, release.StatusDeployed) } func TestInstallReleaseWithValues(t *testing.T) { @@ -119,7 +133,7 @@ func TestInstallReleaseClientOnly(t *testing.T) { instAction.Run(buildChart(), nil) // disregard output is.Equal(instAction.cfg.Capabilities, chartutil.DefaultCapabilities) - is.Equal(instAction.cfg.KubeClient, &kubefake.PrintingKubeClient{Out: ioutil.Discard}) + is.Equal(instAction.cfg.KubeClient, &kubefake.PrintingKubeClient{Out: io.Discard}) } func TestInstallRelease_NoName(t *testing.T) { @@ -130,7 +144,7 @@ func TestInstallRelease_NoName(t *testing.T) { if err == nil { t.Fatal("expected failure when no name is specified") } - assert.Contains(t, err.Error(), "name is required") + assert.Contains(t, err.Error(), "no name provided") } func TestInstallRelease_WithNotes(t *testing.T) { @@ -241,7 +255,7 @@ func TestInstallRelease_DryRun(t *testing.T) { is.Equal(res.Info.Description, "Dry run complete") } -// Regression test for #7955: Lookup must not connect to Kubernetes on a dry-run. +// Regression test for #7955 func TestInstallRelease_DryRun_Lookup(t *testing.T) { is := assert.New(t) instAction := installAction(t) @@ -356,12 +370,40 @@ func TestInstallRelease_Wait(t *testing.T) { instAction.Wait = true vals := map[string]interface{}{} + goroutines := runtime.NumGoroutine() + res, err := instAction.Run(buildChart(), vals) is.Error(err) is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, release.StatusFailed) + + is.Equal(goroutines, runtime.NumGoroutine()) } +func TestInstallRelease_Wait_Interrupted(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "interrupted-release" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitDuration = 10 * time.Second + instAction.cfg.KubeClient = failer + instAction.Wait = true + vals := map[string]interface{}{} + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + time.AfterFunc(time.Second, cancel) + + goroutines := runtime.NumGoroutine() + res, err := instAction.RunWithContext(ctx, buildChart(), vals) + is.Error(err) + is.Contains(res.Info.Description, "Release \"interrupted-release\" failed: context canceled") + is.Equal(res.Info.Status, release.StatusFailed) + + is.Equal(goroutines+1, runtime.NumGoroutine()) // installation goroutine still is in background + time.Sleep(10 * time.Second) // wait for goroutine to finish + is.Equal(goroutines, runtime.NumGoroutine()) +} func TestInstallRelease_WaitForJobs(t *testing.T) { is := assert.New(t) instAction := installAction(t) @@ -419,7 +461,33 @@ func TestInstallRelease_Atomic(t *testing.T) { is.Contains(err.Error(), "an error occurred while uninstalling the release") }) } +func TestInstallRelease_Atomic_Interrupted(t *testing.T) { + + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "interrupted-release" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitDuration = 10 * time.Second + instAction.cfg.KubeClient = failer + instAction.Atomic = true + vals := map[string]interface{}{} + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + time.AfterFunc(time.Second, cancel) + + res, err := instAction.RunWithContext(ctx, buildChart(), vals) + is.Error(err) + is.Contains(err.Error(), "context canceled") + is.Contains(err.Error(), "atomic") + is.Contains(err.Error(), "uninstalled") + // 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) + +} func TestNameTemplate(t *testing.T) { testCases := []nameTemplateTestCase{ // Just a straight up nop please @@ -494,15 +562,11 @@ func TestInstallReleaseOutputDir(t *testing.T) { instAction := installAction(t) vals := map[string]interface{}{} - dir, err := ioutil.TempDir("", "output-dir") - if err != nil { - log.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() instAction.OutputDir = dir - _, err = instAction.Run(buildChart(withSampleTemplates(), withMultipleManifestTemplate()), vals) + _, err := instAction.Run(buildChart(withSampleTemplates(), withMultipleManifestTemplate()), vals) if err != nil { t.Fatalf("Failed install: %s", err) } @@ -530,11 +594,7 @@ func TestInstallOutputDirWithReleaseName(t *testing.T) { instAction := installAction(t) vals := map[string]interface{}{} - dir, err := ioutil.TempDir("", "output-dir") - if err != nil { - log.Fatal(err) - } - defer os.RemoveAll(dir) + dir := t.TempDir() instAction.OutputDir = dir instAction.UseReleaseName = true @@ -542,7 +602,7 @@ func TestInstallOutputDirWithReleaseName(t *testing.T) { newDir := filepath.Join(dir, instAction.ReleaseName) - _, err = instAction.Run(buildChart(withSampleTemplates(), withMultipleManifestTemplate()), vals) + _, err := instAction.Run(buildChart(withSampleTemplates(), withMultipleManifestTemplate()), vals) if err != nil { t.Fatalf("Failed install: %s", err) } @@ -624,32 +684,32 @@ func TestNameAndChartGenerateName(t *testing.T) { { "local filepath", "./chart", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "dot filepath", ".", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "empty filepath", "", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "packaged chart", "chart.tgz", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "packaged chart with .tar.gz extension", "chart.tar.gz", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "packaged chart with local extension", "./chart.tgz", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, } @@ -668,3 +728,33 @@ func TestNameAndChartGenerateName(t *testing.T) { }) } } + +func TestInstallWithLabels(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.Labels = map[string]string{ + "key1": "val1", + "key2": "val2", + } + res, err := instAction.Run(buildChart(), nil) + if err != nil { + t.Fatalf("Failed install: %s", err) + } + + is.Equal(instAction.Labels, res.Labels) +} + +func TestInstallWithSystemLabels(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.Labels = map[string]string{ + "owner": "val1", + "key2": "val2", + } + _, err := instAction.Run(buildChart(), nil) + if err == nil { + t.Fatal("expected an error") + } + + is.Equal(fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()), err) +} diff --git a/pkg/action/lint.go b/pkg/action/lint.go index 2292c14bf..e71cfe733 100644 --- a/pkg/action/lint.go +++ b/pkg/action/lint.go @@ -17,7 +17,6 @@ limitations under the License. package action import ( - "io/ioutil" "os" "path/filepath" "strings" @@ -36,6 +35,7 @@ type Lint struct { Strict bool Namespace string WithSubcharts bool + Quiet bool } // LintResult is the result of Lint @@ -75,12 +75,22 @@ func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult { return result } +// HasWarningsOrErrors checks is LintResult has any warnings or errors +func HasWarningsOrErrors(result *LintResult) bool { + for _, msg := range result.Messages { + if msg.Severity > support.InfoSev { + return true + } + } + return len(result.Errors) > 0 +} + 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") + tempDir, err := os.MkdirTemp("", "helm-lint") if err != nil { return linter, errors.Wrap(err, "unable to create temp dir to extract tarball") } @@ -96,7 +106,7 @@ func lintChart(path string, vals map[string]interface{}, namespace string, stric return linter, errors.Wrap(err, "unable to extract tarball") } - files, err := ioutil.ReadDir(tempDir) + files, err := os.ReadDir(tempDir) if err != nil { return linter, errors.Wrapf(err, "unable to read temporary output directory %s", tempDir) } diff --git a/pkg/action/lint_test.go b/pkg/action/lint_test.go index 1828461f3..ff69407ca 100644 --- a/pkg/action/lint_test.go +++ b/pkg/action/lint_test.go @@ -149,12 +149,12 @@ func TestLint_ChartWithWarnings(t *testing.T) { } }) - t.Run("should fail with errors when strict", func(t *testing.T) { + t.Run("should pass with no 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)) + if result := testLint.Run(testCharts, values); len(result.Errors) != 0 { + t.Error("expected no errors, but got", len(result.Errors)) } }) } diff --git a/pkg/action/list.go b/pkg/action/list.go index c9e6e364a..af0725c4a 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -125,6 +125,7 @@ type List struct { // Filter is a filter that is applied to the results Filter string Short bool + NoHeaders bool TimeFormat string Uninstalled bool Superseded bool diff --git a/pkg/action/package.go b/pkg/action/package.go index 52920956f..698169032 100644 --- a/pkg/action/package.go +++ b/pkg/action/package.go @@ -19,7 +19,6 @@ package action import ( "bufio" "fmt" - "io/ioutil" "os" "syscall" @@ -137,7 +136,7 @@ func (p *Package) Clearsign(filename string) error { return err } - return ioutil.WriteFile(filename+".prov", []byte(sig), 0644) + return os.WriteFile(filename+".prov", []byte(sig), 0644) } // promptUser implements provenance.PassphraseFetcher diff --git a/pkg/action/package_test.go b/pkg/action/package_test.go index 5c5fed571..d04efdaa6 100644 --- a/pkg/action/package_test.go +++ b/pkg/action/package_test.go @@ -17,7 +17,6 @@ limitations under the License. package action import ( - "io/ioutil" "os" "path" "testing" @@ -30,7 +29,6 @@ import ( 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 { @@ -50,7 +48,6 @@ func TestPassphraseFileFetcher(t *testing.T) { 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 { @@ -68,10 +65,9 @@ func TestPassphraseFileFetcher_WithLineBreak(t *testing.T) { } func TestPassphraseFileFetcher_WithInvalidStdin(t *testing.T) { - directory := ensure.TempDir(t) - defer os.RemoveAll(directory) + directory := t.TempDir() - stdin, err := ioutil.TempFile(directory, "non-existing") + stdin, err := os.CreateTemp(directory, "non-existing") if err != nil { t.Fatal("Unable to create test file", err) } diff --git a/pkg/action/pull.go b/pkg/action/pull.go index fa1247054..787553125 100644 --- a/pkg/action/pull.go +++ b/pkg/action/pull.go @@ -18,7 +18,6 @@ package action import ( "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -29,6 +28,7 @@ import ( "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/repo" ) @@ -71,6 +71,11 @@ func NewPullWithOpts(opts ...PullOpt) *Pull { return p } +// SetRegistryClient sets the registry client on the pull configuration object. +func (p *Pull) SetRegistryClient(client *registry.Client) { + p.cfg.RegistryClient = client +} + // Run executes 'helm pull' against the given release. func (p *Pull) Run(chartRef string) (string, error) { var out strings.Builder @@ -85,19 +90,17 @@ func (p *Pull) Run(chartRef string) (string, error) { getter.WithPassCredentialsAll(p.PassCredentialsAll), getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile), getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify), + getter.WithPlainHTTP(p.PlainHTTP), }, + RegistryClient: p.cfg.RegistryClient, 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") - } - + if registry.IsOCI(chartRef) { c.Options = append(c.Options, - getter.WithRegistryClient(p.cfg.RegistryClient), - getter.WithTagName(p.Version)) + getter.WithRegistryClient(p.cfg.RegistryClient)) + c.RegistryClient = p.cfg.RegistryClient } if p.Verify { @@ -111,7 +114,7 @@ func (p *Pull) Run(chartRef string) (string, error) { dest := p.DestDir if p.Untar { var err error - dest, err = ioutil.TempDir("", "helm-") + dest, err = os.MkdirTemp("", "helm-") if err != nil { return out.String(), errors.Wrap(err, "failed to untar") } diff --git a/pkg/action/push.go b/pkg/action/push.go new file mode 100644 index 000000000..68d2ba42d --- /dev/null +++ b/pkg/action/push.go @@ -0,0 +1,112 @@ +/* +Copyright The 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" + "strings" + + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/pusher" + "helm.sh/helm/v3/pkg/registry" + "helm.sh/helm/v3/pkg/uploader" +) + +// Push is the action for uploading a chart. +// +// It provides the implementation of 'helm push'. +type Push struct { + Settings *cli.EnvSettings + cfg *Configuration + certFile string + keyFile string + caFile string + insecureSkipTLSverify bool + plainHTTP bool + out io.Writer +} + +// PushOpt is a type of function that sets options for a push action. +type PushOpt func(*Push) + +// WithPushConfig sets the cfg field on the push configuration object. +func WithPushConfig(cfg *Configuration) PushOpt { + return func(p *Push) { + p.cfg = cfg + } +} + +// WithTLSClientConfig sets the certFile, keyFile, and caFile fields on the push configuration object. +func WithTLSClientConfig(certFile, keyFile, caFile string) PushOpt { + return func(p *Push) { + p.certFile = certFile + p.keyFile = keyFile + p.caFile = caFile + } +} + +// WithInsecureSkipTLSVerify determines if a TLS Certificate will be checked +func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) PushOpt { + return func(p *Push) { + p.insecureSkipTLSverify = insecureSkipTLSVerify + } +} + +// WithPlainHTTP configures the use of plain HTTP connections. +func WithPlainHTTP(plainHTTP bool) PushOpt { + return func(p *Push) { + p.plainHTTP = plainHTTP + } +} + +// WithOptWriter sets the registryOut field on the push configuration object. +func WithPushOptWriter(out io.Writer) PushOpt { + return func(p *Push) { + p.out = out + } +} + +// NewPushWithOpts creates a new push, with configuration options. +func NewPushWithOpts(opts ...PushOpt) *Push { + p := &Push{} + for _, fn := range opts { + fn(p) + } + return p +} + +// Run executes 'helm push' against the given chart archive. +func (p *Push) Run(chartRef string, remote string) (string, error) { + var out strings.Builder + + c := uploader.ChartUploader{ + Out: &out, + Pushers: pusher.All(p.Settings), + Options: []pusher.Option{ + pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile), + pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSverify), + pusher.WithPlainHTTP(p.plainHTTP), + }, + } + + if registry.IsOCI(remote) { + // Don't use the default registry client if tls options are set. + c.Options = append(c.Options, pusher.WithRegistryClient(p.cfg.RegistryClient)) + } + + return out.String(), c.UploadTo(chartRef, remote) +} diff --git a/pkg/action/registry_login.go b/pkg/action/registry_login.go index 00f6e2644..a55f2de58 100644 --- a/pkg/action/registry_login.go +++ b/pkg/action/registry_login.go @@ -18,11 +18,51 @@ package action import ( "io" + + "helm.sh/helm/v3/pkg/registry" ) // RegistryLogin performs a registry login operation. type RegistryLogin struct { - cfg *Configuration + cfg *Configuration + certFile string + keyFile string + caFile string + insecure bool +} + +type RegistryLoginOpt func(*RegistryLogin) error + +// WithCertFile specifies the path to the certificate file to use for TLS. +func WithCertFile(certFile string) RegistryLoginOpt { + return func(r *RegistryLogin) error { + r.certFile = certFile + return nil + } +} + +// WithKeyFile specifies whether to very certificates when communicating. +func WithInsecure(insecure bool) RegistryLoginOpt { + return func(r *RegistryLogin) error { + r.insecure = insecure + return nil + } +} + +// WithKeyFile specifies the path to the key file to use for TLS. +func WithKeyFile(keyFile string) RegistryLoginOpt { + return func(r *RegistryLogin) error { + r.keyFile = keyFile + return nil + } +} + +// WithCAFile specifies the path to the CA file to use for TLS. +func WithCAFile(caFile string) RegistryLoginOpt { + return func(r *RegistryLogin) error { + r.caFile = caFile + return nil + } } // NewRegistryLogin creates a new RegistryLogin object with the given configuration. @@ -33,6 +73,16 @@ func NewRegistryLogin(cfg *Configuration) *RegistryLogin { } // 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) +func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string, opts ...RegistryLoginOpt) error { + for _, opt := range opts { + if err := opt(a); err != nil { + return err + } + } + + return a.cfg.RegistryClient.Login( + hostname, + registry.LoginOptBasicAuth(username, password), + registry.LoginOptInsecure(a.insecure), + registry.LoginOptTLSClientConfig(a.certFile, a.keyFile, a.caFile)) } diff --git a/pkg/action/release_testing.go b/pkg/action/release_testing.go index ecaeaf59f..3c10cecf8 100644 --- a/pkg/action/release_testing.go +++ b/pkg/action/release_testing.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "io" + "sort" "time" "github.com/pkg/errors" @@ -29,6 +30,11 @@ import ( "helm.sh/helm/v3/pkg/release" ) +const ( + ExcludeNameFilter = "!name" + IncludeNameFilter = "name" +) + // ReleaseTesting is the action for testing a release. // // It provides the implementation of 'helm test'. @@ -66,9 +72,9 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) { skippedHooks := []*release.Hook{} executingHooks := []*release.Hook{} - if len(r.Filters["!name"]) != 0 { + if len(r.Filters[ExcludeNameFilter]) != 0 { for _, h := range rel.Hooks { - if contains(r.Filters["!name"], h.Name) { + if contains(r.Filters[ExcludeNameFilter], h.Name) { skippedHooks = append(skippedHooks, h) } else { executingHooks = append(executingHooks, h) @@ -76,10 +82,10 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) { } rel.Hooks = executingHooks } - if len(r.Filters["name"]) != 0 { + if len(r.Filters[IncludeNameFilter]) != 0 { executingHooks = nil for _, h := range rel.Hooks { - if contains(r.Filters["name"], h.Name) { + if contains(r.Filters[IncludeNameFilter], h.Name) { executingHooks = append(executingHooks, h) } else { skippedHooks = append(skippedHooks, h) @@ -107,9 +113,17 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error { return errors.Wrap(err, "unable to get kubernetes client to fetch pod logs") } - for _, h := range rel.Hooks { + hooksByWight := append([]*release.Hook{}, rel.Hooks...) + sort.Stable(hookByWeight(hooksByWight)) + for _, h := range hooksByWight { for _, e := range h.Events { if e == release.HookTest { + if contains(r.Filters[ExcludeNameFilter], h.Name) { + continue + } + if len(r.Filters[IncludeNameFilter]) > 0 && !contains(r.Filters[IncludeNameFilter], h.Name) { + continue + } req := client.CoreV1().Pods(r.Namespace).GetLogs(h.Name, &v1.PodLogOptions{}) logReader, err := req.Stream(context.Background()) if err != nil { diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index f3f958f3d..f4ae896e3 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -110,6 +110,24 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele previousVersion = currentRelease.Version - 1 } + historyReleases, err := r.cfg.Releases.History(name) + if err != nil { + return nil, nil, err + } + + // Check if the history version to be rolled back exists + previousVersionExist := false + for _, historyRelease := range historyReleases { + version := historyRelease.Version + if previousVersion == version { + previousVersionExist = true + break + } + } + if !previousVersionExist { + return nil, nil, errors.Errorf("release has no %d version", previousVersion) + } + r.cfg.Log("rolling back %s (current: v%d, target: v%d)", name, currentRelease.Version, previousVersion) previousRelease, err := r.cfg.Releases.Get(name, previousVersion) @@ -164,6 +182,11 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas r.cfg.Log("rollback hooks disabled for %s", targetRelease.Name) } + // It is safe to use "force" here because these are resources currently rendered by the chart. + err = target.Visit(setMetadataVisitor(targetRelease.Name, targetRelease.Namespace, true)) + if err != nil { + return targetRelease, errors.Wrap(err, "unable to set metadata visitor from target release") + } results, err := r.cfg.KubeClient.Update(current, target, r.Force) if err != nil { diff --git a/pkg/action/show.go b/pkg/action/show.go index 4eab2b4fd..6ed855b83 100644 --- a/pkg/action/show.go +++ b/pkg/action/show.go @@ -17,6 +17,7 @@ limitations under the License. package action import ( + "bytes" "fmt" "strings" @@ -27,6 +28,7 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/registry" ) // ShowOutputFormat is the format of the output of `helm show` @@ -41,6 +43,8 @@ const ( ShowValues ShowOutputFormat = "values" // ShowReadme is the format which only shows the chart's README ShowReadme ShowOutputFormat = "readme" + // ShowCRDs is the format which only shows the chart's CRDs + ShowCRDs ShowOutputFormat = "crds" ) var readmeFileNames = []string{"readme.md", "readme.txt", "readme"} @@ -61,12 +65,29 @@ type Show struct { } // NewShow creates a new Show object with the given configuration. +// Deprecated: Use NewShowWithConfig +// TODO Helm 4: Fold NewShowWithConfig back into NewShow func NewShow(output ShowOutputFormat) *Show { return &Show{ OutputFormat: output, } } +// NewShowWithConfig creates a new Show object with the given configuration. +func NewShowWithConfig(output ShowOutputFormat, cfg *Configuration) *Show { + sh := &Show{ + OutputFormat: output, + } + sh.ChartPathOptions.registryClient = cfg.RegistryClient + + return sh +} + +// SetRegistryClient sets the registry client to use when pulling a chart from a registry. +func (s *Show) SetRegistryClient(client *registry.Client) { + s.ChartPathOptions.registryClient = client +} + // Run executes 'helm show' against the given release. func (s *Show) Run(chartpath string) (string, error) { if s.chart == nil { @@ -106,14 +127,25 @@ func (s *Show) Run(chartpath string) (string, error) { } 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 + if readme != nil { + if s.OutputFormat == ShowAll { + fmt.Fprintln(&out, "---") + } + fmt.Fprintf(&out, "%s\n", readme.Data) + } + } + + if s.OutputFormat == ShowCRDs || s.OutputFormat == ShowAll { + crds := s.chart.CRDObjects() + if len(crds) > 0 { + if s.OutputFormat == ShowAll && !bytes.HasPrefix(crds[0].File.Data, []byte("---")) { + fmt.Fprintln(&out, "---") + } + for _, crd := range crds { + fmt.Fprintf(&out, "%s\n", string(crd.File.Data)) + } } - fmt.Fprintf(&out, "%s\n", readme.Data) } return out.String(), nil } @@ -121,6 +153,9 @@ func (s *Show) Run(chartpath string) (string, error) { func findReadme(files []*chart.File) (file *chart.File) { for _, file := range files { for _, n := range readmeFileNames { + if file == nil { + continue + } if strings.EqualFold(file.Name, n) { return file } diff --git a/pkg/action/show_test.go b/pkg/action/show_test.go index a2efdc8ac..8b617ea85 100644 --- a/pkg/action/show_test.go +++ b/pkg/action/show_test.go @@ -23,11 +23,15 @@ import ( ) func TestShow(t *testing.T) { - client := NewShow(ShowAll) + config := actionConfigFixture(t) + client := NewShowWithConfig(ShowAll, config) client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, Files: []*chart.File{ {Name: "README.md", Data: []byte("README\n")}, + {Name: "crds/ignoreme.txt", Data: []byte("error")}, + {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, }, Raw: []*chart.File{ {Name: "values.yaml", Data: []byte("VALUES\n")}, @@ -48,6 +52,12 @@ VALUES --- README +--- +foo + +--- +bar + ` if output != expect { t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) @@ -83,3 +93,61 @@ func TestShowValuesByJsonPathFormat(t *testing.T) { t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) } } + +func TestShowCRDs(t *testing.T) { + client := NewShow(ShowCRDs) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "alpine"}, + Files: []*chart.File{ + {Name: "crds/ignoreme.txt", Data: []byte("error")}, + {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, + }, + } + + output, err := client.Run("") + if err != nil { + t.Fatal(err) + } + + expect := `--- +foo + +--- +bar + +` + if output != expect { + t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) + } +} + +func TestShowNoReadme(t *testing.T) { + client := NewShow(ShowAll) + client.chart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "alpine"}, + Files: []*chart.File{ + {Name: "crds/ignoreme.txt", Data: []byte("error")}, + {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, + }, + } + + output, err := client.Run("") + if err != nil { + t.Fatal(err) + } + + expect := `name: alpine + +--- +foo + +--- +bar + +` + 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 index 1c556e28d..ee1c9d613 100644 --- a/pkg/action/status.go +++ b/pkg/action/status.go @@ -17,6 +17,10 @@ limitations under the License. package action import ( + "bytes" + "errors" + + "helm.sh/helm/v3/pkg/kube" "helm.sh/helm/v3/pkg/release" ) @@ -32,6 +36,14 @@ type Status struct { // only affect print type table. // TODO Helm 4: Remove this flag and output the description by default. ShowDescription bool + + // ShowResources sets if the resources should be retrieved with the status. + // TODO Helm 4: Remove this flag and output the resources by default. + ShowResources bool + + // ShowResourcesTable is used with ShowResources. When true this will cause + // the resulting objects to be retrieved as a kind=table. + ShowResourcesTable bool } // NewStatus creates a new Status object with the given configuration. @@ -47,5 +59,37 @@ func (s *Status) Run(name string) (*release.Release, error) { return nil, err } - return s.cfg.releaseContent(name, s.Version) + if !s.ShowResources { + return s.cfg.releaseContent(name, s.Version) + } + + rel, err := s.cfg.releaseContent(name, s.Version) + if err != nil { + return nil, err + } + + if kubeClient, ok := s.cfg.KubeClient.(kube.InterfaceResources); ok { + var resources kube.ResourceList + if s.ShowResourcesTable { + resources, err = kubeClient.BuildTable(bytes.NewBufferString(rel.Manifest), false) + if err != nil { + return nil, err + } + } else { + resources, err = s.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false) + if err != nil { + return nil, err + } + } + + resp, err := kubeClient.Get(resources, true) + if err != nil { + return nil, err + } + + rel.Info.Resources = resp + + return rel, nil + } + return nil, errors.New("unable to get kubeClient with interface InterfaceResources") } diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index c762159cb..40d82243e 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -22,7 +22,10 @@ import ( "github.com/pkg/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/kube" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" helmtime "helm.sh/helm/v3/pkg/time" @@ -34,11 +37,14 @@ import ( type Uninstall struct { cfg *Configuration - DisableHooks bool - DryRun bool - KeepHistory bool - Timeout time.Duration - Description string + DisableHooks bool + DryRun bool + IgnoreNotFound bool + KeepHistory bool + Wait bool + DeletionPropagation string + Timeout time.Duration + Description string } // NewUninstall creates a new Uninstall object with the given configuration. @@ -69,6 +75,9 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) rels, err := u.cfg.Releases.History(name) if err != nil { + if u.IgnoreNotFound { + return nil, nil + } return nil, errors.Wrapf(err, "uninstall: Release not loaded: %s", name) } if len(rels) < 1 { @@ -110,13 +119,25 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) u.cfg.Log("uninstall: Failed to store updated release: %s", err) } - kept, errs := u.deleteRelease(rel) + deletedResources, kept, errs := u.deleteRelease(rel) + if errs != nil { + u.cfg.Log("uninstall: Failed to delete release: %s", errs) + return nil, errors.Errorf("failed to delete release: %s", name) + } if kept != "" { kept = "These resources were kept due to the resource policy:\n" + kept } res.Info = kept + if u.Wait { + if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceExt); ok { + if err := kubeClient.WaitForDelete(deletedResources, u.Timeout); err != nil { + errs = append(errs, err) + } + } + } + if !u.DisableHooks { if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil { errs = append(errs, err) @@ -172,12 +193,12 @@ func joinErrors(errs []error) string { 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) { +// deleteRelease deletes the release and returns list of delete resources and manifests that were kept in the deletion process +func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, 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")} + return nil, rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")} } manifests := releaseutil.SplitManifests(rel.Manifest) @@ -187,7 +208,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) { // 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")} + return nil, rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")} } filesToKeep, filesToDelete := filterManifestsToKeep(files) @@ -203,10 +224,28 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) { 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")} + return nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")} } if len(resources) > 0 { + if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceDeletionPropagation); ok { + _, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.cfg, u.DeletionPropagation)) + return resources, kept, errs + } _, errs = u.cfg.KubeClient.Delete(resources) } - return kept, errs + return resources, kept, errs +} + +func parseCascadingFlag(cfg *Configuration, cascadingFlag string) v1.DeletionPropagation { + switch cascadingFlag { + case "orphan": + return v1.DeletePropagationOrphan + case "foreground": + return v1.DeletePropagationForeground + case "background": + return v1.DeletePropagationBackground + default: + cfg.Log("uninstall: given cascade value: %s, defaulting to delete propagation background", cascadingFlag) + return v1.DeletePropagationBackground + } } diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index 53c3bf8f9..869ffb8c7 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -17,9 +17,13 @@ limitations under the License. package action import ( + "fmt" "testing" "github.com/stretchr/testify/assert" + + kubefake "helm.sh/helm/v3/pkg/kube/fake" + "helm.sh/helm/v3/pkg/release" ) func uninstallAction(t *testing.T) *Uninstall { @@ -28,6 +32,17 @@ func uninstallAction(t *testing.T) *Uninstall { return unAction } +func TestUninstallRelease_ignoreNotFound(t *testing.T) { + unAction := uninstallAction(t) + unAction.DryRun = false + unAction.IgnoreNotFound = true + + is := assert.New(t) + res, err := unAction.Run("release-non-exist") + is.Nil(res) + is.NoError(err) +} + func TestUninstallRelease_deleteRelease(t *testing.T) { is := assert.New(t) @@ -60,3 +75,66 @@ func TestUninstallRelease_deleteRelease(t *testing.T) { ` is.Contains(res.Info, expected) } + +func TestUninstallRelease_Wait(t *testing.T) { + is := assert.New(t) + + unAction := uninstallAction(t) + unAction.DisableHooks = true + unAction.DryRun = false + unAction.Wait = true + + rel := releaseStub() + rel.Name = "come-fail-away" + rel.Manifest = `{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "secret" + }, + "type": "Opaque", + "data": { + "password": "password" + } + }` + unAction.cfg.Releases.Create(rel) + failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitError = fmt.Errorf("U timed out") + unAction.cfg.KubeClient = failer + res, err := unAction.Run(rel.Name) + is.Error(err) + is.Contains(err.Error(), "U timed out") + is.Equal(res.Release.Info.Status, release.StatusUninstalled) +} + +func TestUninstallRelease_Cascade(t *testing.T) { + is := assert.New(t) + + unAction := uninstallAction(t) + unAction.DisableHooks = true + unAction.DryRun = false + unAction.Wait = false + unAction.DeletionPropagation = "foreground" + + rel := releaseStub() + rel.Name = "come-fail-away" + rel.Manifest = `{ + "apiVersion": "v1", + "kind": "Secret", + "metadata": { + "name": "secret" + }, + "type": "Opaque", + "data": { + "password": "password" + } + }` + unAction.cfg.Releases.Create(rel) + failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.DeleteWithPropagationError = fmt.Errorf("Uninstall with cascade failed") + failer.BuildDummy = true + unAction.cfg.KubeClient = failer + _, err := unAction.Run(rel.Name) + is.Error(err) + is.Contains(err.Error(), "failed to delete release: come-fail-away") +} diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 07d9cb40e..c74b502e2 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -21,6 +21,7 @@ import ( "context" "fmt" "strings" + "sync" "time" "github.com/pkg/errors" @@ -31,6 +32,7 @@ import ( "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/kube" "helm.sh/helm/v3/pkg/postrender" + "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/storage/driver" @@ -69,8 +71,9 @@ type Upgrade struct { // 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 + // DryRunOption controls whether the operation is prepared, but not executed with options on whether or not to interact with the remote cluster. + DryRunOption string // Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. // // This should be used with caution. @@ -91,6 +94,7 @@ type Upgrade struct { SubNotes bool // Description is the description of this operation Description string + Labels map[string]string // PostRender is an optional post-renderer // // If this is non-nil, then after templates are rendered, they will be sent to the @@ -100,17 +104,40 @@ type Upgrade struct { DisableOpenAPIValidation bool // Get missing dependencies DependencyUpdate bool + // Lock to control raceconditions when the process receives a SIGTERM + Lock sync.Mutex + // Enable DNS lookups when rendering templates + EnableDNS bool +} + +type resultMessage struct { + r *release.Release + e error } // NewUpgrade creates a new Upgrade object with the given configuration. func NewUpgrade(cfg *Configuration) *Upgrade { - return &Upgrade{ + up := &Upgrade{ cfg: cfg, } + up.ChartPathOptions.registryClient = cfg.RegistryClient + + return up +} + +// SetRegistryClient sets the registry client to use when fetching charts. +func (u *Upgrade) SetRegistryClient(client *registry.Client) { + u.ChartPathOptions.registryClient = client } // Run executes the upgrade on the given release. func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + ctx := context.Background() + return u.RunWithContext(ctx, name, chart, vals) +} + +// RunWithContext executes the upgrade on the given release with context. +func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { if err := u.cfg.KubeClient.IsReachable(); err != nil { return nil, err } @@ -122,6 +149,7 @@ func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface 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 { @@ -131,12 +159,13 @@ func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface u.cfg.Releases.MaxHistory = u.MaxHistory u.cfg.Log("performing update for %s", name) - res, err := u.performUpgrade(currentRelease, upgradedRelease) + res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease) if err != nil { return res, err } - if !u.DryRun { + // Do not update for dry runs + if !u.isDryRun() { u.cfg.Log("updating status for upgraded release for %s", name) if err := u.cfg.Releases.Update(upgradedRelease); err != nil { return res, err @@ -146,6 +175,14 @@ func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface return res, nil } +// isDryRun returns true if Upgrade is set to run as a DryRun +func (u *Upgrade) isDryRun() bool { + if u.DryRun || u.DryRunOption == "client" || u.DryRunOption == "server" || u.DryRunOption == "true" { + return true + } + return false +} + // 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 { @@ -190,7 +227,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin return nil, nil, err } - if err := chartutil.ProcessDependencies(chart, vals); err != nil { + if err := chartutil.ProcessDependenciesWithMerge(chart, vals); err != nil { return nil, nil, err } @@ -214,11 +251,21 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin return nil, nil, err } - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun) + // Determine whether or not to interact with remote + var interactWithRemote bool + if !u.isDryRun() || u.DryRunOption == "server" || u.DryRunOption == "none" || u.DryRunOption == "false" { + interactWithRemote = true + } + + hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS) if err != nil { return nil, nil, err } + if driver.ContainsSystemLabels(u.Labels) { + return nil, nil, fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()) + } + // Store an upgraded release. upgradedRelease := &release.Release{ Name: name, @@ -234,6 +281,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin Version: revision, Manifest: manifestDoc.String(), Hooks: hooks, + Labels: mergeCustomLabels(lastRelease.Labels, u.Labels), } if len(notesTxt) > 0 { @@ -243,7 +291,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin return currentRelease, upgradedRelease, err } -func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Release) (*release.Release, error) { +func (u *Upgrade) performUpgrade(ctx context.Context, 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 @@ -281,7 +329,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea 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") + return nil, errors.Wrap(err, "Unable to continue with update") } toBeUpdated.Visit(func(r *resource.Info, err error) error { @@ -292,7 +340,8 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea return nil }) - if u.DryRun { + // Run if it is a dry run + if u.isDryRun() { u.cfg.Log("dry run for %s", upgradedRelease.Name) if len(u.Description) > 0 { upgradedRelease.Info.Description = u.Description @@ -306,11 +355,51 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea if err := u.cfg.Releases.Create(upgradedRelease); err != nil { return nil, err } + rChan := make(chan resultMessage) + ctxChan := make(chan resultMessage) + doneChan := make(chan interface{}) + defer close(doneChan) + go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease) + go u.handleContext(ctx, doneChan, ctxChan, upgradedRelease) + select { + case result := <-rChan: + return result.r, result.e + case result := <-ctxChan: + return result.r, result.e + } +} + +// Function used to lock the Mutex, this is important for the case when the atomic flag is set. +// In that case the upgrade will finish before the rollback is finished so it is necessary to wait for the rollback to finish. +// The rollback will be trigger by the function failRelease +func (u *Upgrade) reportToPerformUpgrade(c chan<- resultMessage, rel *release.Release, created kube.ResourceList, err error) { + u.Lock.Lock() + if err != nil { + rel, err = u.failRelease(rel, created, err) + } + c <- resultMessage{r: rel, e: err} + u.Lock.Unlock() +} +// Setup listener for SIGINT and SIGTERM +func (u *Upgrade) handleContext(ctx context.Context, done chan interface{}, c chan<- resultMessage, upgradedRelease *release.Release) { + select { + case <-ctx.Done(): + err := ctx.Err() + + // when the atomic flag is set the ongoing release finish first and doesn't give time for the rollback happens. + u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, err) + case <-done: + return + } +} +func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) { // 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)) + u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err)) + return } } else { u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name) @@ -319,7 +408,8 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea results, err := u.cfg.KubeClient.Update(current, target, u.Force) if err != nil { u.cfg.recordRelease(originalRelease) - return u.failRelease(upgradedRelease, results.Created, err) + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) + return } if u.Recreate { @@ -333,15 +423,20 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea } if u.Wait { + u.cfg.Log( + "waiting for release %s resources (created: %d updated: %d deleted: %d)", + upgradedRelease.Name, len(results.Created), len(results.Updated), len(results.Deleted)) 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) + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) + return } } else { if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil { u.cfg.recordRelease(originalRelease) - return u.failRelease(upgradedRelease, results.Created, err) + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) + return } } } @@ -349,7 +444,8 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea // 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)) + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err)) + return } } @@ -362,8 +458,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea } else { upgradedRelease.Info.Description = "Upgrade complete" } - - return upgradedRelease, nil + u.reportToPerformUpgrade(c, upgradedRelease, nil, nil) } func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) { @@ -509,3 +604,13 @@ 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) } + +func mergeCustomLabels(current, desired map[string]string) map[string]string { + labels := mergeStrStrMaps(current, desired) + for k, v := range labels { + if v == "null" { + delete(labels, k) + } + } + return labels +} diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 5cca7ca1a..77656e1c5 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -17,17 +17,21 @@ limitations under the License. package action import ( + "context" "fmt" + "reflect" "testing" + "time" "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/storage/driver" "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" + helmtime "helm.sh/helm/v3/pkg/time" ) func upgradeAction(t *testing.T) *Upgrade { @@ -38,6 +42,33 @@ func upgradeAction(t *testing.T) *Upgrade { return upAction } +func TestUpgradeRelease_Success(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "previous-release" + rel.Info.Status = release.StatusDeployed + req.NoError(upAction.cfg.Releases.Create(rel)) + + upAction.Wait = true + vals := map[string]interface{}{} + + ctx, done := context.WithCancel(context.Background()) + res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) + done() + req.NoError(err) + is.Equal(res.Info.Status, release.StatusDeployed) + + // Detecting previous bug where context termination after successful release + // caused release to fail. + time.Sleep(time.Millisecond * 100) + lastRelease, err := upAction.cfg.Releases.Last(rel.Name) + req.NoError(err) + is.Equal(lastRelease.Info.Status, release.StatusDeployed) +} + func TestUpgradeRelease_Wait(t *testing.T) { is := assert.New(t) req := require.New(t) @@ -225,7 +256,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { withValues(chartDefaultValues), withMetadataDependency(dependency), ) - now := time.Now() + now := helmtime.Now() existingValues := map[string]interface{}{ "subchart": map[string]interface{}{ "enabled": false, @@ -296,3 +327,158 @@ func TestUpgradeRelease_Pending(t *testing.T) { _, err := upAction.Run(rel.Name, buildChart(), vals) req.Contains(err.Error(), "progress", err) } + +func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { + + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "interrupted-release" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitDuration = 10 * time.Second + upAction.cfg.KubeClient = failer + upAction.Wait = true + vals := map[string]interface{}{} + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + time.AfterFunc(time.Second, cancel) + + res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) + + req.Error(err) + is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled") + is.Equal(res.Info.Status, release.StatusFailed) + +} + +func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) { + + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "interrupted-release" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitDuration = 5 * time.Second + upAction.cfg.KubeClient = failer + upAction.Atomic = true + vals := map[string]interface{}{} + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + time.AfterFunc(time.Second, cancel) + + res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) + + req.Error(err) + is.Contains(err.Error(), "release interrupted-release failed, and has been rolled back due to atomic being set: context canceled") + + // 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) +} + +func TestMergeCustomLabels(t *testing.T) { + var tests = [][3]map[string]string{ + {nil, nil, map[string]string{}}, + {map[string]string{}, map[string]string{}, map[string]string{}}, + {map[string]string{"k1": "v1", "k2": "v2"}, nil, map[string]string{"k1": "v1", "k2": "v2"}}, + {nil, map[string]string{"k1": "v1", "k2": "v2"}, map[string]string{"k1": "v1", "k2": "v2"}}, + {map[string]string{"k1": "v1", "k2": "v2"}, map[string]string{"k1": "null", "k2": "v3"}, map[string]string{"k2": "v3"}}, + } + for _, test := range tests { + if output := mergeCustomLabels(test[0], test[1]); !reflect.DeepEqual(test[2], output) { + t.Errorf("Expected {%v}, got {%v}", test[2], output) + } + } +} + +func TestUpgradeRelease_Labels(t *testing.T) { + is := assert.New(t) + upAction := upgradeAction(t) + + rel := releaseStub() + rel.Name = "labels" + // It's needed to check that suppressed release would keep original labels + rel.Labels = map[string]string{ + "key1": "val1", + "key2": "val2.1", + } + rel.Info.Status = release.StatusDeployed + + err := upAction.cfg.Releases.Create(rel) + is.NoError(err) + + upAction.Labels = map[string]string{ + "key1": "null", + "key2": "val2.2", + "key3": "val3", + } + // setting newValues and upgrading + res, err := upAction.Run(rel.Name, buildChart(), nil) + is.NoError(err) + + // Now make sure it is actually upgraded and labels were merged + 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(mergeCustomLabels(rel.Labels, upAction.Labels), updatedRes.Labels) + + // Now make sure it is suppressed release still contains original labels + initialRes, err := upAction.cfg.Releases.Get(res.Name, 1) + is.NoError(err) + + if initialRes == nil { + is.Fail("Updated Release is nil") + return + } + is.Equal(initialRes.Info.Status, release.StatusSuperseded) + is.Equal(initialRes.Labels, rel.Labels) +} + +func TestUpgradeRelease_SystemLabels(t *testing.T) { + is := assert.New(t) + upAction := upgradeAction(t) + + rel := releaseStub() + rel.Name = "labels" + // It's needed to check that suppressed release would keep original labels + rel.Labels = map[string]string{ + "key1": "val1", + "key2": "val2.1", + } + rel.Info.Status = release.StatusDeployed + + err := upAction.cfg.Releases.Create(rel) + is.NoError(err) + + upAction.Labels = map[string]string{ + "key1": "null", + "key2": "val2.2", + "owner": "val3", + } + // setting newValues and upgrading + _, err = upAction.Run(rel.Name, buildChart(), nil) + if err == nil { + t.Fatal("expected an error") + } + + is.Equal(fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()), err) +} diff --git a/pkg/action/validate.go b/pkg/action/validate.go index 6e074f78b..73eb1937b 100644 --- a/pkg/action/validate.go +++ b/pkg/action/validate.go @@ -51,7 +51,7 @@ func existingResourceConflict(resources kube.ResourceList, releaseName, releaseN if apierrors.IsNotFound(err) { return nil } - return errors.Wrap(err, "could not get information about the resource") + return errors.Wrapf(err, "could not get information about the resource %s", resourceString(info)) } // Allow adoption of the resource if it is managed by Helm and is annotated with correct release name and namespace. diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go index ef8cec3ad..62d60765c 100644 --- a/pkg/chart/chart_test.go +++ b/pkg/chart/chart_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/chart/dependency.go b/pkg/chart/dependency.go index b2819f373..4ef5eeb32 100644 --- a/pkg/chart/dependency.go +++ b/pkg/chart/dependency.go @@ -53,6 +53,9 @@ type Dependency struct { // the chart. This check must be done at load time before the dependency's charts are // loaded. func (d *Dependency) Validate() error { + if d == nil { + return ValidationError("dependencies must not contain empty or null nodes") + } d.Name = sanitizeString(d.Name) d.Version = sanitizeString(d.Version) d.Repository = sanitizeString(d.Repository) diff --git a/pkg/chart/dependency_test.go b/pkg/chart/dependency_test.go index 99c45b4b5..90488a966 100644 --- a/pkg/chart/dependency_test.go +++ b/pkg/chart/dependency_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/chart/loader/archive.go b/pkg/chart/loader/archive.go index 8b38cb89f..196e5f81d 100644 --- a/pkg/chart/loader/archive.go +++ b/pkg/chart/loader/archive.go @@ -85,7 +85,10 @@ func ensureArchive(name string, raw *os.File) error { 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" { + + // Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject. + // Fix for: https://github.com/helm/helm/issues/12261 + if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) { // 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. @@ -98,6 +101,12 @@ func ensureArchive(name string, raw *os.File) error { return nil } +// isGZipApplication checks whether the achieve is of the application/x-gzip type. +func isGZipApplication(data []byte) bool { + sig := []byte("\x1F\x8B\x08") + return bytes.HasPrefix(data, sig) +} + // 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 diff --git a/pkg/chart/loader/directory.go b/pkg/chart/loader/directory.go index bbe543870..489eea93c 100644 --- a/pkg/chart/loader/directory.go +++ b/pkg/chart/loader/directory.go @@ -19,7 +19,6 @@ package loader import ( "bytes" "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -102,7 +101,7 @@ func LoadDir(dir string) (*chart.Chart, error) { return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name) } - data, err := ioutil.ReadFile(name) + data, err := os.ReadFile(name) if err != nil { return errors.Wrapf(err, "error reading %s", n) } diff --git a/pkg/chart/loader/load_test.go b/pkg/chart/loader/load_test.go index 9605b3152..098e6155f 100644 --- a/pkg/chart/loader/load_test.go +++ b/pkg/chart/loader/load_test.go @@ -21,7 +21,6 @@ import ( "bytes" "compress/gzip" "io" - "io/ioutil" "log" "os" "path/filepath" @@ -90,13 +89,13 @@ func TestLoadDirWithSymlink(t *testing.T) { 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) + data, err := os.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") + archive, err := os.ReadFile("testdata/frobnitz_with_bom.tgz") if err != nil { t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) } @@ -403,11 +402,7 @@ func TestLoadV2WithReqs(t *testing.T) { } func TestLoadInvalidArchive(t *testing.T) { - tmpdir, err := ioutil.TempDir("", "helm-test-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpdir) + tmpdir := t.TempDir() writeTar := func(filename, internalPath string, body []byte) { dest, err := os.Create(filename) @@ -459,7 +454,7 @@ func TestLoadInvalidArchive(t *testing.T) { } { illegalChart := filepath.Join(tmpdir, tt.chartname) writeTar(illegalChart, tt.internal, []byte("hello: world")) - _, err = Load(illegalChart) + _, err := Load(illegalChart) if err == nil { t.Fatal("expected error when unpacking illegal files") } @@ -471,7 +466,7 @@ func TestLoadInvalidArchive(t *testing.T) { // 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) + _, err := Load(illegalChart) if err.Error() != "validation: chart.metadata.name is required" { t.Error(err) } diff --git a/pkg/chart/metadata.go b/pkg/chart/metadata.go index 1925e45ac..ae572abb7 100644 --- a/pkg/chart/metadata.go +++ b/pkg/chart/metadata.go @@ -34,6 +34,9 @@ type Maintainer struct { // Validate checks valid data and sanitizes string characters. func (m *Maintainer) Validate() error { + if m == nil { + return ValidationError("maintainers must not contain empty or null nodes") + } m.Name = sanitizeString(m.Name) m.Email = sanitizeString(m.Email) m.URL = sanitizeString(m.URL) diff --git a/pkg/chart/metadata_test.go b/pkg/chart/metadata_test.go index 9f881a4e1..cc04f095b 100644 --- a/pkg/chart/metadata_test.go +++ b/pkg/chart/metadata_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -72,6 +72,30 @@ func TestValidate(t *testing.T) { }, ValidationError("dependency \"bad\" has disallowed characters in the alias"), }, + { + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + nil, + }, + }, + ValidationError("dependencies must not contain empty or null nodes"), + }, + { + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Maintainers: []*Maintainer{ + nil, + }, + }, + ValidationError("maintainers must not contain empty or null nodes"), + }, { &Metadata{APIVersion: "v2", Name: "test", Version: "1.2.3.4"}, ValidationError("chart.metadata.version \"1.2.3.4\" is invalid"), diff --git a/pkg/chartutil/capabilities.go b/pkg/chartutil/capabilities.go index d24228056..5f57e11a5 100644 --- a/pkg/chartutil/capabilities.go +++ b/pkg/chartutil/capabilities.go @@ -28,21 +28,21 @@ import ( helmversion "helm.sh/helm/v3/internal/version" ) -const ( - k8sVersionMajor = 1 - k8sVersionMinor = 20 -) - var ( + // The Kubernetes version can be set by LDFLAGS. In order to do that the value + // must be a string. + k8sVersionMajor = "1" + k8sVersionMinor = "20" + // DefaultVersionSet is the default version set, which includes only Core V1 ("v1"). 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), + Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor), + Major: k8sVersionMajor, + Minor: k8sVersionMinor, }, APIVersions: DefaultVersionSet, HelmVersion: helmversion.Get(), diff --git a/pkg/chartutil/capabilities_test.go b/pkg/chartutil/capabilities_test.go index fad20f1dc..af56907eb 100644 --- a/pkg/chartutil/capabilities_test.go +++ b/pkg/chartutil/capabilities_test.go @@ -62,8 +62,8 @@ func TestDefaultCapabilities(t *testing.T) { func TestDefaultCapabilitiesHelmVersion(t *testing.T) { hv := DefaultCapabilities.HelmVersion - if hv.Version != "v3.6" { - t.Errorf("Expected default HelmVersion to be v3.6, got %q", hv.Version) + if hv.Version != "v3.13" { + t.Errorf("Expected default HelmVersion to be v3.13, got %q", hv.Version) } } diff --git a/pkg/chartutil/chartfile.go b/pkg/chartutil/chartfile.go index 808a902b1..4f537a6e7 100644 --- a/pkg/chartutil/chartfile.go +++ b/pkg/chartutil/chartfile.go @@ -17,7 +17,6 @@ limitations under the License. package chartutil import ( - "io/ioutil" "os" "path/filepath" @@ -29,7 +28,7 @@ import ( // LoadChartfile loads a Chart.yaml file into a *chart.Metadata. func LoadChartfile(filename string) (*chart.Metadata, error) { - b, err := ioutil.ReadFile(filename) + b, err := os.ReadFile(filename) if err != nil { return nil, err } @@ -55,7 +54,7 @@ func SaveChartfile(filename string, cf *chart.Metadata) error { if err != nil { return err } - return ioutil.WriteFile(filename, out, 0644) + return os.WriteFile(filename, out, 0644) } // IsChartDir validate a chart directory. @@ -73,7 +72,7 @@ func IsChartDir(dirName string) (bool, error) { return false, errors.Errorf("no %s exists in directory %q", ChartfileName, dirName) } - chartYamlContent, err := ioutil.ReadFile(chartYaml) + chartYamlContent, err := os.ReadFile(chartYaml) if err != nil { return false, errors.Errorf("cannot read %s in directory %q", ChartfileName, dirName) } diff --git a/pkg/chartutil/chartfile_test.go b/pkg/chartutil/chartfile_test.go index fb5f15376..ef5c5462a 100644 --- a/pkg/chartutil/chartfile_test.go +++ b/pkg/chartutil/chartfile_test.go @@ -35,11 +35,11 @@ func TestLoadChartfile(t *testing.T) { func verifyChartfile(t *testing.T, f *chart.Metadata, name string) { - if f == nil { + if f == nil { //nolint:staticcheck t.Fatal("Failed verifyChartfile because f is nil") } - if f.APIVersion != chart.APIVersionV1 { + if f.APIVersion != chart.APIVersionV1 { //nolint:staticcheck t.Errorf("Expected API Version %q, got %q", chart.APIVersionV1, f.APIVersion) } diff --git a/pkg/chartutil/coalesce.go b/pkg/chartutil/coalesce.go index 7b1ee1d80..6cf23a122 100644 --- a/pkg/chartutil/coalesce.go +++ b/pkg/chartutil/coalesce.go @@ -17,6 +17,7 @@ limitations under the License. package chartutil import ( + "fmt" "log" "github.com/mitchellh/copystructure" @@ -25,16 +26,53 @@ import ( "helm.sh/helm/v3/pkg/chart" ) +func concatPrefix(a, b string) string { + if a == "" { + return b + } + return fmt.Sprintf("%s.%s", a, b) +} + // 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. +// - 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) { + valsCopy, err := copyValues(vals) + if err != nil { + return vals, err + } + return coalesce(log.Printf, chrt, valsCopy, "", false) +} + +// MergeValues is used to merge the values in a chart and its subcharts. This +// is different from Coalescing as nil/null values are preserved. +// +// 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. +// +// Retaining Nils is useful when processes early in a Helm action or business +// logic need to retain them for when Coalescing will happen again later in the +// business logic. +func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { + valsCopy, err := copyValues(vals) + if err != nil { + return vals, err + } + return coalesce(log.Printf, chrt, valsCopy, "", true) +} + +func copyValues(vals map[string]interface{}) (Values, error) { v, err := copystructure.Copy(vals) if err != nil { return vals, err @@ -45,19 +83,26 @@ func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, err if valsCopy == nil { valsCopy = make(map[string]interface{}) } - return coalesce(chrt, valsCopy) + + return valsCopy, nil } +type printFn func(format string, v ...interface{}) + // 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) +// This is a helper function for CoalesceValues and MergeValues. +// +// Note, the merge argument specifies whether this is being used by MergeValues +// or CoalesceValues. Coalescing removes null values and their keys in some +// situations while merging keeps the null values. +func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { + coalesceValues(printf, ch, dest, prefix, merge) + return coalesceDeps(printf, ch, dest, prefix, merge) } // coalesceDeps coalesces the dependencies of the given chart. -func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) { +func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (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. @@ -67,13 +112,12 @@ func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]in } if dv, ok := dest[subchart.Name()]; ok { dvmap := dv.(map[string]interface{}) - + subPrefix := concatPrefix(prefix, chrt.Metadata.Name) // Get globals out of dest and merge them into dvmap. - coalesceGlobals(dvmap, dest) - + coalesceGlobals(printf, dvmap, dest, subPrefix, merge) // Now coalesce the rest of the values. var err error - dest[subchart.Name()], err = coalesce(subchart, dvmap) + dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge) if err != nil { return dest, err } @@ -85,20 +129,20 @@ func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]in // coalesceGlobals copies the globals out of src and merges them into dest. // // For convenience, returns dest. -func coalesceGlobals(dest, src map[string]interface{}) { +func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, merge bool) { 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) + 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) + printf("warning: skipping globals because source %s is not a table.", GlobalKey) return } @@ -114,19 +158,21 @@ func coalesceGlobals(dest, src map[string]interface{}) { 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) + 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) + subPrefix := concatPrefix(prefix, key) + // In this location coalesceTablesFullKey should always have + // merge set to true. The output of coalesceGlobals is run + // through coalesce where any nils will be removed. + coalesceTablesFullKey(printf, vv, destvmap, subPrefix, true) 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 + printf("key %s is table. Skipping", key) } else { // TODO: Do we need to do any additional checking on the value? dg[key] = val @@ -146,11 +192,38 @@ func copyMap(src map[string]interface{}) map[string]interface{} { // 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 { +func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) { + subPrefix := concatPrefix(prefix, c.Metadata.Name) + + // Using c.Values directly when coalescing a table can cause problems where + // the original c.Values is altered. Creating a deep copy stops the problem. + // This section is fault-tolerant as there is no ability to return an error. + valuesCopy, err := copystructure.Copy(c.Values) + var vc map[string]interface{} + var ok bool + if err != nil { + // If there is an error something is wrong with copying c.Values it + // means there is a problem in the deep copying package or something + // wrong with c.Values. In this case we will use c.Values and report + // an error. + printf("warning: unable to copy values, err: %s", err) + vc = c.Values + } else { + vc, ok = valuesCopy.(map[string]interface{}) + if !ok { + // c.Values has a map[string]interface{} structure. If the copy of + // it cannot be treated as map[string]interface{} there is something + // strangely wrong. Log it and use c.Values + printf("warning: unable to convert values copy to values type") + vc = c.Values + } + } + + for key, val := range vc { if value, ok := v[key]; ok { - if value == nil { - // When the YAML value is null, we remove the value's key. + if value == nil && !merge { + // When the YAML value is null and we are coalescing instead of + // merging, 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) @@ -159,15 +232,15 @@ func coalesceValues(c *chart.Chart, v map[string]interface{}) { 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 + // the warning if val != nil { - log.Printf("warning: skipped value for %s: Not a table.", key) + printf("warning: skipped value for %s.%s: Not a table.", subPrefix, key) } - continue + } else { + // Because v has higher precedence than nv, dest values override src + // values. + coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge) } - // 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. @@ -180,6 +253,17 @@ func coalesceValues(c *chart.Chart, v map[string]interface{}) { // // dest is considered authoritative. func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { + return coalesceTablesFullKey(log.Printf, dst, src, "", false) +} + +func MergeTables(dst, src map[string]interface{}) map[string]interface{} { + return coalesceTablesFullKey(log.Printf, dst, src, "", true) +} + +// coalesceTablesFullKey merges a source map into a destination map. +// +// dest is considered authoritative. +func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string, merge bool) map[string]interface{} { // When --reuse-values is set but there are no modifications yet, return new values if src == nil { return dst @@ -190,18 +274,19 @@ 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 dv, ok := dst[key]; ok && dv == nil { + fullkey := concatPrefix(prefix, key) + if dv, ok := dst[key]; ok && !merge && 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{})) + coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey, merge) } else { - log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val) + printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val) } } else if istable(dv) && val != nil { - log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val) + printf("warning: destination for %s is a table. Ignoring non-table value (%v)", fullkey, val) } } return dst diff --git a/pkg/chartutil/coalesce_test.go b/pkg/chartutil/coalesce_test.go index fc55e0a25..61b718d97 100644 --- a/pkg/chartutil/coalesce_test.go +++ b/pkg/chartutil/coalesce_test.go @@ -18,6 +18,7 @@ package chartutil import ( "encoding/json" + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -78,17 +79,27 @@ func TestCoalesceValues(t *testing.T) { "right": "exists", "scope": "moby", "top": "nope", + "global": map[string]interface{}{ + "nested2": map[string]interface{}{"l0": "moby"}, + }, }, }, withDeps(&chart.Chart{ Metadata: &chart.Metadata{Name: "pequod"}, - Values: map[string]interface{}{"name": "pequod", "scope": "pequod"}, + Values: map[string]interface{}{ + "name": "pequod", + "scope": "pequod", + "global": map[string]interface{}{ + "nested2": map[string]interface{}{"l1": "pequod"}, + }, + }, }, &chart.Chart{ Metadata: &chart.Metadata{Name: "ahab"}, Values: map[string]interface{}{ "global": map[string]interface{}{ - "nested": map[string]interface{}{"foo": "bar"}, + "nested": map[string]interface{}{"foo": "bar"}, + "nested2": map[string]interface{}{"l2": "ahab"}, }, "scope": "ahab", "name": "ahab", @@ -99,7 +110,12 @@ func TestCoalesceValues(t *testing.T) { ), &chart.Chart{ Metadata: &chart.Metadata{Name: "spouter"}, - Values: map[string]interface{}{"scope": "spouter"}, + Values: map[string]interface{}{ + "scope": "spouter", + "global": map[string]interface{}{ + "nested2": map[string]interface{}{"l1": "spouter"}, + }, + }, }, ) @@ -152,6 +168,19 @@ func TestCoalesceValues(t *testing.T) { {"{{.spouter.global.nested.boat}}", "true"}, {"{{.pequod.global.nested.sail}}", "true"}, {"{{.spouter.global.nested.sail}}", ""}, + + {"{{.global.nested2.l0}}", "moby"}, + {"{{.global.nested2.l1}}", ""}, + {"{{.global.nested2.l2}}", ""}, + {"{{.pequod.global.nested2.l0}}", "moby"}, + {"{{.pequod.global.nested2.l1}}", "pequod"}, + {"{{.pequod.global.nested2.l2}}", ""}, + {"{{.pequod.ahab.global.nested2.l0}}", "moby"}, + {"{{.pequod.ahab.global.nested2.l1}}", "pequod"}, + {"{{.pequod.ahab.global.nested2.l2}}", "ahab"}, + {"{{.spouter.global.nested2.l0}}", "moby"}, + {"{{.spouter.global.nested2.l1}}", "spouter"}, + {"{{.spouter.global.nested2.l2}}", ""}, } for _, tt := range tests { @@ -184,6 +213,160 @@ func TestCoalesceValues(t *testing.T) { is.Equal(valsCopy, vals) } +func TestMergeValues(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", + "global": map[string]interface{}{ + "nested2": map[string]interface{}{"l0": "moby"}, + }, + }, + }, + withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "pequod"}, + Values: map[string]interface{}{ + "name": "pequod", + "scope": "pequod", + "global": map[string]interface{}{ + "nested2": map[string]interface{}{"l1": "pequod"}, + }, + }, + }, + &chart.Chart{ + Metadata: &chart.Metadata{Name: "ahab"}, + Values: map[string]interface{}{ + "global": map[string]interface{}{ + "nested": map[string]interface{}{"foo": "bar"}, + "nested2": map[string]interface{}{"l2": "ahab"}, + }, + "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", + "global": map[string]interface{}{ + "nested2": map[string]interface{}{"l1": "spouter"}, + }, + }, + }, + ) + + vals, err := ReadValues(testCoalesceValuesYaml) + if err != nil { + t.Fatal(err) + } + + // taking a copy of the values before passing it + // to MergeValues 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 := MergeValues(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.nested.foo}}", "bar"}, + {"{{.pequod.ahab.global.subject}}", "Queequeg"}, + {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, + {"{{.pequod.global.name}}", "Ishmael"}, + {"{{.pequod.global.nested.foo}}", ""}, + {"{{.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}}", ""}, + + {"{{.global.nested2.l0}}", "moby"}, + {"{{.global.nested2.l1}}", ""}, + {"{{.global.nested2.l2}}", ""}, + {"{{.pequod.global.nested2.l0}}", "moby"}, + {"{{.pequod.global.nested2.l1}}", "pequod"}, + {"{{.pequod.global.nested2.l2}}", ""}, + {"{{.pequod.ahab.global.nested2.l0}}", "moby"}, + {"{{.pequod.ahab.global.nested2.l1}}", "pequod"}, + {"{{.pequod.ahab.global.nested2.l2}}", "ahab"}, + {"{{.spouter.global.nested2.l0}}", "moby"}, + {"{{.spouter.global.nested2.l1}}", "spouter"}, + {"{{.spouter.global.nested2.l2}}", ""}, + } + + 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 is different from coalescing. Here the null/nil values are not + // removed. + nullKeys := []string{"bottom", "right", "left", "front"} + for _, nullKey := range nullKeys { + if vv, ok := v[nullKey]; !ok { + t.Errorf("Expected key %q to be present but it was removed", nullKey) + } else if vv != nil { + t.Errorf("Expected key %q to be null but it has a value of %v", nullKey, vv) + } + } + + if _, ok := v["nested"].(map[string]interface{})["boat"]; !ok { + t.Error("Expected nested boat key to be present but it was removed") + } + + subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{}) + if _, ok := subchart["boat"]; !ok { + t.Error("Expected subchart boat key to be present but it was removed") + } + + if _, ok := subchart["nested"].(map[string]interface{})["bar"]; !ok { + t.Error("Expected subchart nested bar key to be present but it was removed") + } + + // CoalesceValues should not mutate the passed arguments + is.Equal(valsCopy, vals) +} + func TestCoalesceTables(t *testing.T) { dst := map[string]interface{}{ "name": "Ishmael", @@ -311,3 +494,207 @@ func TestCoalesceTables(t *testing.T) { t.Errorf("Expected hole string, got %v", dst2["boat"]) } } + +func TestMergeTables(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. + MergeTables(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"]) + } + + // This is one test that is different from CoalesceTables. Because country + // is a nil value and it's not removed it's still present. + if _, ok = addr["country"]; !ok { + t.Error("The country is 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"]) + } + + // This is one test that is different from CoalesceTables. Because hole + // is a nil value and it's not removed it's still present. + if _, ok = dst["hole"]; !ok { + t.Error("The hole no longer 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", + "nilval": nil, + } + + // 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 + MergeTables(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"]) + } + + if dst2["nilval"] != nil { + t.Error("Expected nilvalue to have nil value but it does not") + } +} + +func TestCoalesceValuesWarnings(t *testing.T) { + + c := withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "level1"}, + Values: map[string]interface{}{ + "name": "moby", + }, + }, + withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "level2"}, + Values: map[string]interface{}{ + "name": "pequod", + }, + }, + &chart.Chart{ + Metadata: &chart.Metadata{Name: "level3"}, + Values: map[string]interface{}{ + "name": "ahab", + "boat": true, + "spear": map[string]interface{}{ + "tip": true, + "sail": map[string]interface{}{ + "cotton": true, + }, + }, + }, + }, + ), + ) + + vals := map[string]interface{}{ + "level2": map[string]interface{}{ + "level3": map[string]interface{}{ + "boat": map[string]interface{}{"mast": true}, + "spear": map[string]interface{}{ + "tip": map[string]interface{}{ + "sharp": true, + }, + "sail": true, + }, + }, + }, + } + + warnings := make([]string, 0) + printf := func(format string, v ...interface{}) { + t.Logf(format, v...) + warnings = append(warnings, fmt.Sprintf(format, v...)) + } + + _, err := coalesce(printf, c, vals, "", false) + if err != nil { + t.Fatal(err) + } + + t.Logf("vals: %v", vals) + assert.Contains(t, warnings, "warning: skipped value for level1.level2.level3.boat: Not a table.") + assert.Contains(t, warnings, "warning: destination for level1.level2.level3.spear.tip is a table. Ignoring non-table value (true)") + assert.Contains(t, warnings, "warning: cannot overwrite table with non table for level1.level2.level3.spear.sail (map[cotton:true])") + +} + +func TestConcatPrefix(t *testing.T) { + assert.Equal(t, "b", concatPrefix("", "b")) + assert.Equal(t, "a.b", concatPrefix("a", "b")) +} diff --git a/pkg/chartutil/create.go b/pkg/chartutil/create.go index ca79e7ab2..a94f56a8a 100644 --- a/pkg/chartutil/create.go +++ b/pkg/chartutil/create.go @@ -19,7 +19,6 @@ package chartutil import ( "fmt" "io" - "io/ioutil" "os" "path/filepath" "regexp" @@ -122,6 +121,8 @@ fullnameOverride: "" serviceAccount: # Specifies whether a service account should be created create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true # Annotations to add to the service account annotations: {} # The name of the service account to use. @@ -129,6 +130,7 @@ serviceAccount: name: "" podAnnotations: {} +podLabels: {} podSecurityContext: {} # fsGroup: 2000 @@ -180,6 +182,19 @@ autoscaling: targetCPUUtilizationPercentage: 80 # targetMemoryUtilizationPercentage: 80 +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + nodeSelector: {} tolerations: [] @@ -295,7 +310,10 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} labels: - {{- include ".selectorLabels" . | nindent 8 }} + {{- include ".labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: @@ -312,7 +330,7 @@ spec: imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http - containerPort: 80 + containerPort: {{ .Values.service.port }} protocol: TCP livenessProbe: httpGet: @@ -324,6 +342,14 @@ spec: port: http resources: {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -366,11 +392,12 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} {{- end }} ` const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2beta1 +apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: {{ include ".fullname" . }} @@ -388,13 +415,17 @@ spec: - type: Resource resource: name: cpu - targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} {{- end }} {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - type: Resource resource: name: memory - targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} {{- end }} {{- end }} ` @@ -673,7 +704,7 @@ 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) + return os.WriteFile(name, content, 0644) } func validateChartName(name string) error { diff --git a/pkg/chartutil/create_test.go b/pkg/chartutil/create_test.go index 9a473fc59..1697c4218 100644 --- a/pkg/chartutil/create_test.go +++ b/pkg/chartutil/create_test.go @@ -18,7 +18,6 @@ package chartutil import ( "bytes" - "io/ioutil" "os" "path/filepath" "testing" @@ -28,11 +27,7 @@ import ( ) func TestCreate(t *testing.T) { - tdir, err := ioutil.TempDir("", "helm-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tdir) + tdir := t.TempDir() c, err := Create("foo", tdir) if err != nil { @@ -70,11 +65,7 @@ func TestCreate(t *testing.T) { } func TestCreateFrom(t *testing.T) { - tdir, err := ioutil.TempDir("", "helm-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tdir) + tdir := t.TempDir() cf := &chart.Metadata{ APIVersion: chart.APIVersionV1, @@ -108,7 +99,7 @@ func TestCreateFrom(t *testing.T) { } // Check each file to make sure has been replaced - b, err := ioutil.ReadFile(filepath.Join(dir, f)) + b, err := os.ReadFile(filepath.Join(dir, f)) if err != nil { t.Errorf("Unable to read file %s: %s", f, err) } @@ -120,11 +111,7 @@ func TestCreateFrom(t *testing.T) { // 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) + tdir := t.TempDir() var errlog bytes.Buffer @@ -143,7 +130,7 @@ func TestCreate_Overwrite(t *testing.T) { t.Fatal(err) } - data, err := ioutil.ReadFile(tplname) + data, err := os.ReadFile(tplname) if err != nil { t.Fatal(err) } diff --git a/pkg/chartutil/dependencies.go b/pkg/chartutil/dependencies.go index d7adbb912..77064c174 100644 --- a/pkg/chartutil/dependencies.go +++ b/pkg/chartutil/dependencies.go @@ -19,15 +19,29 @@ import ( "log" "strings" + "github.com/mitchellh/copystructure" + "helm.sh/helm/v3/pkg/chart" ) // ProcessDependencies checks through this chart's dependencies, processing accordingly. +// +// TODO: For Helm v4 this can be combined with or turned into ProcessDependenciesWithMerge func ProcessDependencies(c *chart.Chart, v Values) error { if err := processDependencyEnabled(c, v, ""); err != nil { return err } - return processDependencyImportValues(c) + return processDependencyImportValues(c, false) +} + +// ProcessDependenciesWithMerge checks through this chart's dependencies, processing accordingly. +// It is similar to ProcessDependencies but it does not remove nil values during +// the import/export handling process. +func ProcessDependenciesWithMerge(c *chart.Chart, v Values) error { + if err := processDependencyEnabled(c, v, ""); err != nil { + return err + } + return processDependencyImportValues(c, true) } // processDependencyConditions disables charts based on condition path value in values @@ -137,6 +151,9 @@ Loop: } for _, req := range c.Metadata.Dependencies { + if req == nil { + continue + } if chartDependency := getAliasDependency(c.Dependencies(), req); chartDependency != nil { chartDependencies = append(chartDependencies, chartDependency) } @@ -217,12 +234,18 @@ func set(path []string, data map[string]interface{}) map[string]interface{} { } // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. -func processImportValues(c *chart.Chart) error { +func processImportValues(c *chart.Chart, merge bool) error { if c.Metadata.Dependencies == nil { return nil } // combine chart values and empty config to get Values - cvals, err := CoalesceValues(c, nil) + var cvals Values + var err error + if merge { + cvals, err = MergeValues(c, nil) + } else { + cvals, err = CoalesceValues(c, nil) + } if err != nil { return err } @@ -252,7 +275,11 @@ func processImportValues(c *chart.Chart) error { continue } // create value map from child to be merged into parent - b = CoalesceTables(cvals, pathToMap(parent, vv.AsMap())) + if merge { + b = MergeTables(b, pathToMap(parent, vv.AsMap())) + } else { + b = CoalesceTables(b, pathToMap(parent, vv.AsMap())) + } case string: child := "exports." + iv outiv = append(outiv, map[string]string{ @@ -264,26 +291,73 @@ func processImportValues(c *chart.Chart) error { log.Printf("Warning: ImportValues missing table: %v", err) continue } - b = CoalesceTables(b, vm.AsMap()) + if merge { + b = MergeTables(b, vm.AsMap()) + } else { + b = CoalesceTables(b, vm.AsMap()) + } } } - // set our formatted import values r.ImportValues = outiv } - // set the new values - c.Values = CoalesceTables(b, cvals) + // Imported values from a child to a parent chart have a lower priority than + // the parents values. This enables parent charts to import a large section + // from a child and then override select parts. This is why b is merged into + // cvals in the code below and not the other way around. + if merge { + // deep copying the cvals as there are cases where pointers can end + // up in the cvals when they are copied onto b in ways that break things. + cvals = deepCopyMap(cvals) + c.Values = MergeTables(cvals, b) + } else { + // Trimming the nil values from cvals is needed for backwards compatibility. + // Previously, the b value had been populated with cvals along with some + // overrides. This caused the coalescing functionality to remove the + // nil/null values. This trimming is for backwards compat. + cvals = trimNilValues(cvals) + c.Values = CoalesceTables(cvals, b) + } return nil } +func deepCopyMap(vals map[string]interface{}) map[string]interface{} { + valsCopy, err := copystructure.Copy(vals) + if err != nil { + return vals + } + return valsCopy.(map[string]interface{}) +} + +func trimNilValues(vals map[string]interface{}) map[string]interface{} { + valsCopy, err := copystructure.Copy(vals) + if err != nil { + return vals + } + valsCopyMap := valsCopy.(map[string]interface{}) + for key, val := range valsCopyMap { + if val == nil { + log.Printf("trim deleting %q", key) + // Iterate over the values and remove nil keys + delete(valsCopyMap, key) + } else if istable(val) { + log.Printf("trim copying %q", key) + // Recursively call into ourselves to remove keys from inner tables + valsCopyMap[key] = trimNilValues(val.(map[string]interface{})) + } + } + + return valsCopyMap +} + // processDependencyImportValues imports specified chart values from child to parent. -func processDependencyImportValues(c *chart.Chart) error { +func processDependencyImportValues(c *chart.Chart, merge bool) error { for _, d := range c.Dependencies() { // recurse - if err := processDependencyImportValues(d); err != nil { + if err := processDependencyImportValues(d, merge); err != nil { return err } } - return processImportValues(c) + return processImportValues(c, merge) } diff --git a/pkg/chartutil/dependencies_test.go b/pkg/chartutil/dependencies_test.go index 9778c0fb0..0a37cf7ca 100644 --- a/pkg/chartutil/dependencies_test.go +++ b/pkg/chartutil/dependencies_test.go @@ -181,6 +181,9 @@ func TestProcessDependencyImportValues(t *testing.T) { e["imported-chartA-B.SPextra5"] = "k8s" e["imported-chartA-B.SC1extra5"] = "tiller" + // These values are imported from the child chart to the parent. Parent + // values take precedence over imported values. This enables importing a + // large section from a child chart and overriding a selection from it. e["overridden-chart1.SC1bool"] = "false" e["overridden-chart1.SC1float"] = "3.141592" e["overridden-chart1.SC1int"] = "99" @@ -193,6 +196,9 @@ func TestProcessDependencyImportValues(t *testing.T) { e["overridden-chartA.SCAstring"] = "jabberwocky" e["overridden-chartA.SPextra4"] = "true" + // These values are imported from the child chart to the parent. Parent + // values take precedence over imported values. This enables importing a + // large section from a child chart and overriding a selection from it. e["overridden-chartA-B.SCAbool"] = "true" e["overridden-chartA-B.SCAfloat"] = "41.3" e["overridden-chartA-B.SCAint"] = "808" @@ -223,7 +229,7 @@ func TestProcessDependencyImportValues(t *testing.T) { e["SCBexported2A"] = "blaster" e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" - if err := processDependencyImportValues(c); err != nil { + if err := processDependencyImportValues(c, false); err != nil { t.Fatalf("processing import values dependencies %v", err) } cc := Values(c.Values) @@ -236,11 +242,83 @@ func TestProcessDependencyImportValues(t *testing.T) { 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) + t.Errorf("failed to match imported float value %v with expected %v for key %q", s, vv, kk) } case bool: if b := strconv.FormatBool(pv); b != vv { - t.Errorf("failed to match imported bool value %v with expected %v", b, vv) + t.Errorf("failed to match imported bool value %v with expected %v for key %q", b, vv, kk) + } + default: + if pv != vv { + t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk) + } + } + } + + // Since this was processed with coalescing there should be no null values. + // Here we verify that. + _, err := cc.PathValue("ensurenull") + if err == nil { + t.Error("expect nil value not found but found it") + } + switch xerr := err.(type) { + case ErrNoValue: + // We found what we expected + default: + t.Errorf("expected an ErrNoValue but got %q instead", xerr) + } + + c = loadChart(t, "testdata/subpop") + if err := processDependencyImportValues(c, true); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + cc = Values(c.Values) + val, err := cc.PathValue("ensurenull") + if err != nil { + t.Error("expect value but ensurenull was not found") + } + if val != nil { + t.Errorf("expect nil value but got %q instead", val) + } +} + +func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { + c := loadChart(t, "testdata/three-level-dependent-chart/umbrella") + + e := make(map[string]string) + + // The order of precedence should be: + // 1. User specified values (e.g CLI) + // 2. Parent chart values + // 3. Imported values + // 4. Sub-chart values + // The 4 app charts here deal with things differently: + // - app1 has a port value set in the umbrella chart. It does not import any + // values so the value from the umbrella chart should be used. + // - app2 has a value in the app chart and imports from the library. The + // app chart value should take precedence. + // - app3 has no value in the app chart and imports the value from the library + // chart. The library chart value should be used. + // - app4 has a value in the app chart and does not import the value from the + // library chart. The app charts value should be used. + e["app1.service.port"] = "3456" + e["app2.service.port"] = "8080" + e["app3.service.port"] = "9090" + e["app4.service.port"] = "1234" + if err := processDependencyImportValues(c, true); 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) } default: if pv != vv { @@ -254,7 +332,7 @@ 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 { + if err := processDependencyImportValues(c, true); err != nil { t.Fatalf("processing import values dependencies %v", err) } diff --git a/pkg/chartutil/doc.go b/pkg/chartutil/doc.go index 8f06bcc9a..49c55ac52 100644 --- a/pkg/chartutil/doc.go +++ b/pkg/chartutil/doc.go @@ -14,16 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package chartutil contains tools for working with charts. +/* +Package chartutil contains tools for working with 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: - - As a directory that contains a Chart.yaml file and other chart things. - - As a tarred gzipped file containing a directory that then contains a - Chart.yaml file. + - As a directory that contains a Chart.yaml file and other chart things. + - As a tarred gzipped file containing a directory that then contains a + Chart.yaml file. This package provides utilities for working with those file formats. diff --git a/pkg/chartutil/expand.go b/pkg/chartutil/expand.go index 6ad09e417..7ae1ae6fa 100644 --- a/pkg/chartutil/expand.go +++ b/pkg/chartutil/expand.go @@ -18,7 +18,6 @@ package chartutil import ( "io" - "io/ioutil" "os" "path/filepath" @@ -72,7 +71,7 @@ func Expand(dir string, r io.Reader) error { return err } - if err := ioutil.WriteFile(outpath, file.Data, 0644); err != nil { + if err := os.WriteFile(outpath, file.Data, 0644); err != nil { return err } } diff --git a/pkg/chartutil/expand_test.go b/pkg/chartutil/expand_test.go index 9a85e3247..f31a3d290 100644 --- a/pkg/chartutil/expand_test.go +++ b/pkg/chartutil/expand_test.go @@ -17,18 +17,13 @@ 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) + dest := t.TempDir() reader, err := os.Open("testdata/frobnitz-1.2.3.tgz") if err != nil { @@ -81,11 +76,7 @@ func TestExpand(t *testing.T) { } func TestExpandFile(t *testing.T) { - dest, err := ioutil.TempDir("", "helm-testing-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dest) + dest := t.TempDir() if err := ExpandFile(dest, "testdata/frobnitz-1.2.3.tgz"); err != nil { t.Fatal(err) diff --git a/pkg/chartutil/jsonschema.go b/pkg/chartutil/jsonschema.go index 753dc98c1..7b9768fd3 100644 --- a/pkg/chartutil/jsonschema.go +++ b/pkg/chartutil/jsonschema.go @@ -55,7 +55,13 @@ func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) err } // ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema -func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) error { +func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) { + defer func() { + if r := recover(); r != nil { + reterr = fmt.Errorf("unable to validate schema: %s", r) + } + }() + valuesData, err := yaml.Marshal(values) if err != nil { return err diff --git a/pkg/chartutil/jsonschema_test.go b/pkg/chartutil/jsonschema_test.go index a0acd5a7f..7610db337 100644 --- a/pkg/chartutil/jsonschema_test.go +++ b/pkg/chartutil/jsonschema_test.go @@ -17,7 +17,7 @@ limitations under the License. package chartutil import ( - "io/ioutil" + "os" "testing" "helm.sh/helm/v3/pkg/chart" @@ -28,7 +28,7 @@ func TestValidateAgainstSingleSchema(t *testing.T) { if err != nil { t.Fatalf("Error reading YAML file: %s", err) } - schema, err := ioutil.ReadFile("./testdata/test-values.schema.json") + schema, err := os.ReadFile("./testdata/test-values.schema.json") if err != nil { t.Fatalf("Error reading YAML file: %s", err) } @@ -38,12 +38,36 @@ func TestValidateAgainstSingleSchema(t *testing.T) { } } +func TestValidateAgainstInvalidSingleSchema(t *testing.T) { + values, err := ReadValuesFile("./testdata/test-values.yaml") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + schema, err := os.ReadFile("./testdata/test-values-invalid.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 := "unable to validate schema: runtime error: invalid " + + "memory address or nil pointer dereference" + if errString != expectedErrString { + t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) + } +} + 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") + schema, err := os.ReadFile("./testdata/test-values.schema.json") if err != nil { t.Fatalf("Error reading YAML file: %s", err) } diff --git a/pkg/chartutil/save_test.go b/pkg/chartutil/save_test.go index b4951535a..db485c7cb 100644 --- a/pkg/chartutil/save_test.go +++ b/pkg/chartutil/save_test.go @@ -21,7 +21,6 @@ import ( "bytes" "compress/gzip" "io" - "io/ioutil" "os" "path" "path/filepath" @@ -30,16 +29,14 @@ import ( "testing" "time" - "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) + tmp := t.TempDir() - for _, dest := range []string{tmp, path.Join(tmp, "newdir")} { + for _, dest := range []string{tmp, filepath.Join(tmp, "newdir")} { t.Run("outDir="+dest, func(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{ @@ -129,11 +126,7 @@ func TestSavePreservesTimestamps(t *testing.T) { // written timestamp for the files. initialCreateTime := time.Now().Add(-1 * time.Second) - tmp, err := ioutil.TempDir("", "helm-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmp) + tmp := t.TempDir() c := &chart.Chart{ Metadata: &chart.Metadata{ @@ -203,11 +196,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) { } func TestSaveDir(t *testing.T) { - tmp, err := ioutil.TempDir("", "helm-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmp) + tmp := t.TempDir() c := &chart.Chart{ Metadata: &chart.Metadata{ @@ -219,7 +208,7 @@ func TestSaveDir(t *testing.T) { {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, Templates: []*chart.File{ - {Name: filepath.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, + {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, }, } @@ -236,11 +225,11 @@ func TestSaveDir(t *testing.T) { t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name()) } - if len(c2.Templates) != 1 || c2.Templates[0].Name != filepath.Join(TemplatesDir, "nested", "dir", "thing.yaml") { + if len(c2.Templates) != 1 || c2.Templates[0].Name != c.Templates[0].Name { t.Fatal("Templates data did not match") } - if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" { + if len(c2.Files) != 1 || c2.Files[0].Name != c.Files[0].Name { t.Fatal("Files data did not match") } } diff --git a/pkg/chartutil/testdata/subpop/values.yaml b/pkg/chartutil/testdata/subpop/values.yaml index d611d6a89..ba70ed406 100644 --- a/pkg/chartutil/testdata/subpop/values.yaml +++ b/pkg/chartutil/testdata/subpop/values.yaml @@ -41,3 +41,5 @@ tags: subchart2alias: enabled: false + +ensurenull: null diff --git a/pkg/chartutil/testdata/test-values-invalid.schema.json b/pkg/chartutil/testdata/test-values-invalid.schema.json new file mode 100644 index 000000000..35a16a2c4 --- /dev/null +++ b/pkg/chartutil/testdata/test-values-invalid.schema.json @@ -0,0 +1 @@ + 1E1111111 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/README.md b/pkg/chartutil/testdata/three-level-dependent-chart/README.md new file mode 100644 index 000000000..536bb9792 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/README.md @@ -0,0 +1,16 @@ +# Three Level Dependent Chart + +This chart is for testing the processing of multi-level dependencies. + +Consists of the following charts: + +- Library Chart +- App Chart (Uses Library Chart as dependency, 2x: app1/app2) +- Umbrella Chart (Has all the app charts as dependencies) + +The precedence is as follows: `library < app < umbrella` + +Catches two use-cases: + +- app overwriting library (app2) +- umbrella overwriting app and library (app1) diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/Chart.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/Chart.yaml new file mode 100644 index 000000000..e5dbe3131 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v2 +name: umbrella +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 + +dependencies: +- name: app1 + version: 0.1.0 + condition: app1.enabled +- name: app2 + version: 0.1.0 + condition: app2.enabled +- name: app3 + version: 0.1.0 + condition: app3.enabled +- name: app4 + version: 0.1.0 + condition: app4.enabled diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml new file mode 100644 index 000000000..388245e31 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: app1 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 + +dependencies: +- name: library + version: 0.1.0 + import-values: + - defaults diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml new file mode 100644 index 000000000..f2f8a90d9 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: library +description: A Helm chart for Kubernetes +type: library +version: 0.1.0 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml new file mode 100644 index 000000000..3fd398b53 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml new file mode 100644 index 000000000..0c08b6cd2 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml @@ -0,0 +1,5 @@ +exports: + defaults: + service: + type: ClusterIP + port: 9090 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml new file mode 100644 index 000000000..8ed8ddf1f --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml new file mode 100644 index 000000000..3728aa930 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml @@ -0,0 +1,3 @@ +service: + type: ClusterIP + port: 1234 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml new file mode 100644 index 000000000..fea2768c7 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: app2 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 + +dependencies: +- name: library + version: 0.1.0 + import-values: + - defaults diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml new file mode 100644 index 000000000..f2f8a90d9 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: library +description: A Helm chart for Kubernetes +type: library +version: 0.1.0 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml new file mode 100644 index 000000000..3fd398b53 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml new file mode 100644 index 000000000..0c08b6cd2 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml @@ -0,0 +1,5 @@ +exports: + defaults: + service: + type: ClusterIP + port: 9090 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml new file mode 100644 index 000000000..8ed8ddf1f --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml new file mode 100644 index 000000000..98bd6d24b --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml @@ -0,0 +1,3 @@ +service: + type: ClusterIP + port: 8080 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml new file mode 100644 index 000000000..a42f58773 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: app3 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 + +dependencies: +- name: library + version: 0.1.0 + import-values: + - defaults diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml new file mode 100644 index 000000000..f2f8a90d9 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: library +description: A Helm chart for Kubernetes +type: library +version: 0.1.0 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml new file mode 100644 index 000000000..3fd398b53 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml new file mode 100644 index 000000000..0c08b6cd2 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml @@ -0,0 +1,5 @@ +exports: + defaults: + service: + type: ClusterIP + port: 9090 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml new file mode 100644 index 000000000..8ed8ddf1f --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml new file mode 100644 index 000000000..b738e2a57 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml @@ -0,0 +1,2 @@ +service: + type: ClusterIP diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml new file mode 100644 index 000000000..574bfdfd0 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: app4 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 + +dependencies: +- name: library + version: 0.1.0 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml new file mode 100644 index 000000000..f2f8a90d9 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: library +description: A Helm chart for Kubernetes +type: library +version: 0.1.0 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml new file mode 100644 index 000000000..3fd398b53 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml new file mode 100644 index 000000000..0c08b6cd2 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml @@ -0,0 +1,5 @@ +exports: + defaults: + service: + type: ClusterIP + port: 9090 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml new file mode 100644 index 000000000..8ed8ddf1f --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml new file mode 100644 index 000000000..3728aa930 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml @@ -0,0 +1,3 @@ +service: + type: ClusterIP + port: 1234 diff --git a/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/values.yaml b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/values.yaml new file mode 100644 index 000000000..de0bafa51 --- /dev/null +++ b/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/values.yaml @@ -0,0 +1,14 @@ +app1: + enabled: true + service: + type: ClusterIP + port: 3456 + +app2: + enabled: true + +app3: + enabled: true + +app4: + enabled: true diff --git a/pkg/chartutil/validate_name.go b/pkg/chartutil/validate_name.go index d253731ec..05c090cb6 100644 --- a/pkg/chartutil/validate_name.go +++ b/pkg/chartutil/validate_name.go @@ -40,19 +40,24 @@ var ( errMissingName = errors.New("no name provided") // errInvalidName indicates that an invalid release name was provided - errInvalidName = errors.New(fmt.Sprintf( + errInvalidName = fmt.Errorf( "invalid release name, must match regex %s and the length must not be longer than 53", - validName.String())) + validName.String()) // errInvalidKubernetesName indicates that the name does not meet the Kubernetes // restrictions on metadata names. - errInvalidKubernetesName = errors.New(fmt.Sprintf( + errInvalidKubernetesName = fmt.Errorf( "invalid metadata name, must match regex %s and the length must not be longer than 253", - validName.String())) + validName.String()) ) const ( - // maxNameLen is the maximum length Helm allows for a release name + // According to the Kubernetes docs (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names) + // some resource names have a max length of 63 characters while others have a max + // length of 253 characters. As we cannot be sure the resources used in a chart, we + // therefore need to limit it to 63 chars and reserve 10 chars for additional part to name + // of the resource. The reason is that chart maintainers can use release name as part of + // the resource name (and some additional chars). maxReleaseNameLen = 53 // maxMetadataNameLen is the maximum length Kubernetes allows for any name. maxMetadataNameLen = 253 diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index e1cdf4642..2fa2bdabb 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -19,7 +19,7 @@ package chartutil import ( "fmt" "io" - "io/ioutil" + "os" "strings" "github.com/pkg/errors" @@ -68,7 +68,7 @@ func (v Values) Table(name string) (Values, error) { // // It protects against nil map panics. func (v Values) AsMap() map[string]interface{} { - if v == nil || len(v) == 0 { + if len(v) == 0 { return map[string]interface{}{} } return v @@ -114,7 +114,7 @@ func ReadValues(data []byte) (vals Values, err error) { // ReadValuesFile will parse a YAML file into a map of values. func ReadValuesFile(filename string) (Values, error) { - data, err := ioutil.ReadFile(filename) + data, err := os.ReadFile(filename) if err != nil { return map[string]interface{}{}, err } diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index ee60d981f..dac2a4bc1 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package cli describes the operating environment for the Helm CLI. +/* +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 @@ -24,19 +25,25 @@ package cli import ( "fmt" + "net/http" "os" "strconv" "strings" "github.com/spf13/pflag" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/rest" + "helm.sh/helm/v3/internal/version" "helm.sh/helm/v3/pkg/helmpath" ) // defaultMaxHistory sets the maximum number of releases to 0: unlimited const defaultMaxHistory = 10 +// defaultBurstLimit sets the default client-side throttling limit +const defaultBurstLimit = 100 + // EnvSettings describes all of the environment settings. type EnvSettings struct { namespace string @@ -56,6 +63,12 @@ type EnvSettings struct { KubeAPIServer string // Custom certificate authority file. KubeCaFile string + // KubeInsecureSkipTLSVerify indicates if server's certificate will not be checked for validity. + // This makes the HTTPS connections insecure + KubeInsecureSkipTLSVerify bool + // KubeTLSServerName overrides the name to use for server certificate validation. + // If it is not provided, the hostname used to contact the server is used + KubeTLSServerName string // Debug indicates whether or not Helm is running in Debug mode. Debug bool // RegistryConfig is the path to the registry config file. @@ -68,22 +81,27 @@ type EnvSettings struct { PluginsDirectory string // MaxHistory is the max release history maintained. MaxHistory int + // BurstLimit is the default client-side throttling limit. + BurstLimit 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")), + 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"), + KubeTLSServerName: os.Getenv("HELM_KUBETLS_SERVER_NAME"), + KubeInsecureSkipTLSVerify: envBoolOr("HELM_KUBEINSECURE_SKIP_TLS_VERIFY", false), + PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")), + RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry/config.json")), + RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), + RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), + BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit), } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) @@ -96,7 +114,17 @@ func New() *EnvSettings { CAFile: &env.KubeCaFile, KubeConfig: &env.KubeConfig, Impersonate: &env.KubeAsUser, + Insecure: &env.KubeInsecureSkipTLSVerify, + TLSServerName: &env.KubeTLSServerName, ImpersonateGroup: &env.KubeAsGroups, + WrapConfigFn: func(config *rest.Config) *rest.Config { + config.Burst = env.BurstLimit + config.Wrap(func(rt http.RoundTripper) http.RoundTripper { + return &retryingRoundTripper{wrapped: rt} + }) + config.UserAgent = version.GetUserAgent() + return config + }, } return env } @@ -111,10 +139,13 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { 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.StringVar(&s.KubeTLSServerName, "kube-tls-server-name", s.KubeTLSServerName, "server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used") + fs.BoolVar(&s.KubeInsecureSkipTLSVerify, "kube-insecure-skip-tls-verify", s.KubeInsecureSkipTLSVerify, "if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure") 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") + fs.IntVar(&s.BurstLimit, "burst-limit", s.BurstLimit, "client-side default throttling limit") } func envOr(name, def string) string { @@ -124,6 +155,18 @@ func envOr(name, def string) string { return def } +func envBoolOr(name string, def bool) bool { + if name == "" { + return def + } + envVal := envOr(name, strconv.FormatBool(def)) + ret, err := strconv.ParseBool(envVal) + if err != nil { + return def + } + return ret +} + func envIntOr(name string, def int) int { if name == "" { return def @@ -157,14 +200,17 @@ func (s *EnvSettings) EnvVars() map[string]string { "HELM_REPOSITORY_CONFIG": s.RepositoryConfig, "HELM_NAMESPACE": s.Namespace(), "HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory), + "HELM_BURST_LIMIT": strconv.Itoa(s.BurstLimit), // 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, + "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, + "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": strconv.FormatBool(s.KubeInsecureSkipTLSVerify), + "HELM_KUBETLS_SERVER_NAME": s.KubeTLSServerName, } if s.KubeConfig != "" { envvars["KUBECONFIG"] = s.KubeConfig @@ -180,6 +226,11 @@ func (s *EnvSettings) Namespace() string { return "default" } +// SetNamespace sets the namespace in the configuration +func (s *EnvSettings) SetNamespace(namespace string) { + s.namespace = namespace +} + // 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 index 31ba7a237..3de6fab4c 100644 --- a/pkg/cli/environment_test.go +++ b/pkg/cli/environment_test.go @@ -23,8 +23,24 @@ import ( "testing" "github.com/spf13/pflag" + + "helm.sh/helm/v3/internal/version" ) +func TestSetNamespace(t *testing.T) { + settings := New() + + if settings.namespace != "" { + t.Errorf("Expected empty namespace, got %s", settings.namespace) + } + + settings.SetNamespace("testns") + if settings.namespace != "testns" { + t.Errorf("Expected namespace testns, got %s", settings.namespace) + } + +} + func TestEnvSettings(t *testing.T) { tests := []struct { name string @@ -34,48 +50,61 @@ func TestEnvSettings(t *testing.T) { envvars map[string]string // expected values - ns, kcontext string - debug bool - maxhistory int - kAsUser string - kAsGroups []string - kCaFile string + ns, kcontext string + debug bool + maxhistory int + kubeAsUser string + kubeAsGroups []string + kubeCaFile string + kubeInsecure bool + kubeTLSServer string + burstLimit int }{ { name: "defaults", ns: "default", maxhistory: defaultMaxHistory, + burstLimit: defaultBurstLimit, }, { - 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 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 --burst-limit 100 --kube-insecure-skip-tls-verify=true --kube-tls-server-name=example.org", + ns: "myns", + debug: true, + maxhistory: defaultMaxHistory, + burstLimit: 100, + kubeAsUser: "poro", + kubeAsGroups: []string{"admins", "teatime", "snackeaters"}, + kubeCaFile: "/tmp/ca.crt", + kubeTLSServer: "example.org", + kubeInsecure: true, }, { - 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 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", "HELM_BURST_LIMIT": "150", "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": "true", "HELM_KUBETLS_SERVER_NAME": "example.org"}, + ns: "yourns", + maxhistory: 5, + burstLimit: 150, + debug: true, + kubeAsUser: "pikachu", + kubeAsGroups: []string{"operators", "snackeaters", "partyanimals"}, + kubeCaFile: "/tmp/ca.crt", + kubeTLSServer: "example.org", + kubeInsecure: true, }, { - 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", + 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 --burst-limit 175 --kube-insecure-skip-tls-verify=true --kube-tls-server-name=example.org", + 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", "HELM_BURST_LIMIT": "200", "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": "true", "HELM_KUBETLS_SERVER_NAME": "example.org"}, + ns: "myns", + debug: true, + maxhistory: 5, + burstLimit: 175, + kubeAsUser: "poro", + kubeAsGroups: []string{"admins", "teatime", "snackeaters"}, + kubeCaFile: "/my/ca.crt", + kubeTLSServer: "example.org", + kubeInsecure: true, }, } @@ -105,19 +134,120 @@ func TestEnvSettings(t *testing.T) { 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 tt.kubeAsUser != settings.KubeAsUser { + t.Errorf("expected kAsUser %q, got %q", tt.kubeAsUser, settings.KubeAsUser) + } + if !reflect.DeepEqual(tt.kubeAsGroups, settings.KubeAsGroups) { + t.Errorf("expected kAsGroups %+v, got %+v", len(tt.kubeAsGroups), len(settings.KubeAsGroups)) + } + if tt.kubeCaFile != settings.KubeCaFile { + t.Errorf("expected kCaFile %q, got %q", tt.kubeCaFile, settings.KubeCaFile) + } + if tt.burstLimit != settings.BurstLimit { + t.Errorf("expected BurstLimit %d, got %d", tt.burstLimit, settings.BurstLimit) } - if !reflect.DeepEqual(tt.kAsGroups, settings.KubeAsGroups) { - t.Errorf("expected kAsGroups %+v, got %+v", len(tt.kAsGroups), len(settings.KubeAsGroups)) + if tt.kubeInsecure != settings.KubeInsecureSkipTLSVerify { + t.Errorf("expected kubeInsecure %t, got %t", tt.kubeInsecure, settings.KubeInsecureSkipTLSVerify) + } + if tt.kubeTLSServer != settings.KubeTLSServerName { + t.Errorf("expected kubeTLSServer %q, got %q", tt.kubeTLSServer, settings.KubeTLSServerName) + } + }) + } +} + +func TestEnvOrBool(t *testing.T) { + const envName = "TEST_ENV_OR_BOOL" + tests := []struct { + name string + env string + val string + def bool + expected bool + }{ + { + name: "unset with default false", + def: false, + expected: false, + }, + { + name: "unset with default true", + def: true, + expected: true, + }, + { + name: "blank env with default false", + env: envName, + def: false, + expected: false, + }, + { + name: "blank env with default true", + env: envName, + def: true, + expected: true, + }, + { + name: "env true with default false", + env: envName, + val: "true", + def: false, + expected: true, + }, + { + name: "env false with default true", + env: envName, + val: "false", + def: true, + expected: false, + }, + { + name: "env fails parsing with default true", + env: envName, + val: "NOT_A_BOOL", + def: true, + expected: true, + }, + { + name: "env fails parsing with default false", + env: envName, + val: "NOT_A_BOOL", + def: false, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.env != "" { + t.Cleanup(func() { + os.Unsetenv(tt.env) + }) + os.Setenv(tt.env, tt.val) } - if tt.kCaFile != settings.KubeCaFile { - t.Errorf("expected kCaFile %q, got %q", tt.kCaFile, settings.KubeCaFile) + actual := envBoolOr(tt.env, tt.def) + if actual != tt.expected { + t.Errorf("expected result %t, got %t", tt.expected, actual) } }) } } +func TestUserAgentHeaderInK8sRESTClientConfig(t *testing.T) { + defer resetEnv()() + + settings := New() + restConfig, err := settings.RESTClientGetter().ToRESTConfig() + if err != nil { + t.Fatal(err) + } + + expectedUserAgent := version.GetUserAgent() + if restConfig.UserAgent != expectedUserAgent { + t.Errorf("expected User-Agent header %q in K8s REST client config, got %q", expectedUserAgent, restConfig.UserAgent) + } +} + func resetEnv() func() { origEnv := os.Environ() diff --git a/pkg/cli/roundtripper.go b/pkg/cli/roundtripper.go new file mode 100644 index 000000000..9cd4eacba --- /dev/null +++ b/pkg/cli/roundtripper.go @@ -0,0 +1,80 @@ +/* +Copyright The 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 ( + "bytes" + "encoding/json" + "io" + "net/http" + "strings" +) + +type retryingRoundTripper struct { + wrapped http.RoundTripper +} + +func (rt *retryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return rt.roundTrip(req, 1, nil) +} + +func (rt *retryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) { + if retry < 0 { + return prevResp, nil + } + resp, rtErr := rt.wrapped.RoundTrip(req) + if rtErr != nil { + return resp, rtErr + } + if resp.StatusCode < 500 { + return resp, rtErr + } + if resp.Header.Get("content-type") != "application/json" { + return resp, rtErr + } + b, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return resp, rtErr + } + + var ke kubernetesError + r := bytes.NewReader(b) + err = json.NewDecoder(r).Decode(&ke) + r.Seek(0, io.SeekStart) + resp.Body = io.NopCloser(r) + if err != nil { + return resp, rtErr + } + if ke.Code < 500 { + return resp, rtErr + } + // Matches messages like "etcdserver: leader changed" + if strings.HasSuffix(ke.Message, "etcdserver: leader changed") { + return rt.roundTrip(req, retry-1, resp) + } + // Matches messages like "rpc error: code = Unknown desc = raft proposal dropped" + if strings.HasSuffix(ke.Message, "raft proposal dropped") { + return rt.roundTrip(req, retry-1, resp) + } + return resp, rtErr +} + +type kubernetesError struct { + Message string `json:"message"` + Code int `json:"code"` +} diff --git a/pkg/cli/values/options.go b/pkg/cli/values/options.go index e6ad71767..06631cd33 100644 --- a/pkg/cli/values/options.go +++ b/pkg/cli/values/options.go @@ -17,7 +17,7 @@ limitations under the License. package values import ( - "io/ioutil" + "io" "net/url" "os" "strings" @@ -29,15 +29,18 @@ import ( "helm.sh/helm/v3/pkg/strvals" ) +// Options captures the different ways to specify values type Options struct { - ValueFiles []string - StringValues []string - Values []string - FileValues []string + ValueFiles []string // -f/--values + StringValues []string // --set-string + Values []string // --set + FileValues []string // --set-file + JSONValues []string // --set-json + LiteralValues []string // --set-literal } // MergeValues merges values from files specified via -f/--values and directly -// via --set, --set-string, or --set-file, marshaling them to YAML +// via --set-json, --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{}{} @@ -57,6 +60,13 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er base = mergeMaps(base, currentMap) } + // User specified a value via --set-json + for _, value := range opts.JSONValues { + if err := strvals.ParseJSON(value, base); err != nil { + return nil, errors.Errorf("failed parsing --set-json data %s", value) + } + } + // User specified a value via --set for _, value := range opts.Values { if err := strvals.ParseInto(value, base); err != nil { @@ -75,6 +85,9 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er for _, value := range opts.FileValues { reader := func(rs []rune) (interface{}, error) { bytes, err := readFile(string(rs), p) + if err != nil { + return nil, err + } return string(bytes), err } if err := strvals.ParseIntoFile(value, base, reader); err != nil { @@ -82,6 +95,13 @@ func (opts *Options) MergeValues(p getter.Providers) (map[string]interface{}, er } } + // User specified a value via --set-literal + for _, value := range opts.LiteralValues { + if err := strvals.ParseLiteralInto(value, base); err != nil { + return nil, errors.Wrap(err, "failed parsing --set-literal data") + } + } + return base, nil } @@ -107,15 +127,21 @@ func mergeMaps(a, b map[string]interface{}) map[string]interface{} { // 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) + return io.ReadAll(os.Stdin) + } + u, err := url.Parse(filePath) + if err != nil { + return nil, err } - 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) + return os.ReadFile(filePath) } data, err := g.Get(filePath, getter.WithURL(filePath)) + if err != nil { + return nil, err + } return data.Bytes(), err } diff --git a/pkg/cli/values/options_test.go b/pkg/cli/values/options_test.go index d988274bf..54124c0fa 100644 --- a/pkg/cli/values/options_test.go +++ b/pkg/cli/values/options_test.go @@ -19,6 +19,8 @@ package values import ( "reflect" "testing" + + "helm.sh/helm/v3/pkg/getter" ) func TestMergeValues(t *testing.T) { @@ -75,3 +77,12 @@ func TestMergeValues(t *testing.T) { t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) } } + +func TestReadFile(t *testing.T) { + var p getter.Providers + filePath := "%a.txt" + _, err := readFile(filePath, p) + if err == nil { + t.Errorf("Expected error when has special strings") + } +} diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 2c0d55a55..a95894e00 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -23,14 +23,15 @@ import ( "path/filepath" "strings" + "github.com/Masterminds/semver/v3" "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/registry" "helm.sh/helm/v3/pkg/repo" ) @@ -102,8 +103,9 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven } name := filepath.Base(u.Path) - if u.Scheme == "oci" { - name = fmt.Sprintf("%s-%s.tgz", name, version) + if u.Scheme == registry.OCIScheme { + idx := strings.LastIndexByte(name, ':') + name = fmt.Sprintf("%s-%s.tgz", name[:idx], name[idx+1:]) } destfile := filepath.Join(dest, name) @@ -139,26 +141,63 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven return destfile, ver, nil } +func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL, error) { + var tag string + var err error + + // Evaluate whether an explicit version has been provided. Otherwise, determine version to use + _, errSemVer := semver.NewVersion(version) + if errSemVer == nil { + tag = version + } else { + // Retrieve list of repository tags + tags, err := c.RegistryClient.Tags(strings.TrimPrefix(ref, fmt.Sprintf("%s://", registry.OCIScheme))) + if err != nil { + return nil, err + } + if len(tags) == 0 { + return nil, errors.Errorf("Unable to locate any tags in provided repository: %s", ref) + } + + // Determine if version provided + // If empty, try to get the highest available tag + // If exact version, try to find it + // If semver constraint string, try to find a match + tag, err = registry.GetTagMatchingVersionOrConstraint(tags, version) + if err != nil { + return nil, err + } + } + + u.Path = fmt.Sprintf("%s:%s", u.Path, tag) + + return u, err +} + // ResolveChartVersion resolves a chart reference to a 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. +// A reference may be an HTTP URL, an oci reference URL, a 'reponame/chartname' +// reference, or a local path. // // A version is a SemVer string (1.2.3-beta.1+f334a6789). // -// - For fully qualified URLs, the version will be ignored (since URLs aren't versioned) -// - For a chart reference -// * 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 +// - For fully qualified URLs, the version will be ignored (since URLs aren't versioned) +// - For a chart reference +// - 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, error) { u, err := url.Parse(ref) if err != nil { return nil, errors.Errorf("invalid chart URL format: %s", ref) } - c.Options = append(c.Options, getter.WithURL(ref)) + + if registry.IsOCI(u.String()) { + return c.getOciURI(ref, version, u) + } rf, err := loadRepoConfig(c.RepositoryConfig) if err != nil { @@ -177,6 +216,8 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er // If there is no special config, return the default HTTP client and // swallow the error. if err == ErrNoOwnerRepo { + // Make sure to add the ref URL as the URL for the getter + c.Options = append(c.Options, getter.WithURL(ref)) return u, nil } return u, err @@ -215,6 +256,10 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er return u, err } + // Now that we have the chart repository information we can use that URL + // to set the URL for the getter. + c.Options = append(c.Options, getter.WithURL(rc.URL)) + r, err := repo.NewChartRepository(rc, c.Getters) if err != nil { return u, err @@ -249,31 +294,13 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er } // TODO: Seems that picking first URL is not fully correct - u, err = url.Parse(cv.URLs[0]) + resolvedURL, err := repo.ResolveReferenceURL(rc.URL, cv.URLs[0]) + if err != nil { 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, 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() - // TODO add user-agent - if _, err := getter.NewHTTPGetter(getter.WithURL(rc.URL)); err != nil { - return repoURL, err - } - return u, err - } - - // TODO add user-agent - return u, nil + return url.Parse(resolvedURL) } // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index 38a06671c..131e21306 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -16,7 +16,6 @@ limitations under the License. package downloader import ( - "net/http" "os" "path/filepath" "testing" @@ -49,6 +48,7 @@ func TestResolveChartRef(t *testing.T) { {name: "reference, testing-relative repo", ref: "testing-relative/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"}, {name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/foo", expect: "http://example.com/helm/charts/foo-1.2.3.tgz"}, {name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"}, + {name: "encoded URL", ref: "encoded-url/foobar", expect: "http://example.com/with%2Fslash/charts/foobar-4.2.1.tgz"}, {name: "full URL, HTTPS, irrelevant version", ref: "https://example.com/foo-1.2.3.tgz", version: "0.1.0", expect: "https://example.com/foo-1.2.3.tgz", fail: true}, {name: "full URL, file", ref: "file:///foo-1.2.3.tgz", fail: true}, {name: "invalid", ref: "invalid-1.2.3", fail: true}, @@ -171,19 +171,7 @@ func TestIsTar(t *testing.T) { } func TestDownloadTo(t *testing.T) { - // Set up a fake repo with basic auth enabled - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") - srv.Stop() - if err != nil { - t.Fatal(err) - } - 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) - } - })) - srv.Start() + srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/*.tgz*") defer srv.Stop() if err := srv.CreateIndex(); err != nil { t.Fatal(err) @@ -281,10 +269,9 @@ func TestDownloadTo_TLS(t *testing.T) { } func TestDownloadTo_VerifyLater(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) - dest := ensure.TempDir(t) - defer os.RemoveAll(dest) + dest := t.TempDir() // Set up a fake repo srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") diff --git a/pkg/downloader/doc.go b/pkg/downloader/doc.go index 9588a7dfe..848468090 100644 --- a/pkg/downloader/doc.go +++ b/pkg/downloader/doc.go @@ -13,7 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package downloader provides a library for downloading charts. +/* +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. This diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 22db8bfdd..a5b0af080 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -20,7 +20,6 @@ import ( "encoding/hex" "fmt" "io" - "io/ioutil" "log" "net/url" "os" @@ -34,7 +33,6 @@ import ( "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" @@ -43,6 +41,7 @@ import ( "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/repo" ) @@ -232,7 +231,7 @@ func (m *Manager) loadChartDir() (*chart.Chart, error) { // // 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) + res := resolver.New(m.ChartPath, m.RepositoryCache, m.RegistryClient) return res.Resolve(req, repoNames) } @@ -249,22 +248,24 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { destPath := filepath.Join(m.ChartPath, "charts") tmpPath := filepath.Join(m.ChartPath, "tmpcharts") - // Create 'charts' directory if it doesn't already exist. - if fi, err := os.Stat(destPath); err != nil { + // Check if 'charts' directory is not actually a directory. If it does not exist, create it. + if fi, err := os.Stat(destPath); err == nil { + if !fi.IsDir() { + return errors.Errorf("%q is not a directory", destPath) + } + } else if os.IsNotExist(err) { if err := os.MkdirAll(destPath, 0755); err != nil { return err } - } else if !fi.IsDir() { - return errors.Errorf("%q is not a directory", destPath) - } - - if err := fs.RenameWithFallback(destPath, tmpPath); err != nil { - return errors.Wrap(err, "unable to move current charts to tmp dir") + } else { + return fmt.Errorf("unable to retrieve file info for '%s': %v", destPath, err) } - if err := os.MkdirAll(destPath, 0755); err != nil { + // Prepare tmpPath + if err := os.MkdirAll(tmpPath, 0755); err != nil { return err } + defer os.RemoveAll(tmpPath) fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) var saveError error @@ -273,10 +274,11 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { // 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) + // NOTE: we are only validating the local dependency conforms to the constraints. No copying to tmpPath is necessary. + chartPath := filepath.Join(destPath, dep.Name) ch, err := loader.LoadDir(chartPath) if err != nil { - return fmt.Errorf("unable to load chart: %v", err) + return fmt.Errorf("unable to load chart '%s': %v", chartPath, err) } constraint, err := semver.NewConstraint(dep.Version) @@ -299,7 +301,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { if m.Debug { fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) } - ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version) + ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version, tmpPath) if err != nil { saveError = err break @@ -310,7 +312,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { // Any failure to resolve/download a chart should fail: // https://github.com/helm/helm/issues/1439 - churl, username, password, insecureskiptlsverify, passcredentialsall, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) + churl, username, password, insecureskiptlsverify, passcredentialsall, caFile, certFile, keyFile, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) if err != nil { saveError = errors.Wrapf(err, "could not find %s", churl) break @@ -329,21 +331,18 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { Keyring: m.Keyring, RepositoryConfig: m.RepositoryConfig, RepositoryCache: m.RepositoryCache, + RegistryClient: m.RegistryClient, Getters: m.Getters, Options: []getter.Option{ getter.WithBasicAuth(username, password), getter.WithPassCredentialsAll(passcredentialsall), getter.WithInsecureSkipVerifyTLS(insecureskiptlsverify), + getter.WithTLSClientConfig(certFile, keyFile, caFile), }, } version := "" - if strings.HasPrefix(churl, "oci://") { - if !resolver.FeatureGateOCI.IsEnabled() { - return errors.Wrapf(resolver.FeatureGateOCI.Error(), - "the repository %s is an OCI registry", churl) - } - + if registry.IsOCI(churl) { churl, version, err = parseOCIRef(churl) if err != nil { return errors.Wrapf(err, "could not parse OCI reference") @@ -353,8 +352,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { getter.WithTagName(version)) } - _, _, err = dl.DownloadTo(churl, version, destPath) - if err != nil { + if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil { saveError = errors.Wrapf(err, "could not download %s", churl) break } @@ -362,36 +360,14 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { churls[churl] = struct{}{} } + // TODO: this should probably be refactored to be a []error, so we can capture and provide more information rather than "last error wins". if saveError == nil { - fmt.Fprintln(m.Out, "Deleting outdated charts") - for _, dep := range deps { - // 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 { + // now we can move all downloaded charts to destPath and delete outdated dependencies + if err := m.safeMoveDeps(deps, tmpPath, destPath); err != nil { return err } - if err := os.RemoveAll(tmpPath); err != nil { - return errors.Wrapf(err, "failed to remove %v", tmpPath) - } } else { fmt.Fprintln(m.Out, "Save error occurred: ", saveError) - fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state") - for _, dep := range deps { - if err := m.safeDeleteDep(dep.Name, destPath); err != nil { - return err - } - } - if err := os.RemoveAll(destPath); err != nil { - return errors.Wrapf(err, "failed to remove %v", destPath) - } - if err := fs.RenameWithFallback(tmpPath, destPath); err != nil { - return errors.Wrap(err, "unable to move current charts to tmp dir") - } return saveError } return nil @@ -409,36 +385,76 @@ func parseOCIRef(chartRef string) (string, string, error) { return chartRef, tag, nil } -// safeDeleteDep deletes any versions of the given dependency in the given directory. +// safeMoveDep moves all dependencies in the source and moves them into dest. // // It does this by first matching the file name to an expected pattern, then loading -// the file to verify that it is a chart with the same name as the given name. +// the file to verify that it is a chart. +// +// Any charts in dest that do not exist in source are removed (barring local dependencies) // -// Because it requires tar file introspection, it is more intensive than a basic delete. +// Because it requires tar file introspection, it is more intensive than a basic move. // // This will only return errors that should stop processing entirely. Other errors // will emit log messages or be ignored. -func (m *Manager) safeDeleteDep(name, dir string) error { - files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz")) +func (m *Manager) safeMoveDeps(deps []*chart.Dependency, source, dest string) error { + existsInSourceDirectory := map[string]bool{} + isLocalDependency := map[string]bool{} + sourceFiles, err := os.ReadDir(source) if err != nil { - // Only for ErrBadPattern return err } - for _, fname := range files { - ch, err := loader.LoadFile(fname) - if err != nil { - fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err) + // attempt to read destFiles; fail fast if we can't + destFiles, err := os.ReadDir(dest) + if err != nil { + return err + } + + for _, dep := range deps { + if dep.Repository == "" { + isLocalDependency[dep.Name] = true + } + } + + for _, file := range sourceFiles { + if file.IsDir() { continue } - if ch.Name() != name { - // This is not the file you are looking for. + filename := file.Name() + sourcefile := filepath.Join(source, filename) + destfile := filepath.Join(dest, filename) + existsInSourceDirectory[filename] = true + if _, err := loader.LoadFile(sourcefile); err != nil { + fmt.Fprintf(m.Out, "Could not verify %s for moving: %s (Skipping)", sourcefile, err) continue } - if err := os.Remove(fname); err != nil { - fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err) + // NOTE: no need to delete the dest; os.Rename replaces it. + if err := fs.RenameWithFallback(sourcefile, destfile); err != nil { + fmt.Fprintf(m.Out, "Unable to move %s to charts dir %s (Skipping)", sourcefile, err) continue } } + + fmt.Fprintln(m.Out, "Deleting outdated charts") + // find all files that exist in dest that do not exist in source; delete them (outdated dependencies) + for _, file := range destFiles { + if !file.IsDir() && !existsInSourceDirectory[file.Name()] { + fname := filepath.Join(dest, file.Name()) + ch, err := loader.LoadFile(fname) + if err != nil { + fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)\n", fname, err) + continue + } + // local dependency - skip + if isLocalDependency[ch.Name()] { + continue + } + if err := os.Remove(fname); err != nil { + fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err) + continue + } + } + } + return nil } @@ -456,7 +472,7 @@ func (m *Manager) hasAllRepos(deps []*chart.Dependency) error { Loop: for _, dd := range deps { // If repo is from local path or OCI, continue - if strings.HasPrefix(dd.Repository, "file://") || strings.HasPrefix(dd.Repository, "oci://") { + if strings.HasPrefix(dd.Repository, "file://") || registry.IsOCI(dd.Repository) { continue } @@ -557,8 +573,7 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string, 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://") { + if dd.Repository == "" { continue } // if dep chart is from local path, verify the path is valid @@ -574,7 +589,7 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string, continue } - if strings.HasPrefix(dd.Repository, "oci://") { + if registry.IsOCI(dd.Repository) { reposMap[dd.Name] = dd.Repository continue } @@ -652,6 +667,7 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { if err != nil { return err } + r.CachePath = m.RepositoryCache wg.Add(1) go func(r *repo.ChartRepository) { if _, err := r.DownloadIndexFile(); err != nil { @@ -687,9 +703,9 @@ 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 (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureskiptlsverify, passcredentialsall bool, err error) { - if strings.HasPrefix(repoURL, "oci://") { - return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", false, false, nil +func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureskiptlsverify, passcredentialsall bool, caFile, certFile, keyFile string, err error) { + if registry.IsOCI(repoURL) { + return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", false, false, "", "", "", nil } for _, cr := range repos { @@ -713,15 +729,18 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]* password = cr.Config.Password passcredentialsall = cr.Config.PassCredentialsAll insecureskiptlsverify = cr.Config.InsecureSkipTLSverify + caFile = cr.Config.CAFile + certFile = cr.Config.CertFile + keyFile = cr.Config.KeyFile return } } - url, err = repo.FindChartInRepoURL(repoURL, name, version, "", "", "", m.Getters) + url, err = repo.FindChartInRepoURL(repoURL, name, version, certFile, keyFile, caFile, m.Getters) if err == nil { - return url, username, password, false, false, err + return url, username, password, false, false, "", "", "", err } err = errors.Errorf("chart %s not found in %s: %s", name, repoURL, err) - return url, username, password, false, false, err + return url, username, password, false, false, "", "", "", err } // findEntryByName finds an entry in the chart repository whose name matches the given name. @@ -779,6 +798,7 @@ func normalizeURL(baseURL, urlOrPath string) (string, error) { return urlOrPath, errors.Wrap(err, "base URL failed to parse") } + u2.RawPath = path.Join(u2.RawPath, urlOrPath) u2.Path = path.Join(u2.Path, urlOrPath) return u2.String(), nil } @@ -825,13 +845,11 @@ func writeLock(chartpath string, lock *chart.Lock, legacyLockfile bool) error { lockfileName = "requirements.lock" } dest := filepath.Join(chartpath, lockfileName) - return ioutil.WriteFile(dest, data, 0644) + return os.WriteFile(dest, data, 0644) } -// archive a dep chart from local directory and save it into charts/ -func tarFromLocalDir(chartpath, name, repo, version string) (string, error) { - destPath := filepath.Join(chartpath, "charts") - +// archive a dep chart from local directory and save it into destPath +func tarFromLocalDir(chartpath, name, repo, version, destPath string) (string, error) { if !strings.HasPrefix(repo, "file://") { return "", errors.Errorf("wrong format: chart %s repository %s", name, repo) } @@ -864,20 +882,6 @@ func tarFromLocalDir(chartpath, name, repo, version string) (string, error) { return "", errors.Errorf("can't get a valid version for dependency %s", name) } -// move files from tmppath to destpath -func move(tmpPath, destPath string) error { - files, _ := ioutil.ReadDir(tmpPath) - for _, file := range files { - filename := file.Name() - tmpfile := filepath.Join(tmpPath, filename) - destfile := filepath.Join(destPath, filename) - if err := 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-" diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go index 0cc6d6f12..f7ab1a568 100644 --- a/pkg/downloader/manager_test.go +++ b/pkg/downloader/manager_test.go @@ -17,11 +17,13 @@ package downloader import ( "bytes" + "os" "path/filepath" "reflect" "testing" "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/repo/repotest" @@ -52,6 +54,7 @@ func TestNormalizeURL(t *testing.T) { }{ {name: "basic URL", base: "https://example.com", path: "http://helm.sh/foo", expect: "http://helm.sh/foo"}, {name: "relative path", base: "https://helm.sh/charts", path: "foo", expect: "https://helm.sh/charts/foo"}, + {name: "Encoded path", base: "https://helm.sh/a%2Fb/charts", path: "foo", expect: "https://helm.sh/a%2Fb/charts/foo"}, } for _, tt := range tests { @@ -81,7 +84,7 @@ func TestFindChartURL(t *testing.T) { version := "0.1.0" repoURL := "http://example.com/charts" - churl, username, password, insecureSkipTLSVerify, passcredentialsall, err := m.findChartURL(name, version, repoURL, repos) + churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, err := m.findChartURL(name, version, repoURL, repos) if err != nil { t.Fatal(err) } @@ -106,7 +109,7 @@ func TestFindChartURL(t *testing.T) { version = "1.2.3" repoURL = "https://example-https-insecureskiptlsverify.com" - churl, username, password, insecureSkipTLSVerify, passcredentialsall, err = m.findChartURL(name, version, repoURL, repos) + churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, err = m.findChartURL(name, version, repoURL, repos) if err != nil { t.Fatal(err) } @@ -213,6 +216,54 @@ func TestGetRepoNames(t *testing.T) { } } +func TestDownloadAll(t *testing.T) { + chartPath := t.TempDir() + m := &Manager{ + Out: new(bytes.Buffer), + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + ChartPath: chartPath, + } + signtest, err := loader.LoadDir(filepath.Join("testdata", "signtest")) + if err != nil { + t.Fatal(err) + } + if err := chartutil.SaveDir(signtest, filepath.Join(chartPath, "testdata")); err != nil { + t.Fatal(err) + } + + local, err := loader.LoadDir(filepath.Join("testdata", "local-subchart")) + if err != nil { + t.Fatal(err) + } + if err := chartutil.SaveDir(local, filepath.Join(chartPath, "charts")); err != nil { + t.Fatal(err) + } + + signDep := &chart.Dependency{ + Name: signtest.Name(), + Repository: "file://./testdata/signtest", + Version: signtest.Metadata.Version, + } + localDep := &chart.Dependency{ + Name: local.Name(), + Repository: "", + Version: local.Metadata.Version, + } + + // create a 'tmpcharts' directory to test #5567 + if err := os.MkdirAll(filepath.Join(chartPath, "tmpcharts"), 0755); err != nil { + t.Fatal(err) + } + if err := m.downloadAll([]*chart.Dependency{signDep, localDep}); err != nil { + t.Error(err) + } + + if _, err := os.Stat(filepath.Join(chartPath, "charts", "signtest-0.1.0.tgz")); os.IsNotExist(err) { + t.Error(err) + } +} + func TestUpdateBeforeBuild(t *testing.T) { // Set up a fake repo srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") diff --git a/pkg/downloader/testdata/repositories.yaml b/pkg/downloader/testdata/repositories.yaml index 32bc395a0..db7a57687 100644 --- a/pkg/downloader/testdata/repositories.yaml +++ b/pkg/downloader/testdata/repositories.yaml @@ -23,4 +23,6 @@ repositories: caFile: "ca" - name: testing-https-insecureskip-tls-verify url: "https://example-https-insecureskiptlsverify.com" - insecure_skip_tls_verify: true + insecure_skip_tls_verify: true + - name: encoded-url + url: "http://example.com/with%2Fslash" diff --git a/pkg/downloader/testdata/repository/encoded-url-index.yaml b/pkg/downloader/testdata/repository/encoded-url-index.yaml new file mode 100644 index 000000000..f9ec867a5 --- /dev/null +++ b/pkg/downloader/testdata/repository/encoded-url-index.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +entries: + foobar: + - name: foobar + description: Foo Chart With Encoded URL + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - charts/foobar-4.2.1.tgz + version: 4.2.1 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + apiVersion: v2 diff --git a/pkg/downloader/testdata/repository/testing-https-insecureskip-tls-verify-index.yaml b/pkg/downloader/testdata/repository/testing-https-insecureskip-tls-verify-index.yaml index 11cfa629c..58f928ff4 100644 --- a/pkg/downloader/testdata/repository/testing-https-insecureskip-tls-verify-index.yaml +++ b/pkg/downloader/testdata/repository/testing-https-insecureskip-tls-verify-index.yaml @@ -1,7 +1,7 @@ apiVersion: v1 entries: tlsfoo: - - name: tlsfoo + - name: tlsfoo description: TLS FOO Chart home: https://helm.sh/helm keywords: [] diff --git a/pkg/engine/doc.go b/pkg/engine/doc.go index 6ff875c46..6b3443aaf 100644 --- a/pkg/engine/doc.go +++ b/pkg/engine/doc.go @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package engine implements the Go text template engine as needed for Helm. +/* +Package engine implements the Go text template engine as needed for Helm. When Helm renders templates it does so with additional functions and different modes (e.g., strict, lint mode). This package handles the helm specific diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 4da64f5cc..150be16b7 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -42,6 +42,15 @@ type Engine struct { LintMode bool // the rest config to connect to the kubernetes api config *rest.Config + // EnableDNS tells the engine to allow DNS lookups when rendering templates + EnableDNS bool +} + +// New creates a new instance of Engine using the passed in rest config. +func New(config *rest.Config) Engine { + return Engine{ + config: config, + } } // Render takes a chart, optional values, and value overrides, and attempts to render the Go templates. @@ -97,7 +106,7 @@ const warnStartDelim = "HELM_ERR_START" const warnEndDelim = "HELM_ERR_END" const recursionMaxNums = 1000 -var warnRegex = regexp.MustCompile(warnStartDelim + `(.*)` + warnEndDelim) +var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim) func warnWrap(warn string) string { return warnStartDelim + warn + warnEndDelim @@ -189,6 +198,14 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render funcMap["lookup"] = NewLookupFunction(e.config) } + // When DNS lookups are not enabled override the sprig function and return + // an empty string. + if !e.EnableDNS { + funcMap["getHostByName"] = func(name string) string { + return "" + } + } + t.Funcs(funcMap) } @@ -344,13 +361,20 @@ 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, vals chartutil.Values) { +func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) map[string]interface{} { + subCharts := make(map[string]interface{}) + chartMetaData := struct { + chart.Metadata + IsRoot bool + }{*c.Metadata, c.IsRoot()} + next := map[string]interface{}{ - "Chart": c.Metadata, + "Chart": chartMetaData, "Files": newFiles(c.Files), "Release": vals["Release"], "Capabilities": vals["Capabilities"], "Values": make(chartutil.Values), + "Subcharts": subCharts, } // If there is a {{.Values.ThisChart}} in the parent metadata, @@ -362,11 +386,14 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil. } for _, child := range c.Dependencies() { - recAllTpls(child, templates, next) + subCharts[child.Name()] = recAllTpls(child, templates, next) } newParentID := c.ChartFullPath() for _, t := range c.Templates { + if t == nil { + continue + } if !isTemplateValid(c, t.Name) { continue } @@ -376,6 +403,8 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil. basePath: path.Join(newParentID, "templates"), } } + + return next } // isTemplateValid returns true if the template is valid for the chart type diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 72ee02626..27bb9e78e 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -18,9 +18,11 @@ package engine import ( "fmt" + "path" "strings" "sync" "testing" + "text/template" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" @@ -89,6 +91,7 @@ func TestRender(t *testing.T) { {Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")}, {Name: "templates/test3", Data: []byte("{{.noValue}}")}, {Name: "templates/test4", Data: []byte("{{toJson .Values}}")}, + {Name: "templates/test5", Data: []byte("{{getHostByName \"helm.sh\"}}")}, }, Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"}, } @@ -117,6 +120,7 @@ func TestRender(t *testing.T) { "moby/templates/test2": "ishmael", "moby/templates/test3": "", "moby/templates/test4": `{"global":{"callme":"Ishmael"},"inner":"inn","outer":"spouter"}`, + "moby/templates/test5": "", } for name, data := range expect { @@ -200,6 +204,42 @@ func TestRenderInternals(t *testing.T) { } } +func TestRenderWIthDNS(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "moby", + Version: "1.2.3", + }, + Templates: []*chart.File{ + {Name: "templates/test1", Data: []byte("{{getHostByName \"helm.sh\"}}")}, + }, + Values: map[string]interface{}{}, + } + + vals := map[string]interface{}{ + "Values": map[string]interface{}{}, + } + + v, err := chartutil.CoalesceValues(c, vals) + if err != nil { + t.Fatalf("Failed to coalesce values: %s", err) + } + + var e Engine + e.EnableDNS = true + out, err := e.Render(c, v) + if err != nil { + t.Errorf("Failed to render templates: %s", err) + } + + for _, val := range c.Templates { + fp := path.Join("moby", val.Name) + if out[fp] == "" { + t.Errorf("Expected IP address, got %q", out[fp]) + } + } +} + func TestParallelRenderInternals(t *testing.T) { // Make sure that we can use one Engine to run parallel template renders. e := new(Engine) @@ -245,44 +285,65 @@ func TestParseErrors(t *testing.T) { 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 := "" }} + cases := []struct { + name string + tpls map[string]renderable + expected string + }{ + { + name: "MissingRequired", + tpls: map[string]renderable{ + "missing_required": {tpl: `{{required "foo is required" .Values.foo}}`, vals: vals}, + }, + expected: `execution error at (missing_required:1:2): foo is required`, + }, + { + name: "MissingRequiredWithColons", + tpls: map[string]renderable{ + "missing_required_with_colons": {tpl: `{{required ":this: message: has many: colons:" .Values.foo}}`, vals: vals}, + }, + expected: `execution error at (missing_required_with_colons:1:2): :this: message: has many: colons:`, + }, + { + name: "Issue6044", + tpls: map[string]renderable{ + "issue6044": { + vals: vals, + tpl: `{{ $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) +{{- required (printf "%s: something is missing" $myvar) $someEmptyValue | repeat 0 }}`, + }, + }, + expected: `execution error at (issue6044:3:4): abc: something is missing`, + }, + { + name: "MissingRequiredWithNewlines", + tpls: map[string]renderable{ + "issue9981": {tpl: `{{required "foo is required\nmore info after the break" .Values.foo}}`, vals: vals}, + }, + expected: `execution error at (issue9981:1:2): foo is required +more info after the break`, + }, + { + name: "FailWithNewlines", + tpls: map[string]renderable{ + "issue9981": {tpl: `{{fail "something is wrong\nlinebreak"}}`, vals: vals}, + }, + expected: `execution error at (issue9981:1:2): something is wrong +linebreak`, + }, } - expected = `execution error at (issue6044:3:4): abc: something is missing` - if err.Error() != expected { - t.Errorf("Expected '%s', got %q", expected, err.Error()) + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + _, err := new(Engine).render(tt.tpls) + if err == nil { + t.Fatalf("Expected failures while rendering: %s", err) + } + if err.Error() != tt.expected { + t.Errorf("Expected %q, got %q", tt.expected, err.Error()) + } + }) } } @@ -346,6 +407,36 @@ func TestAllTemplates(t *testing.T) { } } +func TestChartValuesContainsIsRoot(t *testing.T) { + ch1 := &chart.Chart{ + Metadata: &chart.Metadata{Name: "parent"}, + Templates: []*chart.File{ + {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, + }, + } + dep1 := &chart.Chart{ + Metadata: &chart.Metadata{Name: "child"}, + Templates: []*chart.File{ + {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, + }, + } + ch1.AddDependency(dep1) + + out, err := Render(ch1, chartutil.Values{}) + if err != nil { + t.Fatalf("failed to render templates: %s", err) + } + expects := map[string]string{ + "parent/charts/child/templates/isroot": "false", + "parent/templates/isroot": "true", + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } +} + func TestRenderDependency(t *testing.T) { deptpl := `{{define "myblock"}}World{{end}}` toptpl := `Hello {{template "myblock"}}` @@ -384,6 +475,8 @@ func TestRenderNestedValues(t *testing.T) { // Ensure namespacing rules are working. deepestpath := "templates/inner.tpl" checkrelease := "templates/release.tpl" + // Ensure subcharts scopes are working. + subchartspath := "templates/subcharts.tpl" deepest := &chart.Chart{ Metadata: &chart.Metadata{Name: "deepest"}, @@ -391,7 +484,7 @@ func TestRenderNestedValues(t *testing.T) { {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: map[string]interface{}{"what": "milkshake"}, + Values: map[string]interface{}{"what": "milkshake", "where": "here"}, } inner := &chart.Chart{ @@ -399,7 +492,7 @@ func TestRenderNestedValues(t *testing.T) { Templates: []*chart.File{ {Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, }, - Values: map[string]interface{}{"who": "Robert"}, + Values: map[string]interface{}{"who": "Robert", "what": "glasses"}, } inner.AddDependency(deepest) @@ -407,12 +500,14 @@ func TestRenderNestedValues(t *testing.T) { Metadata: &chart.Metadata{Name: "top"}, Templates: []*chart.File{ {Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, + {Name: subchartspath, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)}, }, Values: map[string]interface{}{ "what": "stinkweed", "who": "me", "herrick": map[string]interface{}{ - "who": "time", + "who": "time", + "what": "Sun", }, }, } @@ -422,7 +517,8 @@ func TestRenderNestedValues(t *testing.T) { "what": "rosebuds", "herrick": map[string]interface{}{ "deepest": map[string]interface{}{ - "what": "flower", + "what": "flower", + "where": "Heaven", }, }, "global": map[string]interface{}{ @@ -469,6 +565,11 @@ func TestRenderNestedValues(t *testing.T) { if out[fullcheckrelease] != "Tomorrow will be dyin" { t.Errorf("Unexpected release: %q", out[fullcheckrelease]) } + + fullchecksubcharts := "top/" + subchartspath + if out[fullchecksubcharts] != "The glorious Lamp of Heaven, the Sun" { + t.Errorf("Unexpected subcharts: %q", out[fullchecksubcharts]) + } } func TestRenderBuiltinValues(t *testing.T) { @@ -488,6 +589,7 @@ func TestRenderBuiltinValues(t *testing.T) { Metadata: &chart.Metadata{Name: "Troy"}, Templates: []*chart.File{ {Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + {Name: "templates/Amata", Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)}, }, } outer.AddDependency(inner) @@ -510,6 +612,7 @@ func TestRenderBuiltinValues(t *testing.T) { expects := map[string]string{ "Troy/charts/Latium/templates/Lavinia": "Troy/charts/Latium/templates/LaviniaLatiumAeneid", "Troy/templates/Aeneas": "Troy/templates/AeneasTroyAeneid", + "Troy/templates/Amata": "Latium Virgil", "Troy/charts/Latium/templates/From": "Virgil Aeneid", } for file, expect := range expects { @@ -767,3 +870,242 @@ func TestRenderRecursionLimit(t *testing.T) { } } + +func TestRenderLoadTemplateForTplFromFile(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplLoadFromFile"}, + Templates: []*chart.File{ + {Name: "templates/base", Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, + {Name: "templates/_function", Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, + }, + Files: []*chart.File{ + {Name: "test", Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, + {Name: "test2", Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)}, + }, + } + + v := chartutil.Values{ + "Values": chartutil.Values{ + "filename": "test", + "filename2": "test2", + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expect := "test-function nested-define-content" + if got := out["TplLoadFromFile/templates/base"]; got != expect { + t.Fatalf("Expected %q, got %q", expect, got) + } +} + +func TestRenderTplEmpty(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplEmpty"}, + Templates: []*chart.File{ + {Name: "templates/empty-string", Data: []byte(`{{tpl "" .}}`)}, + {Name: "templates/empty-action", Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, + {Name: "templates/only-defines", Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, + }, + } + v := chartutil.Values{ + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expects := map[string]string{ + "TplEmpty/templates/empty-string": "", + "TplEmpty/templates/empty-action": "", + "TplEmpty/templates/only-defines": "", + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } +} + +func TestRenderTplTemplateNames(t *testing.T) { + // .Template.BasePath and .Name make it through + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplTemplateNames"}, + Templates: []*chart.File{ + {Name: "templates/default-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, + {Name: "templates/default-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, + {Name: "templates/modified-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, + {Name: "templates/modified-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)}, + // Current implementation injects the 'tpl' template as if it were a template file, and + // so only BasePath and Name make it through. + {Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, + }, + } + v := chartutil.Values{ + "Values": chartutil.Values{ + "dot": chartutil.Values{ + "Template": chartutil.Values{ + "BasePath": "path/to/template", + "Name": "name-of-template", + "Field": "extra-field", + }, + }, + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expects := map[string]string{ + "TplTemplateNames/templates/default-basepath": "TplTemplateNames/templates", + "TplTemplateNames/templates/default-name": "TplTemplateNames/templates/default-name", + "TplTemplateNames/templates/modified-basepath": "path/to/template", + "TplTemplateNames/templates/modified-name": "name-of-template", + "TplTemplateNames/templates/modified-field": "", + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } +} + +func TestRenderTplRedefines(t *testing.T) { + // Redefining a template inside 'tpl' does not affect the outer definition + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplRedefines"}, + Templates: []*chart.File{ + {Name: "templates/_partials", Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, + {Name: "templates/partial", Data: []byte( + `before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`, + )}, + {Name: "templates/manifest", Data: []byte( + `{{define "manifest"}}original-in-manifest{{end}}` + + `before: {{include "manifest" .}}\n{{tpl .Values.manifestText .}}\nafter: {{include "manifest" .}}`, + )}, + // The current implementation replaces the manifest text and re-parses, so a + // partial template defined only in the manifest invoking tpl cannot be accessed + // by that tpl call. + //{Name: "templates/manifest-only", Data: []byte( + // `{{define "manifest-only"}}only-in-manifest{{end}}` + + // `before: {{include "manifest-only" .}}\n{{tpl .Values.manifestOnlyText .}}\nafter: {{include "manifest-only" .}}`, + //)}, + }, + } + v := chartutil.Values{ + "Values": chartutil.Values{ + "partialText": `{{define "partial"}}redefined-in-tpl{{end}}tpl: {{include "partial" .}}`, + "manifestText": `{{define "manifest"}}redefined-in-tpl{{end}}tpl: {{include "manifest" .}}`, + "manifestOnlyText": `tpl: {{include "manifest-only" .}}`, + }, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expects := map[string]string{ + "TplRedefines/templates/partial": `before: original-in-partial\ntpl: original-in-partial\nafter: original-in-partial`, + "TplRedefines/templates/manifest": `before: original-in-manifest\ntpl: redefined-in-tpl\nafter: original-in-manifest`, + //"TplRedefines/templates/manifest-only": `before: only-in-manifest\ntpl: only-in-manifest\nafter: only-in-manifest`, + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } +} + +func TestRenderTplMissingKey(t *testing.T) { + // Rendering a missing key results in empty/zero output. + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplMissingKey"}, + Templates: []*chart.File{ + {Name: "templates/manifest", Data: []byte( + `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, + )}, + }, + } + v := chartutil.Values{ + "Values": chartutil.Values{}, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + out, err := Render(c, v) + if err != nil { + t.Fatal(err) + } + + expects := map[string]string{ + "TplMissingKey/templates/manifest": `missingValue: `, + } + for file, expect := range expects { + if out[file] != expect { + t.Errorf("Expected %q, got %q", expect, out[file]) + } + } +} + +func TestRenderTplMissingKeyString(t *testing.T) { + // Rendering a missing key results in error + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"}, + Templates: []*chart.File{ + {Name: "templates/manifest", Data: []byte( + `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, + )}, + }, + } + v := chartutil.Values{ + "Values": chartutil.Values{}, + "Chart": c.Metadata, + "Release": chartutil.Values{ + "Name": "TestRelease", + }, + } + + e := new(Engine) + e.Strict = true + + out, err := e.Render(c, v) + if err == nil { + t.Errorf("Expected error, got %v", out) + return + } + switch err.(type) { + case (template.ExecError): + errTxt := fmt.Sprint(err) + if !strings.Contains(errTxt, "noSuchKey") { + t.Errorf("Expected error to contain 'noSuchKey', got %s", errTxt) + } + default: + // Some unexpected error. + t.Fatal(err) + } +} diff --git a/pkg/engine/files.go b/pkg/engine/files.go index d7e62da5a..f2cfdb3f3 100644 --- a/pkg/engine/files.go +++ b/pkg/engine/files.go @@ -99,7 +99,8 @@ func (f files) Glob(pattern string) files { // The output will not be indented, so you will want to pipe this to the // 'indent' template function. // -// data: +// data: +// // {{ .Files.Glob("config/**").AsConfig() | indent 4 }} func (f files) AsConfig() string { if f == nil { @@ -128,8 +129,9 @@ func (f files) AsConfig() string { // The output will not be indented, so you will want to pipe this to the // 'indent' template function. // -// data: -// {{ .Files.Glob("secrets/*").AsSecrets() }} +// data: +// +// {{ .Files.Glob("secrets/*").AsSecrets() | indent 4 }} func (f files) AsSecrets() string { if f == nil { return "" @@ -155,6 +157,9 @@ func (f files) Lines(path string) []string { if f == nil || f[path] == nil { return []string{} } - - return strings.Split(string(f[path]), "\n") + s := string(f[path]) + if s[len(s)-1] == '\n' { + s = s[:len(s)-1] + } + return strings.Split(s, "\n") } diff --git a/pkg/engine/files_test.go b/pkg/engine/files_test.go index 4b37724f9..e53263c76 100644 --- a/pkg/engine/files_test.go +++ b/pkg/engine/files_test.go @@ -28,7 +28,8 @@ var cases = []struct { {"ship/stowaway.txt", "Legatt"}, {"story/name.txt", "The Secret Sharer"}, {"story/author.txt", "Joseph Conrad"}, - {"multiline/test.txt", "bar\nfoo"}, + {"multiline/test.txt", "bar\nfoo\n"}, + {"multiline/test_with_blank_lines.txt", "bar\nfoo\n\n\n"}, } func getTestFiles() files { @@ -96,3 +97,15 @@ func TestLines(t *testing.T) { as.Equal("bar", out[0]) } + +func TestBlankLines(t *testing.T) { + as := assert.New(t) + + f := getTestFiles() + + out := f.Lines("multiline/test_with_blank_lines.txt") + as.Len(out, 4) + + as.Equal("bar", out[0]) + as.Equal("", out[3]) +} diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 92b4c3383..8f05a3a1d 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -35,12 +35,11 @@ import ( // // Known late-bound functions: // -// - "include" -// - "tpl" +// - "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") diff --git a/pkg/engine/funcs_test.go b/pkg/engine/funcs_test.go index 62c63ec2b..29bc121b5 100644 --- a/pkg/engine/funcs_test.go +++ b/pkg/engine/funcs_test.go @@ -117,9 +117,9 @@ func TestFuncs(t *testing.T) { // 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. +// the tooling to not update to a minor or patch version. `go install` 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{}{ diff --git a/pkg/engine/lookup_func.go b/pkg/engine/lookup_func.go index d1bf1105a..b378ca9d6 100644 --- a/pkg/engine/lookup_func.go +++ b/pkg/engine/lookup_func.go @@ -77,7 +77,7 @@ func NewLookupFunction(config *rest.Config) lookupFunc { } } -// getDynamicClientOnUnstructured returns a dynamic client on an Unstructured type. This client can be further namespaced. +// getDynamicClientOnKind 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) diff --git a/pkg/gates/doc.go b/pkg/gates/doc.go index 762fdb8c6..6592cf4d4 100644 --- a/pkg/gates/doc.go +++ b/pkg/gates/doc.go @@ -13,7 +13,8 @@ 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. +/* +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. */ diff --git a/pkg/getter/doc.go b/pkg/getter/doc.go index c53ef1ae0..11cf6153e 100644 --- a/pkg/getter/doc.go +++ b/pkg/getter/doc.go @@ -13,7 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package getter provides a generalize tool for fetching data by scheme. +/* +Package getter provides a generalize tool for fetching data by scheme. This provides a method by which the plugin system can load arbitrary protocol handlers based upon a URL scheme. diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index 78add728a..45ab4da7e 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -18,12 +18,13 @@ package getter import ( "bytes" + "net/http" "time" "github.com/pkg/errors" - "helm.sh/helm/v3/internal/experimental/registry" "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/registry" ) // options are generic parameters to be provided to the getter during instantiation. @@ -36,6 +37,7 @@ type options struct { caFile string unTar bool insecureSkipVerifyTLS bool + plainHTTP bool username string password string passCredentialsAll bool @@ -43,6 +45,7 @@ type options struct { version string registryClient *registry.Client timeout time.Duration + transport *http.Transport } // Option allows specifying various settings configurable by the user for overriding the defaults @@ -94,6 +97,12 @@ func WithTLSClientConfig(certFile, keyFile, caFile string) Option { } } +func WithPlainHTTP(plainHTTP bool) Option { + return func(opts *options) { + opts.plainHTTP = plainHTTP + } +} + // WithTimeout sets the timeout for requests func WithTimeout(timeout time.Duration) Option { return func(opts *options) { @@ -119,6 +128,13 @@ func WithUntar() Option { } } +// WithTransport sets the http.Transport to allow overwriting the HTTPGetter default. +func WithTransport(transport *http.Transport) Option { + return func(opts *options) { + opts.transport = transport + } +} + // Getter is an interface to support GET to the specified URL. type Getter interface { // Get file content by url string @@ -163,13 +179,25 @@ func (p Providers) ByScheme(scheme string) (Getter, error) { return nil, errors.Errorf("scheme %q not supported", scheme) } +const ( + // The cost timeout references curl's default connection timeout. + // https://github.com/curl/curl/blob/master/lib/connect.h#L40C21-L40C21 + // The helm commands are usually executed manually. Considering the acceptable waiting time, we reduced the entire request time to 120s. + DefaultHTTPTimeout = 120 +) + +var defaultOptions = []Option{WithTimeout(time.Second * DefaultHTTPTimeout)} + var httpProvider = Provider{ Schemes: []string{"http", "https"}, - New: NewHTTPGetter, + New: func(options ...Option) (Getter, error) { + options = append(options, defaultOptions...) + return NewHTTPGetter(options...) + }, } var ociProvider = Provider{ - Schemes: []string{"oci"}, + Schemes: []string{registry.OCIScheme}, New: NewOCIGetter, } diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 94b64381c..b53e558e3 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -21,6 +21,7 @@ import ( "io" "net/http" "net/url" + "sync" "github.com/pkg/errors" @@ -31,7 +32,9 @@ import ( // HTTPGetter is the default HTTP(/S) backend handler type HTTPGetter struct { - opts options + opts options + transport *http.Transport + once sync.Once } // Get performs a Get from repo.Getter and returns the body. @@ -43,13 +46,11 @@ func (g *HTTPGetter) Get(href string, options ...Option) (*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 // separate helm calls from other tools interacting with repos. - req, err := http.NewRequest("GET", href, nil) + req, err := http.NewRequest(http.MethodGet, href, nil) if err != nil { - return buf, err + return nil, err } req.Header.Set("User-Agent", version.GetUserAgent()) @@ -61,11 +62,11 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) { // with the basic auth is the one being fetched. u1, err := url.Parse(g.opts.url) if err != nil { - return buf, errors.Wrap(err, "Unable to parse getter URL") + return nil, errors.Wrap(err, "Unable to parse getter URL") } u2, err := url.Parse(href) if err != nil { - return buf, errors.Wrap(err, "Unable to parse URL getting from") + return nil, errors.Wrap(err, "Unable to parse URL getting from") } // Host on URL (returned from url.Parse) contains the port if present. @@ -84,14 +85,15 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) { resp, err := client.Do(req) if err != nil { - return buf, err + return nil, err } - if resp.StatusCode != 200 { - return buf, errors.Errorf("failed to fetch %s : %s", href, resp.Status) + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, errors.Errorf("failed to fetch %s : %s", href, resp.Status) } + buf := bytes.NewBuffer(nil) _, err = io.Copy(buf, resp.Body) - resp.Body.Close() return buf, err } @@ -107,16 +109,25 @@ func NewHTTPGetter(options ...Option) (Getter, error) { } func (g *HTTPGetter) httpClient() (*http.Client, error) { - transport := &http.Transport{ - DisableCompression: true, - Proxy: http.ProxyFromEnvironment, + if g.opts.transport != nil { + return &http.Client{ + Transport: g.opts.transport, + Timeout: g.opts.timeout, + }, nil } - if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" { - tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile) + + g.once.Do(func() { + g.transport = &http.Transport{ + DisableCompression: true, + Proxy: http.ProxyFromEnvironment, + } + }) + + if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" || g.opts.insecureSkipVerifyTLS { + tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile, g.opts.insecureSkipVerifyTLS) if err != nil { return nil, errors.Wrap(err, "can't create TLS config for client") } - tlsConf.BuildNameToCertificate() sni, err := urlutil.ExtractHostname(g.opts.url) if err != nil { @@ -124,21 +135,21 @@ func (g *HTTPGetter) httpClient() (*http.Client, error) { } tlsConf.ServerName = sni - transport.TLSClientConfig = tlsConf + g.transport.TLSClientConfig = tlsConf } if g.opts.insecureSkipVerifyTLS { - if transport.TLSClientConfig == nil { - transport.TLSClientConfig = &tls.Config{ + if g.transport.TLSClientConfig == nil { + g.transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } } else { - transport.TLSClientConfig.InsecureSkipVerify = true + g.transport.TLSClientConfig.InsecureSkipVerify = true } } client := &http.Client{ - Transport: transport, + Transport: g.transport, Timeout: g.opts.timeout, } diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index d823aec0d..c727d0d7c 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -50,6 +50,7 @@ func TestHTTPGetter(t *testing.T) { ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem") insecure := false timeout := time.Second * 5 + transport := &http.Transport{} // Test with options g, err = NewHTTPGetter( @@ -59,6 +60,7 @@ func TestHTTPGetter(t *testing.T) { WithTLSClientConfig(pub, priv, ca), WithInsecureSkipVerifyTLS(insecure), WithTimeout(timeout), + WithTransport(transport), ) if err != nil { t.Fatal(err) @@ -105,6 +107,10 @@ func TestHTTPGetter(t *testing.T) { t.Errorf("Expected NewHTTPGetter to contain %s as Timeout flag, got %s", timeout, hg.opts.timeout) } + if hg.opts.transport != transport { + t.Errorf("Expected NewHTTPGetter to contain %p as Transport, got %p", transport, hg.opts.transport) + } + // Test if setting insecureSkipVerifyTLS is being passed to the ops insecure = true @@ -149,9 +155,8 @@ func TestHTTPGetter(t *testing.T) { 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") + defaultUserAgent := version.GetUserAgent() if r.UserAgent() != defaultUserAgent { t.Errorf("Expected '%s', got '%s'", defaultUserAgent, r.UserAgent()) } @@ -173,6 +178,7 @@ func TestDownload(t *testing.T) { } // test with http server + const expectedUserAgent = "I am Groot" basicAuthSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if !ok || username != "username" || password != "password" { @@ -279,13 +285,13 @@ func TestDownload(t *testing.T) { 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") + insecureSkipTLSverify := false tlsSrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca) + tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca, insecureSkipTLSverify) 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() @@ -396,24 +402,24 @@ func TestHTTPGetterTarDownload(t *testing.T) { func TestHttpClientInsecureSkipVerify(t *testing.T) { g := HTTPGetter{} g.opts.url = "https://localhost" - verifyInsecureSkipVerify(t, g, "Blank HTTPGetter", false) + 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) + 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) + 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) + transport := verifyInsecureSkipVerify(t, &g, "HTTPGetter with 2 way ssl", true) if len(transport.TLSClientConfig.Certificates) <= 0 { t.Fatal("transport.TLSClientConfig.Certificates is not present") } @@ -422,17 +428,17 @@ func TestHttpClientInsecureSkipVerify(t *testing.T) { } } -func verifyInsecureSkipVerify(t *testing.T, g HTTPGetter, caseName string, expectedValue bool) *http.Transport { +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 { + if returnVal == nil { //nolint:staticcheck t.Fatalf("Expected non nil value for http client") } - transport := (returnVal.Transport).(*http.Transport) + transport := (returnVal.Transport).(*http.Transport) //nolint:staticcheck gotValue := false if transport.TLSClientConfig != nil { gotValue = transport.TLSClientConfig.InsecureSkipVerify @@ -443,3 +449,84 @@ func verifyInsecureSkipVerify(t *testing.T, g HTTPGetter, caseName string, expec } return transport } + +func TestDefaultHTTPTransportReuse(t *testing.T) { + g := HTTPGetter{} + + httpClient1, err := g.httpClient() + + if err != nil { + t.Fatal(err) + } + + if httpClient1 == nil { //nolint:staticcheck + t.Fatalf("Expected non nil value for http client") + } + + transport1 := (httpClient1.Transport).(*http.Transport) //nolint:staticcheck + + httpClient2, err := g.httpClient() + + if err != nil { + t.Fatal(err) + } + + if httpClient2 == nil { //nolint:staticcheck + t.Fatalf("Expected non nil value for http client") + } + + transport2 := (httpClient2.Transport).(*http.Transport) //nolint:staticcheck + + if transport1 != transport2 { + t.Fatalf("Expected default transport to be reused") + } +} + +func TestHTTPTransportOption(t *testing.T) { + transport := &http.Transport{} + + g := HTTPGetter{} + g.opts.transport = transport + httpClient1, err := g.httpClient() + + if err != nil { + t.Fatal(err) + } + + if httpClient1 == nil { //nolint:staticcheck + t.Fatalf("Expected non nil value for http client") + } + + transport1 := (httpClient1.Transport).(*http.Transport) //nolint:staticcheck + + if transport1 != transport { + t.Fatalf("Expected transport option to be applied") + } + + httpClient2, err := g.httpClient() + + if err != nil { + t.Fatal(err) + } + + if httpClient2 == nil { //nolint:staticcheck + t.Fatalf("Expected non nil value for http client") + } + + transport2 := (httpClient2.Transport).(*http.Transport) //nolint:staticcheck + + if transport1 != transport2 { + t.Fatalf("Expected applied transport to be reused") + } + + g = HTTPGetter{} + g.opts.url = "https://localhost" + g.opts.certFile = "testdata/client.crt" + g.opts.keyFile = "testdata/client.key" + g.opts.insecureSkipVerifyTLS = true + g.opts.transport = transport + usedTransport := verifyInsecureSkipVerify(t, &g, "HTTPGetter with 2 way ssl", false) + if usedTransport.TLSClientConfig != nil { + t.Fatal("transport.TLSClientConfig should not be set") + } +} diff --git a/pkg/getter/ocigetter.go b/pkg/getter/ocigetter.go index 3f85b9862..209786bd7 100644 --- a/pkg/getter/ocigetter.go +++ b/pkg/getter/ocigetter.go @@ -18,17 +18,25 @@ package getter import ( "bytes" "fmt" + "net" + "net/http" "strings" + "sync" + "time" - "helm.sh/helm/v3/internal/experimental/registry" + "helm.sh/helm/v3/internal/tlsutil" + "helm.sh/helm/v3/internal/urlutil" + "helm.sh/helm/v3/pkg/registry" ) // OCIGetter is the default HTTP(/S) backend handler type OCIGetter struct { - opts options + opts options + transport *http.Transport + once sync.Once } -//Get performs a Get from repo.Getter and returns the body. +// 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) @@ -38,37 +46,41 @@ func (g *OCIGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { 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) + // if the user has already provided a configured registry client, use it, + // this is particularly true when user has his own way of handling the client credentials. + if client == nil { + c, err := g.newRegistryClient() + if err != nil { + return nil, err + } + client = c } - r, err := registry.ParseReference(ref) - if err != nil { - return nil, err + ref := strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)) + + var pullOpts []registry.PullOption + requestingProv := strings.HasSuffix(ref, ".prov") + if requestingProv { + ref = strings.TrimSuffix(ref, ".prov") + pullOpts = append(pullOpts, + registry.PullOptWithChart(false), + registry.PullOptWithProv(true)) } - buf, err := client.PullChart(r) + result, err := client.Pull(ref, pullOpts...) if err != nil { return nil, err } - return buf, nil + if requestingProv { + return bytes.NewBuffer(result.Prov.Data), nil + } + return bytes.NewBuffer(result.Chart.Data), 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, - }, - } + var client OCIGetter for _, opt := range ops { opt(&client.opts) @@ -76,3 +88,68 @@ func NewOCIGetter(ops ...Option) (Getter, error) { return &client, nil } + +func (g *OCIGetter) newRegistryClient() (*registry.Client, error) { + if g.opts.transport != nil { + client, err := registry.NewClient( + registry.ClientOptHTTPClient(&http.Client{ + Transport: g.opts.transport, + Timeout: g.opts.timeout, + }), + ) + if err != nil { + return nil, err + } + return client, nil + } + + g.once.Do(func() { + g.transport = &http.Transport{ + // From https://github.com/google/go-containerregistry/blob/31786c6cbb82d6ec4fb8eb79cd9387905130534e/pkg/v1/remote/options.go#L87 + DisableCompression: true, + DialContext: (&net.Dialer{ + // By default we wrap the transport in retries, so reduce the + // default dial timeout to 5s to avoid 5x 30s of connection + // timeouts when doing the "ping" on certain http registries. + Timeout: 5 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + }) + + if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" || g.opts.insecureSkipVerifyTLS { + tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile, g.opts.insecureSkipVerifyTLS) + if err != nil { + return nil, fmt.Errorf("can't create TLS config for client: %w", err) + } + + sni, err := urlutil.ExtractHostname(g.opts.url) + if err != nil { + return nil, err + } + tlsConf.ServerName = sni + + g.transport.TLSClientConfig = tlsConf + } + + opts := []registry.ClientOption{registry.ClientOptHTTPClient(&http.Client{ + Transport: g.transport, + Timeout: g.opts.timeout, + })} + if g.opts.plainHTTP { + opts = append(opts, registry.ClientOptPlainHTTP()) + } + + client, err := registry.NewClient(opts...) + + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/getter/ocigetter_test.go b/pkg/getter/ocigetter_test.go index fc548b7a6..d0834d9fc 100644 --- a/pkg/getter/ocigetter_test.go +++ b/pkg/getter/ocigetter_test.go @@ -16,15 +16,136 @@ limitations under the License. package getter import ( + "net/http" + "path/filepath" "testing" + "time" + + "helm.sh/helm/v3/pkg/registry" ) -func TestNewOCIGetter(t *testing.T) { - testfn := func(ops *options) { - if ops.registryClient == nil { - t.Fatalf("the OCIGetter's registryClient should not be null") - } +func TestOCIGetter(t *testing.T) { + g, err := NewOCIGetter(WithURL("oci://example.com")) + if err != nil { + t.Fatal(err) + } + + if _, ok := g.(*OCIGetter); !ok { + t.Fatal("Expected NewOCIGetter to produce an *OCIGetter") + } + + cd := "../../testdata" + join := filepath.Join + ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem") + timeout := time.Second * 5 + transport := &http.Transport{} + insecureSkipVerifyTLS := false + plainHTTP := false + + // Test with options + g, err = NewOCIGetter( + WithBasicAuth("I", "Am"), + WithTLSClientConfig(pub, priv, ca), + WithTimeout(timeout), + WithTransport(transport), + WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS), + WithPlainHTTP(plainHTTP), + ) + if err != nil { + t.Fatal(err) + } + + og, ok := g.(*OCIGetter) + if !ok { + t.Fatal("expected NewOCIGetter to produce an *OCIGetter") + } + + if og.opts.username != "I" { + t.Errorf("Expected NewOCIGetter to contain %q as the username, got %q", "I", og.opts.username) + } + + if og.opts.password != "Am" { + t.Errorf("Expected NewOCIGetter to contain %q as the password, got %q", "Am", og.opts.password) + } + + if og.opts.certFile != pub { + t.Errorf("Expected NewOCIGetter to contain %q as the public key file, got %q", pub, og.opts.certFile) + } + + if og.opts.keyFile != priv { + t.Errorf("Expected NewOCIGetter to contain %q as the private key file, got %q", priv, og.opts.keyFile) + } + + if og.opts.caFile != ca { + t.Errorf("Expected NewOCIGetter to contain %q as the CA file, got %q", ca, og.opts.caFile) + } + + if og.opts.timeout != timeout { + t.Errorf("Expected NewOCIGetter to contain %s as Timeout flag, got %s", timeout, og.opts.timeout) + } + + if og.opts.transport != transport { + t.Errorf("Expected NewOCIGetter to contain %p as Transport, got %p", transport, og.opts.transport) + } + + if og.opts.plainHTTP != plainHTTP { + t.Errorf("Expected NewOCIGetter to have plainHTTP as %t, got %t", plainHTTP, og.opts.plainHTTP) } - NewOCIGetter(testfn) + if og.opts.insecureSkipVerifyTLS != insecureSkipVerifyTLS { + t.Errorf("Expected NewOCIGetter to have insecureSkipVerifyTLS as %t, got %t", insecureSkipVerifyTLS, og.opts.insecureSkipVerifyTLS) + } + + // Test if setting registryClient is being passed to the ops + registryClient, err := registry.NewClient() + if err != nil { + t.Fatal(err) + } + + g, err = NewOCIGetter( + WithRegistryClient(registryClient), + ) + if err != nil { + t.Fatal(err) + } + og, ok = g.(*OCIGetter) + if !ok { + t.Fatal("expected NewOCIGetter to produce an *OCIGetter") + } + + if og.opts.registryClient != registryClient { + t.Errorf("Expected NewOCIGetter to contain %p as RegistryClient, got %p", registryClient, og.opts.registryClient) + } +} + +func TestOCIHTTPTransportReuse(t *testing.T) { + g := OCIGetter{} + + _, err := g.newRegistryClient() + + if err != nil { + t.Fatal(err) + } + + if g.transport == nil { + t.Fatalf("Expected non nil value for transport") + } + + transport1 := g.transport + + _, err = g.newRegistryClient() + + if err != nil { + t.Fatal(err) + } + + if g.transport == nil { + t.Fatalf("Expected non nil value for transport") + } + + transport2 := g.transport + + if transport1 != transport2 { + t.Fatalf("Expected default transport to be reused") + } } diff --git a/pkg/helmpath/home_unix_test.go b/pkg/helmpath/home_unix_test.go index 6a72152c4..977002549 100644 --- a/pkg/helmpath/home_unix_test.go +++ b/pkg/helmpath/home_unix_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build !windows +//go:build !windows package helmpath diff --git a/pkg/helmpath/home_windows_test.go b/pkg/helmpath/home_windows_test.go index 796ced62c..073e6347f 100644 --- a/pkg/helmpath/home_windows_test.go +++ b/pkg/helmpath/home_windows_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build windows +//go:build windows package helmpath diff --git a/pkg/helmpath/lazypath_darwin.go b/pkg/helmpath/lazypath_darwin.go index e112b8337..eba6dde15 100644 --- a/pkg/helmpath/lazypath_darwin.go +++ b/pkg/helmpath/lazypath_darwin.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build darwin +//go:build darwin package helmpath diff --git a/pkg/helmpath/lazypath_darwin_test.go b/pkg/helmpath/lazypath_darwin_test.go index 9381a44e2..d0503e0e1 100644 --- a/pkg/helmpath/lazypath_darwin_test.go +++ b/pkg/helmpath/lazypath_darwin_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build darwin +//go:build darwin package helmpath diff --git a/pkg/helmpath/lazypath_unix.go b/pkg/helmpath/lazypath_unix.go index b4eae9f66..82fb4b6f1 100644 --- a/pkg/helmpath/lazypath_unix.go +++ b/pkg/helmpath/lazypath_unix.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build !windows,!darwin +//go:build !windows && !darwin package helmpath diff --git a/pkg/helmpath/lazypath_unix_test.go b/pkg/helmpath/lazypath_unix_test.go index 96d66e7a5..657982b2d 100644 --- a/pkg/helmpath/lazypath_unix_test.go +++ b/pkg/helmpath/lazypath_unix_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build !windows,!darwin +//go:build !windows && !darwin package helmpath diff --git a/pkg/helmpath/lazypath_windows.go b/pkg/helmpath/lazypath_windows.go index 057a3af14..230aee2a9 100644 --- a/pkg/helmpath/lazypath_windows.go +++ b/pkg/helmpath/lazypath_windows.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build windows +//go:build windows package helmpath diff --git a/pkg/helmpath/lazypath_windows_test.go b/pkg/helmpath/lazypath_windows_test.go index 866e7b9d9..dedfd5720 100644 --- a/pkg/helmpath/lazypath_windows_test.go +++ b/pkg/helmpath/lazypath_windows_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build windows +//go:build windows package helmpath diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 2565d1832..0772678d1 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -17,10 +17,14 @@ limitations under the License. package kube // import "helm.sh/helm/v3/pkg/kube" import ( + "bytes" "context" "encoding/json" "fmt" "io" + "os" + "path/filepath" + "reflect" "strings" "sync" "time" @@ -33,10 +37,13 @@ import ( apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" + multierror "github.com/hashicorp/go-multierror" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "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" @@ -45,6 +52,7 @@ import ( "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" cachetools "k8s.io/client-go/tools/cache" watchtools "k8s.io/client-go/tools/watch" cmdutil "k8s.io/kubectl/pkg/cmd/util" @@ -55,8 +63,20 @@ var ErrNoObjectsVisited = errors.New("no objects visited") var metadataAccessor = meta.NewAccessor() +// ManagedFieldsManager is the name of the manager of Kubernetes managedFields +// first introduced in Kubernetes 1.18 +var ManagedFieldsManager string + // Client represents a client capable of communicating with the Kubernetes API. type Client struct { + // Factory provides a minimal version of the kubectl Factory interface. If + // you need the full Factory you can type switch to the full interface. + // Since Kubernetes Go API does not provide backwards compatibility across + // minor versions, this API does not follow Helm backwards compatibility. + // Helm is exposing Kubernetes in this property and cannot guarantee this + // will not change. The minimal interface only has the functions that Helm + // needs. The smaller surface area of the interface means there is a lower + // chance of it changing. Factory Factory Log func(string, ...interface{}) // Namespace allows to bypass the kubeconfig file for the choice of the namespace @@ -100,7 +120,7 @@ func (c *Client) getKubeClient() (*kubernetes.Clientset, error) { return c.kubeClient, err } -// IsReachable tests connectivity to the cluster +// IsReachable tests connectivity to the cluster. func (c *Client) IsReachable() error { client, err := c.getKubeClient() if err == genericclioptions.ErrEmptyConfig { @@ -126,7 +146,142 @@ func (c *Client) Create(resources ResourceList) (*Result, error) { return &Result{Created: resources}, nil } -// Wait up to the given timeout for the specified resources to be ready +func transformRequests(req *rest.Request) { + tableParam := strings.Join([]string{ + fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName), + fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName), + "application/json", + }, ",") + req.SetHeader("Accept", tableParam) + + // if sorting, ensure we receive the full object in order to introspect its fields via jsonpath + req.Param("includeObject", "Object") +} + +// Get retrieves the resource objects supplied. If related is set to true the +// related pods are fetched as well. If the passed in resources are a table kind +// the related resources will also be fetched as kind=table. +func (c *Client) Get(resources ResourceList, related bool) (map[string][]runtime.Object, error) { + buf := new(bytes.Buffer) + objs := make(map[string][]runtime.Object) + + podSelectors := []map[string]string{} + err := resources.Visit(func(info *resource.Info, err error) error { + if err != nil { + return err + } + + gvk := info.ResourceMapping().GroupVersionKind + vk := gvk.Version + "/" + gvk.Kind + obj, err := getResource(info) + if err != nil { + fmt.Fprintf(buf, "Get resource %s failed, err:%v\n", info.Name, err) + } else { + objs[vk] = append(objs[vk], obj) + + // Only fetch related pods if they are requested + if related { + // Discover if the existing object is a table. If it is, request + // the pods as Tables. Otherwise request them normally. + objGVK := obj.GetObjectKind().GroupVersionKind() + var isTable bool + if objGVK.Kind == "Table" { + isTable = true + } + + objs, err = c.getSelectRelationPod(info, objs, isTable, &podSelectors) + if err != nil { + c.Log("Warning: get the relation pod is failed, err:%s", err.Error()) + } + } + } + + return nil + }) + if err != nil { + return nil, err + } + + return objs, nil +} + +func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]runtime.Object, table bool, podSelectors *[]map[string]string) (map[string][]runtime.Object, error) { + if info == nil { + return objs, nil + } + c.Log("get relation pod of object: %s/%s/%s", info.Namespace, info.Mapping.GroupVersionKind.Kind, info.Name) + selector, ok, _ := getSelectorFromObject(info.Object) + if !ok { + return objs, nil + } + + for index := range *podSelectors { + if reflect.DeepEqual((*podSelectors)[index], selector) { + // check if pods for selectors are already added. This avoids duplicate printing of pods + return objs, nil + } + } + + *podSelectors = append(*podSelectors, selector) + + var infos []*resource.Info + var err error + if table { + infos, err = c.Factory.NewBuilder(). + Unstructured(). + ContinueOnError(). + NamespaceParam(info.Namespace). + DefaultNamespace(). + ResourceTypes("pods"). + LabelSelector(labels.Set(selector).AsSelector().String()). + TransformRequests(transformRequests). + Do().Infos() + if err != nil { + return objs, err + } + } else { + infos, err = c.Factory.NewBuilder(). + Unstructured(). + ContinueOnError(). + NamespaceParam(info.Namespace). + DefaultNamespace(). + ResourceTypes("pods"). + LabelSelector(labels.Set(selector).AsSelector().String()). + Do().Infos() + if err != nil { + return objs, err + } + } + vk := "v1/Pod(related)" + + for _, info := range infos { + objs[vk] = append(objs[vk], info.Object) + } + return objs, nil +} + +func getSelectorFromObject(obj runtime.Object) (map[string]string, bool, error) { + typed := obj.(*unstructured.Unstructured) + kind := typed.Object["kind"] + switch kind { + case "ReplicaSet", "Deployment", "StatefulSet", "DaemonSet", "Job": + return unstructured.NestedStringMap(typed.Object, "spec", "selector", "matchLabels") + case "ReplicationController": + return unstructured.NestedStringMap(typed.Object, "spec", "selector") + default: + return nil, false, nil + } +} + +func getResource(info *resource.Info) (runtime.Object, error) { + obj, err := resource.NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name) + if err != nil { + return nil, err + } + return obj, nil +} + +// Wait waits 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 { @@ -156,6 +311,15 @@ func (c *Client) WaitWithJobs(resources ResourceList, timeout time.Duration) err return w.waitForResources(resources) } +// WaitForDelete wait up to the given timeout for the specified resources to be deleted. +func (c *Client) WaitForDelete(resources ResourceList, timeout time.Duration) error { + w := waiter{ + log: c.Log, + timeout: timeout, + } + return w.waitForDeletedResources(resources) +} + func (c *Client) namespace() string { if c.Namespace != "" { return c.Namespace @@ -177,7 +341,32 @@ func (c *Client) newBuilder() *resource.Builder { // 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) + validationDirective := metav1.FieldValidationIgnore + if validate { + validationDirective = metav1.FieldValidationStrict + } + + schema, err := c.Factory.Validator(validationDirective) + if err != nil { + return nil, err + } + result, err := c.newBuilder(). + Unstructured(). + Schema(schema). + Stream(reader, ""). + Do().Infos() + return result, scrubValidationError(err) +} + +// BuildTable validates for Kubernetes objects and returns unstructured infos. +// The returned kind is a Table. +func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, error) { + validationDirective := metav1.FieldValidationIgnore + if validate { + validationDirective = metav1.FieldValidationStrict + } + + schema, err := c.Factory.Validator(validationDirective) if err != nil { return nil, err } @@ -185,6 +374,7 @@ func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) { Unstructured(). Schema(schema). Stream(reader, ""). + TransformRequests(transformRequests). Do().Infos() return result, scrubValidationError(err) } @@ -206,7 +396,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err return err } - helper := resource.NewHelper(info.Client, info.Mapping) + helper := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()) 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") @@ -249,7 +439,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err } for _, info := range original.Difference(target) { - c.Log("Deleting %q in %s...", info.Name, info.Namespace) + c.Log("Deleting %s %q in namespace %s...", info.Mapping.GroupVersionKind.Kind, info.Name, info.Namespace) if err := info.Get(); err != nil { c.Log("Unable to get obj %q, err: %s", info.Name, err) @@ -263,7 +453,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy) continue } - if err := deleteResource(info); err != nil { + if err := deleteResource(info, metav1.DeletePropagationBackground); err != nil { c.Log("Failed to delete %q, err: %s", info.ObjectName(), err) continue } @@ -272,33 +462,47 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err return res, nil } -// 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. +// Delete deletes Kubernetes resources specified in the resources list with +// background cascade deletion. 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) { + return delete(c, resources, metav1.DeletePropagationBackground) +} + +// Delete deletes Kubernetes resources specified in the resources list with +// given deletion propagation policy. 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) DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) { + return delete(c, resources, policy) +} + +func delete(c *Client, resources ResourceList, propagation metav1.DeletionPropagation) (*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) - if err := c.skipIfNotFound(deleteResource(info)); err != nil { - mtx.Lock() - defer mtx.Unlock() - // Collect the error and continue on - errs = append(errs, err) - } else { + err := deleteResource(info, propagation) + if err == nil || apierrors.IsNotFound(err) { + if err != nil { + c.Log("Ignoring delete failure for %q %s: %v", info.Name, info.Mapping.GroupVersionKind, err) + } mtx.Lock() defer mtx.Unlock() res.Deleted = append(res.Deleted, info) + return nil } + mtx.Lock() + defer mtx.Unlock() + // Collect the error and continue on + errs = append(errs, err) 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") + if errors.Is(err, ErrNoObjectsVisited) { + err = fmt.Errorf("object not found, skipping delete: %w", err) } errs = append(errs, err) } @@ -308,14 +512,6 @@ func (c *Client) Delete(resources ResourceList) (*Result, []error) { return res, nil } -func (c *Client) skipIfNotFound(err error) error { - if apierrors.IsNotFound(err) { - c.Log("%v", err) - return nil - } - return err -} - func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error { return func(info *resource.Info) error { return c.watchUntilReady(t, info) @@ -324,16 +520,16 @@ func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error { // WatchUntilReady watches the resources given and waits until it is ready. // -// This function is mainly for hook implementations. It watches for a resource to +// This method is mainly for hook implementations. It watches for a resource to // hit a particular milestone. The milestone depends on the Kind. // // For most kinds, it checks to see if the resource is marked as Added or Modified // by the Kubernetes event stream. For some kinds, it does more: // -// - Jobs: A job is marked "Ready" when it has successfully completed. This is -// ascertained by watching the Status fields in a job's output. -// - 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. +// - 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(resources ResourceList, timeout time.Duration) error { @@ -343,6 +539,8 @@ func (c *Client) WatchUntilReady(resources ResourceList, timeout time.Duration) } func perform(infos ResourceList, fn func(*resource.Info) error) error { + var result error + if len(infos) == 0 { return ErrNoObjectsVisited } @@ -353,10 +551,31 @@ func perform(infos ResourceList, fn func(*resource.Info) error) error { for range infos { err := <-errs if err != nil { - return err + result = multierror.Append(result, err) } } - return nil + + return result +} + +// getManagedFieldsManager returns the manager string. If one was set it will be returned. +// Otherwise, one is calculated based on the name of the binary. +func getManagedFieldsManager() string { + + // When a manager is explicitly set use it + if ManagedFieldsManager != "" { + return ManagedFieldsManager + } + + // When no manager is set and no calling application can be found it is unknown + if len(os.Args[0]) == 0 { + return "unknown" + } + + // When there is an application that can be determined and no set manager + // use the base name. This is one of the ways Kubernetes libs handle figuring + // names out. + return filepath.Base(os.Args[0]) } func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<- error) { @@ -377,17 +596,16 @@ func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<- } func createResource(info *resource.Info) error { - obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) + obj, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).Create(info.Namespace, true, info.Object) if err != nil { return err } return info.Refresh(obj, true) } -func deleteResource(info *resource.Info) error { - policy := metav1.DeletePropagationBackground +func deleteResource(info *resource.Info, policy metav1.DeletionPropagation) error { opts := &metav1.DeleteOptions{PropagationPolicy: &policy} - _, err := resource.NewHelper(info.Client, info.Mapping).DeleteWithOptions(info.Namespace, info.Name, opts) + _, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts) return err } @@ -402,7 +620,7 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P } // Fetch the current object for the three way merge - helper := resource.NewHelper(target.Client, target.Mapping) + helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager()) 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) @@ -444,7 +662,7 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool) error { var ( obj runtime.Object - helper = resource.NewHelper(target.Client, target.Mapping) + helper = resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager()) kind = target.Mapping.GroupVersionKind.Kind ) @@ -463,7 +681,7 @@ func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, } if patch == nil || string(patch) == "{}" { - c.Log("Looks like there are no changes for %s %q", target.Mapping.GroupVersionKind.Kind, target.Name) + c.Log("Looks like there are no changes for %s %q", 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 { @@ -472,6 +690,7 @@ func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, return nil } // send patch to server + c.Log("Patch %s %q in namespace %s", kind, target.Name, target.Namespace) 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) @@ -610,6 +829,9 @@ func (c *Client) WaitAndGetCompletedPodPhase(name string, timeout time.Duration) FieldSelector: fmt.Sprintf("metadata.name=%s", name), TimeoutSeconds: &to, }) + if err != nil { + return v1.PodUnknown, err + } for event := range watcher.ResultChan() { p, ok := event.Object.(*v1.Pod) diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index de5358aee..55aa5d8ed 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -19,7 +19,6 @@ package kube import ( "bytes" "io" - "io/ioutil" "net/http" "strings" "testing" @@ -37,7 +36,7 @@ var unstructuredSerializer = resource.UnstructuredPlusDefaultContentConfig().Neg var codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) func objBody(obj runtime.Object) io.ReadCloser { - return ioutil.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) + return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) } func newPod(name string) v1.Pod { @@ -87,7 +86,7 @@ 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(codec, obj)))) + body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) return &http.Response{StatusCode: code, Header: header, Body: body}, nil } @@ -123,7 +122,7 @@ func TestUpdate(t *testing.T) { 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) + data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("could not dump request: %s", err) } @@ -136,7 +135,7 @@ func TestUpdate(t *testing.T) { case p == "/namespaces/default/pods/dolphin" && m == "GET": return newResponse(404, notFoundBody()) case p == "/namespaces/default/pods/starfish" && m == "PATCH": - data, err := ioutil.ReadAll(req.Body) + data, err := io.ReadAll(req.Body) if err != nil { t.Fatalf("could not dump request: %s", err) } @@ -253,6 +252,45 @@ func TestBuild(t *testing.T) { } } +func TestBuildTable(t *testing.T) { + tests := []struct { + name string + namespace string + reader io.Reader + count int + err bool + }{ + { + name: "Valid input", + namespace: "test", + reader: strings.NewReader(guestbookManifest), + count: 6, + }, { + name: "Valid input, deploying resources into different namespaces", + namespace: "test", + reader: strings.NewReader(namespacedGuestbookManifest), + count: 1, + }, + } + + c := newTestClient(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test for an invalid manifest + infos, err := c.BuildTable(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") { + t.Error("error message was not scrubbed") + } + + if len(infos) != tt.count { + t.Errorf("expected %d result objects, got %d", tt.count, len(infos)) + } + }) + } +} + func TestPerform(t *testing.T) { tests := []struct { name string @@ -402,7 +440,7 @@ spec: spec: containers: - name: master - image: k8s.gcr.io/redis:e2e # or just image: redis + image: registry.k8s.io/redis:e2e # or just image: redis resources: requests: cpu: 100m diff --git a/pkg/kube/factory.go b/pkg/kube/factory.go index f47f9d9f6..f19d62dc3 100644 --- a/pkg/kube/factory.go +++ b/pkg/kube/factory.go @@ -18,6 +18,7 @@ package kube // import "helm.sh/helm/v3/pkg/kube" import ( "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "k8s.io/kubectl/pkg/validation" @@ -25,14 +26,26 @@ import ( // Factory provides abstractions that allow the Kubectl command to be extended across multiple types // of resources and different API sets. +// This interface is a minimal copy of the kubectl Factory interface containing only the functions +// needed by Helm. Since Kubernetes Go APIs, including interfaces, can change in any minor release +// this interface is not covered by the Helm backwards compatibility guarantee. The reasons for the +// minimal copy is that it does not include the full interface. Changes or additions to functions +// Helm does not need are not impacted or exposed. This minimizes the impact of Kubernetes changes +// being exposed. type Factory interface { // ToRawKubeConfigLoader return kubeconfig loader as-is ToRawKubeConfigLoader() clientcmd.ClientConfig + + // DynamicClient returns a dynamic client ready for use + DynamicClient() (dynamic.Interface, error) + // 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) + Validator(validationDirective string) (validation.Schema, error) } diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go index ff800864c..267020d57 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/fake.go @@ -22,6 +22,8 @@ import ( "time" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/resource" "helm.sh/helm/v3/pkg/kube" @@ -33,13 +35,18 @@ import ( type FailingKubeClient struct { PrintingKubeClient CreateError error + GetError error WaitError error DeleteError error + DeleteWithPropagationError error WatchUntilReadyError error UpdateError error BuildError error + BuildTableError error + BuildDummy bool BuildUnstructuredError error WaitAndGetCompletedPodPhaseError error + WaitDuration time.Duration } // Create returns the configured error if set or prints @@ -50,8 +57,17 @@ func (f *FailingKubeClient) Create(resources kube.ResourceList) (*kube.Result, e return f.PrintingKubeClient.Create(resources) } -// Wait returns the configured error if set or prints +// Get returns the configured error if set or prints +func (f *FailingKubeClient) Get(resources kube.ResourceList, related bool) (map[string][]runtime.Object, error) { + if f.GetError != nil { + return nil, f.GetError + } + return f.PrintingKubeClient.Get(resources, related) +} + +// Waits the amount of time defined on f.WaitDuration, then returns the configured error if set or prints. func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error { + time.Sleep(f.WaitDuration) if f.WaitError != nil { return f.WaitError } @@ -63,7 +79,15 @@ func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Dur if f.WaitError != nil { return f.WaitError } - return f.PrintingKubeClient.Wait(resources, d) + return f.PrintingKubeClient.WaitWithJobs(resources, d) +} + +// WaitForDelete returns the configured error if set or prints +func (f *FailingKubeClient) WaitForDelete(resources kube.ResourceList, d time.Duration) error { + if f.WaitError != nil { + return f.WaitError + } + return f.PrintingKubeClient.WaitForDelete(resources, d) } // Delete returns the configured error if set or prints @@ -95,9 +119,20 @@ func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error if f.BuildError != nil { return []*resource.Info{}, f.BuildError } + if f.BuildDummy { + return createDummyResourceList(), nil + } return f.PrintingKubeClient.Build(r, false) } +// BuildTable returns the configured error if set or prints +func (f *FailingKubeClient) BuildTable(r io.Reader, _ bool) (kube.ResourceList, error) { + if f.BuildTableError != nil { + return []*resource.Info{}, f.BuildTableError + } + return f.PrintingKubeClient.BuildTable(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 { @@ -105,3 +140,21 @@ func (f *FailingKubeClient) WaitAndGetCompletedPodPhase(s string, d time.Duratio } return f.PrintingKubeClient.WaitAndGetCompletedPodPhase(s, d) } + +// DeleteWithPropagationPolicy returns the configured error if set or prints +func (f *FailingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) { + if f.DeleteWithPropagationError != nil { + return nil, []error{f.DeleteWithPropagationError} + } + return f.PrintingKubeClient.DeleteWithPropagationPolicy(resources, policy) +} + +func createDummyResourceList() kube.ResourceList { + var resInfo resource.Info + resInfo.Name = "dummyName" + resInfo.Namespace = "dummyNamespace" + var resourceList kube.ResourceList + resourceList.Append(&resInfo) + return resourceList + +} diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go index e8bd1845b..e6c4b6207 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -22,6 +22,8 @@ import ( "time" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/resource" "helm.sh/helm/v3/pkg/kube" @@ -47,6 +49,14 @@ func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result, return &kube.Result{Created: resources}, nil } +func (p *PrintingKubeClient) Get(resources kube.ResourceList, related bool) (map[string][]runtime.Object, error) { + _, err := io.Copy(p.Out, bufferize(resources)) + if err != nil { + return nil, err + } + return make(map[string][]runtime.Object), nil +} + func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error { _, err := io.Copy(p.Out, bufferize(resources)) return err @@ -57,6 +67,11 @@ func (p *PrintingKubeClient) WaitWithJobs(resources kube.ResourceList, _ time.Du return err } +func (p *PrintingKubeClient) WaitForDelete(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. @@ -91,11 +106,27 @@ func (p *PrintingKubeClient) Build(_ io.Reader, _ bool) (kube.ResourceList, erro return []*resource.Info{}, nil } +// BuildTable implements KubeClient BuildTable. +func (p *PrintingKubeClient) BuildTable(_ 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 } +// DeleteWithPropagationPolicy implements KubeClient delete. +// +// It only prints out the content to be deleted. +func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) { + _, err := io.Copy(p.Out, bufferize(resources)) + if err != nil { + return nil, []error{err} + } + return &kube.Result{Deleted: resources}, nil +} + func bufferize(resources kube.ResourceList) io.Reader { var builder strings.Builder for _, info := range resources { diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go index 545985996..ce42ed950 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -21,6 +21,8 @@ import ( "time" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) // Interface represents a client capable of communicating with the Kubernetes API. @@ -30,14 +32,19 @@ type Interface interface { // Create creates one or more resources. Create(resources ResourceList) (*Result, error) + // Wait waits up to the given timeout for the specified resources to be ready. Wait(resources ResourceList, timeout time.Duration) error + // WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs. 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 + // WatchUntilReady watches the resources given and waits until it is ready. + // + // This method is mainly for hook implementations. It watches for a resource to + // hit a particular milestone. The milestone depends on the Kind. // // For Jobs, "ready" means the Job ran to completion (exited without error). // For Pods, "ready" means the Pod phase is marked "succeeded". @@ -49,9 +56,9 @@ type Interface interface { // if it doesn't exist. Update(original, target ResourceList, force bool) (*Result, error) - // Build creates a resource list from a Reader + // Build creates a resource list from a Reader. // - // reader must contain a YAML stream (one or more YAML documents separated + // Reader must contain a YAML stream (one or more YAML documents separated // by "\n---\n") // // Validates against OpenAPI schema if validate is true. @@ -61,8 +68,49 @@ type Interface interface { // 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 checks whether the client is able to connect to the cluster. IsReachable() error } +// InterfaceExt is introduced to avoid breaking backwards compatibility for Interface implementers. +// +// TODO Helm 4: Remove InterfaceExt and integrate its method(s) into the Interface. +type InterfaceExt interface { + // WaitForDelete wait up to the given timeout for the specified resources to be deleted. + WaitForDelete(resources ResourceList, timeout time.Duration) error +} + +// InterfaceDeletionPropagation is introduced to avoid breaking backwards compatibility for Interface implementers. +// +// TODO Helm 4: Remove InterfaceDeletionPropagation and integrate its method(s) into the Interface. +type InterfaceDeletionPropagation interface { + // Delete destroys one or more resources. The deletion propagation is handled as per the given deletion propagation value. + DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) +} + +// InterfaceResources is introduced to avoid breaking backwards compatibility for Interface implementers. +// +// TODO Helm 4: Remove InterfaceResources and integrate its method(s) into the Interface. +type InterfaceResources interface { + // Get details of deployed resources. + // The first argument is a list of resources to get. The second argument + // specifies if related pods should be fetched. For example, the pods being + // managed by a deployment. + Get(resources ResourceList, related bool) (map[string][]runtime.Object, error) + + // BuildTable creates a resource list from a Reader. This differs from + // Interface.Build() in that a table kind is returned. A table is useful + // if you want to use a printer to display the information. + // + // Reader must contain a YAML stream (one or more YAML documents separated + // by "\n---\n") + // + // Validates against OpenAPI schema if validate is true. + // TODO Helm 4: Integrate into Build with an argument + BuildTable(reader io.Reader, validate bool) (ResourceList, error) +} + var _ Interface = (*Client)(nil) +var _ InterfaceExt = (*Client)(nil) +var _ InterfaceDeletionPropagation = (*Client)(nil) +var _ InterfaceResources = (*Client)(nil) diff --git a/pkg/kube/ready.go b/pkg/kube/ready.go index 19b93e386..7172a42bc 100644 --- a/pkg/kube/ready.go +++ b/pkg/kube/ready.go @@ -18,6 +18,7 @@ package kube // import "helm.sh/helm/v3/pkg/kube" import ( "context" + "fmt" appsv1 "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" @@ -83,8 +84,8 @@ type ReadyChecker struct { // IsReady checks if v is ready. It supports checking readiness for pods, // deployments, persistent volume claims, services, daemon sets, custom -// resource definitions, stateful sets, replication controllers, and replica -// sets. All other resource kinds are always considered ready. +// resource definitions, stateful sets, replication controllers, jobs (optional), +// and replica sets. All other resource kinds are always considered ready. // // IsReady will fetch the latest state of the object from the server prior to // performing readiness checks, and it will return any error encountered. @@ -105,9 +106,11 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err case *batchv1.Job: if c.checkJobs { job, err := c.client.BatchV1().Jobs(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{}) - if err != nil || !c.jobReady(job) { + if err != nil { return false, err } + ready, err := c.jobReady(job) + return ready, err } case *appsv1.Deployment, *appsv1beta1.Deployment, *appsv1beta2.Deployment, *extensionsv1beta1.Deployment: currentDeployment, err := c.client.AppsV1().Deployments(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{}) @@ -222,16 +225,17 @@ func (c *ReadyChecker) isPodReady(pod *corev1.Pod) bool { return false } -func (c *ReadyChecker) jobReady(job *batchv1.Job) bool { +func (c *ReadyChecker) jobReady(job *batchv1.Job) (bool, error) { if job.Status.Failed > *job.Spec.BackoffLimit { c.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName()) - return false + // If a job is failed, it can't recover, so throw an error + return false, fmt.Errorf("job is failed: %s/%s", job.GetNamespace(), job.GetName()) } - if job.Status.Succeeded < *job.Spec.Completions { + if job.Spec.Completions != nil && job.Status.Succeeded < *job.Spec.Completions { c.log("Job is not completed: %s/%s", job.GetNamespace(), job.GetName()) - return false + return false, nil } - return true + return true, nil } func (c *ReadyChecker) serviceReady(s *corev1.Service) bool { @@ -291,7 +295,7 @@ func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool { c.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) + maxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(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 @@ -353,9 +357,16 @@ func (c *ReadyChecker) crdReady(crd apiextv1.CustomResourceDefinition) bool { func (c *ReadyChecker) 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 { + c.log("StatefulSet skipped ready check: %s/%s. updateStrategy is %v", sts.Namespace, sts.Name, sts.Spec.UpdateStrategy.Type) return true } + // Make sure the status is up-to-date with the StatefulSet changes + if sts.Status.ObservedGeneration < sts.Generation { + c.log("StatefulSet is not ready: %s/%s. update has not yet been observed", sts.Namespace, sts.Name) + return false + } + // Dereference all the pointers because StatefulSets like them var partition int // 1 is the default for replicas if not set @@ -377,7 +388,7 @@ func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool { expectedReplicas := replicas - partition // Make sure all the updated pods have been scheduled - if int(sts.Status.UpdatedReplicas) != expectedReplicas { + if int(sts.Status.UpdatedReplicas) < expectedReplicas { c.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 } @@ -386,6 +397,15 @@ func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool { c.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 } + // This check only makes sense when all partitions are being upgraded otherwise during a + // partioned rolling upgrade, this condition will never evaluate to true, leading to + // error. + if partition == 0 && sts.Status.CurrentRevision != sts.Status.UpdateRevision { + c.log("StatefulSet is not ready: %s/%s. currentRevision %s does not yet match updateRevision %s", sts.Namespace, sts.Name, sts.Status.CurrentRevision, sts.Status.UpdateRevision) + return false + } + + c.log("StatefulSet is ready: %s/%s. %d out of %d expected pods are ready", sts.Namespace, sts.Name, sts.Status.ReadyReplicas, replicas) return true } diff --git a/pkg/kube/ready_test.go b/pkg/kube/ready_test.go index 8fd20bd9f..e8e71d8aa 100644 --- a/pkg/kube/ready_test.go +++ b/pkg/kube/ready_test.go @@ -4,7 +4,6 @@ Copyright The 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 @@ -157,10 +156,45 @@ func Test_ReadyChecker_statefulSetReady(t *testing.T) { { name: "statefulset is not ready when partition is set", args: args{ - sts: newStatefulSet("foo", 1, 1, 1, 1), + sts: newStatefulSet("foo", 2, 1, 1, 0), + }, + want: false, + }, + { + name: "statefulset is ready when partition is set and no change in template", + args: args{ + sts: newStatefulSet("foo", 2, 1, 2, 2), + }, + want: true, + }, + { + name: "statefulset is ready when partition is greater than replicas", + args: args{ + sts: newStatefulSet("foo", 1, 2, 1, 1), + }, + want: true, + }, + { + name: "statefulset is not ready when status of latest generation has not yet been observed", + args: args{ + sts: newStatefulSetWithNewGeneration("foo", 1, 0, 1, 1), }, want: false, }, + { + name: "statefulset is not ready when current revision for current replicas does not match update revision for updated replicas", + args: args{ + sts: newStatefulSetWithUpdateRevision("foo", 1, 0, 1, 1, "foo-bbbbbbb"), + }, + want: false, + }, + { + name: "statefulset is ready when current revision for current replicas does not match update revision for updated replicas when using partition !=0", + args: args{ + sts: newStatefulSetWithUpdateRevision("foo", 3, 2, 3, 3, "foo-bbbbbbb"), + }, + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -235,50 +269,68 @@ func Test_ReadyChecker_jobReady(t *testing.T) { job *batchv1.Job } tests := []struct { - name string - args args - want bool + name string + args args + want bool + wantErr bool }{ { - name: "job is completed", - args: args{job: newJob("foo", 1, 1, 1, 0)}, - want: true, + name: "job is completed", + args: args{job: newJob("foo", 1, intToInt32(1), 1, 0)}, + want: true, + wantErr: false, }, { - name: "job is incomplete", - args: args{job: newJob("foo", 1, 1, 0, 0)}, - want: false, + name: "job is incomplete", + args: args{job: newJob("foo", 1, intToInt32(1), 0, 0)}, + want: false, + wantErr: false, }, { - name: "job is failed", - args: args{job: newJob("foo", 1, 1, 0, 1)}, - want: false, + name: "job is failed but within BackoffLimit", + args: args{job: newJob("foo", 1, intToInt32(1), 0, 1)}, + want: false, + wantErr: false, }, { - name: "job is completed with retry", - args: args{job: newJob("foo", 1, 1, 1, 1)}, - want: true, + name: "job is completed with retry", + args: args{job: newJob("foo", 1, intToInt32(1), 1, 1)}, + want: true, + wantErr: false, }, { - name: "job is failed with retry", - args: args{job: newJob("foo", 1, 1, 0, 2)}, - want: false, + name: "job is failed and beyond BackoffLimit", + args: args{job: newJob("foo", 1, intToInt32(1), 0, 2)}, + want: false, + wantErr: true, }, { - name: "job is completed single run", - args: args{job: newJob("foo", 0, 1, 1, 0)}, - want: true, + name: "job is completed single run", + args: args{job: newJob("foo", 0, intToInt32(1), 1, 0)}, + want: true, + wantErr: false, }, { - name: "job is failed single run", - args: args{job: newJob("foo", 0, 1, 0, 1)}, - want: false, + name: "job is failed single run", + args: args{job: newJob("foo", 0, intToInt32(1), 0, 1)}, + want: false, + wantErr: true, + }, + { + name: "job with null completions", + args: args{job: newJob("foo", 0, nil, 1, 0)}, + want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := NewReadyChecker(fake.NewSimpleClientset(), nil) - if got := c.jobReady(tt.args.job); got != tt.want { + got, err := c.jobReady(tt.args.job) + if (err != nil) != tt.wantErr { + t.Errorf("jobReady() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { t.Errorf("jobReady() = %v, want %v", got, tt.want) } }) @@ -358,8 +410,9 @@ func newDaemonSet(name string, maxUnavailable, numberReady, desiredNumberSchedul func newStatefulSet(name string, replicas, partition, readyReplicas, updatedReplicas int) *appsv1.StatefulSet { return &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: defaultNamespace, + Name: name, + Namespace: defaultNamespace, + Generation: int64(1), }, Spec: appsv1.StatefulSetSpec{ UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ @@ -385,12 +438,27 @@ func newStatefulSet(name string, replicas, partition, readyReplicas, updatedRepl }, }, Status: appsv1.StatefulSetStatus{ - UpdatedReplicas: int32(updatedReplicas), - ReadyReplicas: int32(readyReplicas), + ObservedGeneration: int64(1), + CurrentRevision: name + "-aaaaaaa", + UpdateRevision: name + "-aaaaaaa", + UpdatedReplicas: int32(updatedReplicas), + ReadyReplicas: int32(readyReplicas), }, } } +func newStatefulSetWithNewGeneration(name string, replicas, partition, readyReplicas, updatedReplicas int) *appsv1.StatefulSet { + ss := newStatefulSet(name, replicas, partition, readyReplicas, updatedReplicas) + ss.Generation++ + return ss +} + +func newStatefulSetWithUpdateRevision(name string, replicas, partition, readyReplicas, updatedReplicas int, updateRevision string) *appsv1.StatefulSet { + ss := newStatefulSet(name, replicas, partition, readyReplicas, updatedReplicas) + ss.Status.UpdateRevision = updateRevision + return ss +} + func newDeployment(name string, replicas, maxSurge, maxUnavailable int) *appsv1.Deployment { return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -481,7 +549,7 @@ func newPersistentVolumeClaim(name string, phase corev1.PersistentVolumeClaimPha } } -func newJob(name string, backoffLimit, completions, succeeded, failed int) *batchv1.Job { +func newJob(name string, backoffLimit int, completions *int32, succeeded int, failed int) *batchv1.Job { return &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -489,7 +557,7 @@ func newJob(name string, backoffLimit, completions, succeeded, failed int) *batc }, Spec: batchv1.JobSpec{ BackoffLimit: intToInt32(backoffLimit), - Completions: intToInt32(completions), + Completions: completions, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: name, diff --git a/pkg/kube/resource_policy.go b/pkg/kube/resource_policy.go index 5f391eb50..46b8680dd 100644 --- a/pkg/kube/resource_policy.go +++ b/pkg/kube/resource_policy.go @@ -22,5 +22,6 @@ 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. +// +// during an uninstallRelease action. const KeepPolicy = "keep" diff --git a/pkg/kube/wait.go b/pkg/kube/wait.go index f9ea2ea85..ecdd38940 100644 --- a/pkg/kube/wait.go +++ b/pkg/kube/wait.go @@ -28,6 +28,7 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -49,7 +50,7 @@ func (w *waiter) waitForResources(created ResourceList) error { ctx, cancel := context.WithTimeout(context.Background(), w.timeout) defer cancel() - return wait.PollImmediateUntil(2*time.Second, func() (bool, error) { + return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { for _, v := range created { ready, err := w.c.IsReady(ctx, v) if !ready || err != nil { @@ -57,7 +58,25 @@ func (w *waiter) waitForResources(created ResourceList) error { } } return true, nil - }, ctx.Done()) + }) +} + +// waitForDeletedResources polls to check if all the resources are deleted or a timeout is reached +func (w *waiter) waitForDeletedResources(deleted ResourceList) error { + w.log("beginning wait for %d resources to be deleted with timeout of %v", len(deleted), w.timeout) + + ctx, cancel := context.WithTimeout(context.Background(), w.timeout) + defer cancel() + + return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { + for _, v := range deleted { + err := v.Get() + if err == nil || !apierrors.IsNotFound(err) { + return false, err + } + } + return true, nil + }) } // SelectorsForObject returns the pod label selector for a given object diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go index 29ed67026..5516ec668 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -17,10 +17,9 @@ limitations under the License. package lint import ( - "io/ioutil" - "os" "strings" "testing" + "time" "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/lint/support" @@ -35,6 +34,8 @@ const badChartDir = "rules/testdata/badchartfile" const badValuesFileDir = "rules/testdata/badvaluesfile" const badYamlFileDir = "rules/testdata/albatross" const goodChartDir = "rules/testdata/goodone" +const subChartValuesDir = "rules/testdata/withsubchart" +const malformedTemplate = "rules/testdata/malformed-template" func TestBadChart(t *testing.T) { m := All(badChartDir, values, namespace, strict).Messages @@ -42,19 +43,14 @@ func TestBadChart(t *testing.T) { 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 2 ERROR messages, check for them - var i, w, e, e2, e3, e4, e5, e6 bool + // There should be one INFO, and 2 ERROR messages, check for them + var i, 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") { i = true } } - if msg.Severity == support.WarningSev { - if strings.Contains(msg.Err.Error(), "directory not found") { - w = true - } - } if msg.Severity == support.ErrorSev { if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVer") { e = true @@ -80,7 +76,7 @@ func TestBadChart(t *testing.T) { } } } - if !e || !e2 || !e3 || !e4 || !e5 || !w || !i || !e6 { + if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 { t.Errorf("Didn't find all the expected errors, got %#v", m) } } @@ -119,11 +115,7 @@ func TestGoodChart(t *testing.T) { // // 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) + dir := t.TempDir() createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir) if err != nil { @@ -144,3 +136,38 @@ func TestHelmCreateChart(t *testing.T) { t.Errorf("Unexpected lint error: %s", msg) } } + +// lint ignores import-values +// See https://github.com/helm/helm/issues/9658 +func TestSubChartValuesChart(t *testing.T) { + m := All(subChartValuesDir, values, namespace, strict).Messages + if len(m) != 0 { + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// lint stuck with malformed template object +// See https://github.com/helm/helm/issues/11391 +func TestMalformedTemplate(t *testing.T) { + c := time.After(3 * time.Second) + ch := make(chan int, 1) + var m []support.Message + go func() { + m = All(malformedTemplate, values, namespace, strict).Messages + ch <- 1 + }() + select { + case <-c: + t.Fatalf("lint malformed template timeout") + case <-ch: + if len(m) != 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "invalid character '{'") { + t.Errorf("All didn't have the error for invalid character '{'") + } + } +} diff --git a/pkg/lint/rules/chartfile.go b/pkg/lint/rules/chartfile.go index b49f2cec0..70532ad4f 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/lint/rules/chartfile.go @@ -18,7 +18,6 @@ package rules // import "helm.sh/helm/v3/pkg/lint/rules" import ( "fmt" - "io/ioutil" "os" "path/filepath" @@ -200,7 +199,7 @@ func validateChartType(cf *chart.Metadata) error { // 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) + b, err := os.ReadFile(filename) if err != nil { return nil, err } diff --git a/pkg/lint/rules/dependencies_test.go b/pkg/lint/rules/dependencies_test.go index 075190eac..24c5faf7f 100644 --- a/pkg/lint/rules/dependencies_test.go +++ b/pkg/lint/rules/dependencies_test.go @@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -16,11 +16,9 @@ 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" @@ -79,8 +77,7 @@ func TestValidateDependencyInMetadata(t *testing.T) { } func TestDependencies(t *testing.T) { - tmp := ensure.TempDir(t) - defer os.RemoveAll(tmp) + tmp := t.TempDir() c := chartWithBadDependencies() err := chartutil.SaveDir(&c, tmp) diff --git a/pkg/lint/rules/deprecations.go b/pkg/lint/rules/deprecations.go index 384c17973..ce19b91d5 100644 --- a/pkg/lint/rules/deprecations.go +++ b/pkg/lint/rules/deprecations.go @@ -18,6 +18,7 @@ package rules // import "helm.sh/helm/v3/pkg/lint/rules" import ( "fmt" + "strconv" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -25,11 +26,12 @@ import ( kscheme "k8s.io/client-go/kubernetes/scheme" ) -const ( +var ( // 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 + // These constants will be overwritten with LDFLAGS. The version components must be + // strings in order for LDFLAGS to set them. + k8sVersionMajor = "1" + k8sVersionMinor = "20" ) // deprecatedAPIError indicates than an API is deprecated in Kubernetes @@ -60,7 +62,16 @@ func validateNoDeprecations(resource *K8sYamlStruct) error { } return err } - if !deprecation.IsDeprecated(runtimeObject, k8sVersionMajor, k8sVersionMinor) { + maj, err := strconv.Atoi(k8sVersionMajor) + if err != nil { + return err + } + min, err := strconv.Atoi(k8sVersionMinor) + if err != nil { + return err + } + + if !deprecation.IsDeprecated(runtimeObject, maj, min) { return nil } gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind) diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index daeab4cfc..000f7ebcf 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -70,6 +70,12 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace Namespace: namespace, } + // lint ignores import-values + // See https://github.com/helm/helm/issues/9658 + if err := chartutil.ProcessDependenciesWithMerge(chart, values); err != nil { + return + } + cvals, err := chartutil.CoalesceValues(chart, values) if err != nil { return @@ -135,10 +141,11 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace break } - // 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 YAML linting fails here, it will always fail in the next block as well, so we should return here. + // fix https://github.com/helm/helm/issues/11391 + if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + return + } if yamlStruct != nil { // NOTE: set to warnings to allow users to support out-of-date kubernetes // Refs https://github.com/helm/helm/issues/8596 @@ -146,6 +153,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct)) linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) + linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent)) } } } @@ -180,10 +188,10 @@ func validateTopIndentLevel(content string) error { // Validation functions func validateTemplatesDir(templatesPath string) error { - if fi, err := os.Stat(templatesPath); err != nil { - return errors.New("directory not found") - } else if !fi.IsDir() { - return errors.New("not a directory") + if fi, err := os.Stat(templatesPath); err == nil { + if !fi.IsDir() { + return errors.New("not a directory") + } } return nil } @@ -293,6 +301,28 @@ func validateMatchSelector(yamlStruct *K8sYamlStruct, manifest string) error { } return nil } +func validateListAnnotations(yamlStruct *K8sYamlStruct, manifest string) error { + if yamlStruct.Kind == "List" { + m := struct { + Items []struct { + Metadata struct { + Annotations map[string]string + } + } + }{} + + if err := yaml.Unmarshal([]byte(manifest), &m); err != nil { + return validateYamlContent(err) + } + + for _, i := range m.Items { + if _, ok := i.Metadata.Annotations["helm.sh/resource-policy"]; ok { + return errors.New("Annotation 'helm.sh/resource-policy' within List objects are ignored") + } + } + } + return nil +} // K8sYamlStruct stubs a Kubernetes YAML file. // diff --git a/pkg/lint/rules/template_test.go b/pkg/lint/rules/template_test.go index bc38445f8..80f9b28ed 100644 --- a/pkg/lint/rules/template_test.go +++ b/pkg/lint/rules/template_test.go @@ -23,7 +23,6 @@ import ( "strings" "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" @@ -221,8 +220,7 @@ func TestDeprecatedAPIFails(t *testing.T) { }, }, } - tmpdir := ensure.TempDir(t) - defer os.RemoveAll(tmpdir) + tmpdir := t.TempDir() if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { t.Fatal(err) @@ -278,8 +276,7 @@ func TestStrictTemplateParsingMapError(t *testing.T) { }, }, } - dir := ensure.TempDir(t) - defer os.RemoveAll(dir) + dir := t.TempDir() if err := chartutil.SaveDir(&ch, dir); err != nil { t.Fatal(err) } @@ -408,8 +405,7 @@ func TestEmptyWithCommentsManifests(t *testing.T) { }, }, } - tmpdir := ensure.TempDir(t) - defer os.RemoveAll(tmpdir) + tmpdir := t.TempDir() if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { t.Fatal(err) @@ -424,3 +420,41 @@ func TestEmptyWithCommentsManifests(t *testing.T) { t.Fatalf("Expected 0 lint errors, got %d", l) } } +func TestValidateListAnnotations(t *testing.T) { + md := &K8sYamlStruct{ + APIVersion: "v1", + Kind: "List", + Metadata: k8sYamlMetadata{ + Name: "list", + }, + } + manifest := ` +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ConfigMap + metadata: + annotations: + helm.sh/resource-policy: keep +` + + if err := validateListAnnotations(md, manifest); err == nil { + t.Fatal("expected list with nested keep annotations to fail") + } + + manifest = ` +apiVersion: v1 +kind: List +metadata: + annotations: + helm.sh/resource-policy: keep +items: + - apiVersion: v1 + kind: ConfigMap +` + + if err := validateListAnnotations(md, manifest); err != nil { + t.Fatalf("List objects keep annotations should pass. got: %s", err) + } +} diff --git a/pkg/lint/rules/testdata/malformed-template/.helmignore b/pkg/lint/rules/testdata/malformed-template/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/pkg/lint/rules/testdata/malformed-template/.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/pkg/lint/rules/testdata/malformed-template/Chart.yaml b/pkg/lint/rules/testdata/malformed-template/Chart.yaml new file mode 100644 index 000000000..11b2c71c2 --- /dev/null +++ b/pkg/lint/rules/testdata/malformed-template/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v2 +name: 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. +# 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" +icon: https://riverrun.io \ No newline at end of file diff --git a/pkg/lint/rules/testdata/malformed-template/templates/bad.yaml b/pkg/lint/rules/testdata/malformed-template/templates/bad.yaml new file mode 100644 index 000000000..213198fda --- /dev/null +++ b/pkg/lint/rules/testdata/malformed-template/templates/bad.yaml @@ -0,0 +1 @@ +{ {- $relname := .Release.Name -}} diff --git a/pkg/lint/rules/testdata/malformed-template/values.yaml b/pkg/lint/rules/testdata/malformed-template/values.yaml new file mode 100644 index 000000000..1cc3182ea --- /dev/null +++ b/pkg/lint/rules/testdata/malformed-template/values.yaml @@ -0,0 +1,82 @@ +# Default values for test. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + 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 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + 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 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pkg/lint/rules/testdata/withsubchart/Chart.yaml b/pkg/lint/rules/testdata/withsubchart/Chart.yaml new file mode 100644 index 000000000..6648daf56 --- /dev/null +++ b/pkg/lint/rules/testdata/withsubchart/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v2 +name: withsubchart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" +icon: http://riverrun.io + +dependencies: + - name: subchart + version: 0.1.16 + repository: "file://../subchart" + import-values: + - child: subchart + parent: subchart + diff --git a/pkg/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml b/pkg/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml new file mode 100644 index 000000000..8610a4f25 --- /dev/null +++ b/pkg/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: subchart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" diff --git a/pkg/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml b/pkg/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml new file mode 100644 index 000000000..6cb6cc2af --- /dev/null +++ b/pkg/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.subchart.name | lower }} diff --git a/pkg/lint/rules/testdata/withsubchart/charts/subchart/values.yaml b/pkg/lint/rules/testdata/withsubchart/charts/subchart/values.yaml new file mode 100644 index 000000000..422a359d5 --- /dev/null +++ b/pkg/lint/rules/testdata/withsubchart/charts/subchart/values.yaml @@ -0,0 +1,2 @@ +subchart: + name: subchart \ No newline at end of file diff --git a/pkg/lint/rules/testdata/withsubchart/templates/mainchart.yaml b/pkg/lint/rules/testdata/withsubchart/templates/mainchart.yaml new file mode 100644 index 000000000..6cb6cc2af --- /dev/null +++ b/pkg/lint/rules/testdata/withsubchart/templates/mainchart.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.subchart.name | lower }} diff --git a/pkg/lint/rules/testdata/withsubchart/values.yaml b/pkg/lint/rules/testdata/withsubchart/values.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/lint/rules/values.go b/pkg/lint/rules/values.go index 79a294326..538d8381b 100644 --- a/pkg/lint/rules/values.go +++ b/pkg/lint/rules/values.go @@ -17,7 +17,6 @@ limitations under the License. package rules import ( - "io/ioutil" "os" "path/filepath" @@ -76,7 +75,7 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err ext := filepath.Ext(valuesPath) schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" - schema, err := ioutil.ReadFile(schemaPath) + schema, err := os.ReadFile(schemaPath) if len(schema) == 0 { return nil } diff --git a/pkg/lint/rules/values_test.go b/pkg/lint/rules/values_test.go index 23335cc01..faa29d48a 100644 --- a/pkg/lint/rules/values_test.go +++ b/pkg/lint/rules/values_test.go @@ -17,7 +17,6 @@ limitations under the License. package rules import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -67,7 +66,6 @@ func TestValidateValuesFileWellFormed(t *testing.T) { 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") @@ -77,7 +75,6 @@ func TestValidateValuesFileWellFormed(t *testing.T) { 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") @@ -90,7 +87,6 @@ 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") @@ -109,7 +105,6 @@ func TestValidateValuesFileSchemaOverrides(t *testing.T) { "password": "swordfish", } tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) - defer os.RemoveAll(tmpdir) createTestingSchema(t, tmpdir) valfile := filepath.Join(tmpdir, "values.yaml") @@ -146,7 +141,6 @@ func TestValidateValuesFile(t *testing.T) { 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") @@ -168,7 +162,7 @@ func TestValidateValuesFile(t *testing.T) { 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 { + if err := os.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 b9a9d0918..bffefe8ff 100644 --- a/pkg/lint/support/doc.go +++ b/pkg/lint/support/doc.go @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package support contains tools for linting charts. +/* +Package support contains tools for linting charts. Linting is the process of testing charts for errors or warnings regarding formatting, compilation, or standards compliance. diff --git a/pkg/plugin/installer/base.go b/pkg/plugin/installer/base.go index dcc3ad644..ba6a55d55 100644 --- a/pkg/plugin/installer/base.go +++ b/pkg/plugin/installer/base.go @@ -18,16 +18,22 @@ package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( "path/filepath" - "helm.sh/helm/v3/pkg/helmpath" + "helm.sh/helm/v3/pkg/cli" ) type base struct { // Source is the reference to a plugin Source string + // PluginsDirectory is the directory where plugins are installed + PluginsDirectory string } func newBase(source string) base { - return base{source} + settings := cli.New() + return base{ + Source: source, + PluginsDirectory: settings.PluginsDirectory, + } } // Path is where the plugin will be installed. @@ -35,5 +41,5 @@ func (b *base) Path() string { if b.Source == "" { return "" } - return helmpath.DataPath("plugins", filepath.Base(b.Source)) + return filepath.Join(b.PluginsDirectory, filepath.Base(b.Source)) } diff --git a/pkg/plugin/installer/base_test.go b/pkg/plugin/installer/base_test.go new file mode 100644 index 000000000..38ef28c3e --- /dev/null +++ b/pkg/plugin/installer/base_test.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 installer // import "helm.sh/helm/v3/pkg/plugin/installer" + +import ( + "os" + "testing" +) + +func TestPath(t *testing.T) { + tests := []struct { + source string + helmPluginsDir string + expectPath string + }{ + { + source: "", + helmPluginsDir: "/helm/data/plugins", + expectPath: "", + }, { + source: "https://github.com/jkroepke/helm-secrets", + helmPluginsDir: "/helm/data/plugins", + expectPath: "/helm/data/plugins/helm-secrets", + }, + } + + for _, tt := range tests { + + os.Setenv("HELM_PLUGINS", tt.helmPluginsDir) + baseIns := newBase(tt.source) + baseInsPath := baseIns.Path() + if baseInsPath != tt.expectPath { + t.Errorf("expected name %s, got %s", tt.expectPath, baseInsPath) + } + os.Unsetenv("HELM_PLUGINS") + } +} diff --git a/pkg/plugin/installer/http_installer.go b/pkg/plugin/installer/http_installer.go index bcbcbde93..49274f83c 100644 --- a/pkg/plugin/installer/http_installer.go +++ b/pkg/plugin/installer/http_installer.go @@ -162,13 +162,13 @@ func (i HTTPInstaller) Path() string { return helmpath.DataPath("plugins", i.PluginName) } -// CleanJoin resolves dest as a subpath of root. +// 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 +// 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 diff --git a/pkg/plugin/installer/http_installer_test.go b/pkg/plugin/installer/http_installer_test.go index e89fea29d..177227c5b 100644 --- a/pkg/plugin/installer/http_installer_test.go +++ b/pkg/plugin/installer/http_installer_test.go @@ -21,7 +21,6 @@ import ( "compress/gzip" "encoding/base64" "fmt" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -80,7 +79,7 @@ func mockArchiveServer() *httptest.Server { } func TestHTTPInstaller(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) srv := mockArchiveServer() defer srv.Close() @@ -129,7 +128,7 @@ func TestHTTPInstaller(t *testing.T) { } func TestHTTPInstallerNonExistentVersion(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) srv := mockArchiveServer() defer srv.Close() source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" @@ -165,7 +164,7 @@ func TestHTTPInstallerUpdate(t *testing.T) { srv := mockArchiveServer() defer srv.Close() source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" - defer ensure.HelmHome(t)() + ensure.HelmHome(t) if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) @@ -209,11 +208,7 @@ func TestHTTPInstallerUpdate(t *testing.T) { 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) + tempDir := t.TempDir() // Set the umask to default open permissions so we can actually test oldmask := syscall.Umask(0000) diff --git a/pkg/plugin/installer/local_installer.go b/pkg/plugin/installer/local_installer.go index c92bc3fb0..759df38be 100644 --- a/pkg/plugin/installer/local_installer.go +++ b/pkg/plugin/installer/local_installer.go @@ -22,6 +22,9 @@ import ( "github.com/pkg/errors" ) +// ErrPluginNotAFolder indicates that the plugin path is not a folder. +var ErrPluginNotAFolder = errors.New("expected plugin to be a folder") + // LocalInstaller installs plugins from the filesystem. type LocalInstaller struct { base @@ -43,6 +46,14 @@ func NewLocalInstaller(source string) (*LocalInstaller, error) { // // Implements Installer. func (i *LocalInstaller) Install() error { + stat, err := os.Stat(i.Source) + if err != nil { + return err + } + if !stat.IsDir() { + return ErrPluginNotAFolder + } + if !isPlugin(i.Source) { return ErrMissingMetadata } diff --git a/pkg/plugin/installer/local_installer_test.go b/pkg/plugin/installer/local_installer_test.go index 0d3de11d1..51408f128 100644 --- a/pkg/plugin/installer/local_installer_test.go +++ b/pkg/plugin/installer/local_installer_test.go @@ -16,7 +16,6 @@ limitations under the License. package installer // import "helm.sh/helm/v3/pkg/plugin/installer" import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -28,12 +27,8 @@ var _ Installer = new(LocalInstaller) func TestLocalInstaller(t *testing.T) { // Make a temp dir - tdir, err := ioutil.TempDir("", "helm-installer-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tdir) - if err := ioutil.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil { + tdir := t.TempDir() + if err := os.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil { t.Fatal(err) } @@ -52,3 +47,19 @@ func TestLocalInstaller(t *testing.T) { } defer os.RemoveAll(filepath.Dir(helmpath.DataPath())) // helmpath.DataPath is like /tmp/helm013130971/helm } + +func TestLocalInstallerNotAFolder(t *testing.T) { + source := "../testdata/plugdir/good/echo/plugin.yaml" + i, err := NewForSource(source, "") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + err = Install(i) + if err == nil { + t.Fatal("expected error") + } + if err != ErrPluginNotAFolder { + t.Fatalf("expected error to equal: %q", err) + } +} diff --git a/pkg/plugin/installer/vcs_installer_test.go b/pkg/plugin/installer/vcs_installer_test.go index 6785264b3..0bb0b6780 100644 --- a/pkg/plugin/installer/vcs_installer_test.go +++ b/pkg/plugin/installer/vcs_installer_test.go @@ -49,7 +49,7 @@ func (r *testRepo) UpdateVersion(version string) error { } func TestVCSInstaller(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) @@ -102,7 +102,7 @@ func TestVCSInstaller(t *testing.T) { } func TestVCSInstallerNonExistentVersion(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) source := "https://github.com/adamreese/helm-env" version := "0.2.0" @@ -124,7 +124,7 @@ func TestVCSInstallerNonExistentVersion(t *testing.T) { } } func TestVCSInstallerUpdate(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) source := "https://github.com/adamreese/helm-env" diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 1399b7116..fb3bc5215 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -17,7 +17,6 @@ package plugin // import "helm.sh/helm/v3/pkg/plugin" import ( "fmt" - "io/ioutil" "os" "path/filepath" "regexp" @@ -122,10 +121,10 @@ func getPlatformCommand(cmds []PlatformCommand) []string { eq := strings.EqualFold for _, c := range cmds { if eq(c.OperatingSystem, runtime.GOOS) { - command = strings.Split(os.ExpandEnv(c.Command), " ") + command = strings.Split(c.Command, " ") } if eq(c.OperatingSystem, runtime.GOOS) && eq(c.Architecture, runtime.GOARCH) { - return strings.Split(os.ExpandEnv(c.Command), " ") + return strings.Split(c.Command, " ") } } return command @@ -149,16 +148,19 @@ func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) { parts = getPlatformCommand(p.Metadata.PlatformCommand) } if platCmdLen == 0 || parts == nil { - parts = strings.Split(os.ExpandEnv(p.Metadata.Command), " ") + parts = strings.Split(p.Metadata.Command, " ") } if len(parts) == 0 || parts[0] == "" { return "", nil, fmt.Errorf("no plugin command is applicable") } - main := parts[0] + main := os.ExpandEnv(parts[0]) baseArgs := []string{} if len(parts) > 1 { - baseArgs = parts[1:] + for _, cmdpart := range parts[1:] { + cmdexp := os.ExpandEnv(cmdpart) + baseArgs = append(baseArgs, cmdexp) + } } if !p.Metadata.IgnoreFlags { baseArgs = append(baseArgs, extraArgs...) @@ -216,7 +218,7 @@ func detectDuplicates(plugs []*Plugin) error { // LoadDir loads a plugin from the given directory. func LoadDir(dirname string) (*Plugin, error) { pluginfile := filepath.Join(dirname, PluginFileName) - data, err := ioutil.ReadFile(pluginfile) + data, err := os.ReadFile(pluginfile) if err != nil { return nil, errors.Wrapf(err, "failed to read plugin at %q", pluginfile) } diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go index 2bbdff21d..abbc90f7d 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/plugin_test.go @@ -86,7 +86,7 @@ func TestPlatformPrepareCommand(t *testing.T) { Name: "test", Command: "echo -n os-arch", PlatformCommand: []PlatformCommand{ - {OperatingSystem: "linux", Architecture: "i386", Command: "echo -n linux-i386"}, + {OperatingSystem: "linux", Architecture: "386", Command: "echo -n linux-386"}, {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"}, @@ -98,8 +98,8 @@ func TestPlatformPrepareCommand(t *testing.T) { var osStrCmp string os := runtime.GOOS arch := runtime.GOARCH - if os == "linux" && arch == "i386" { - osStrCmp = "linux-i386" + if os == "linux" && arch == "386" { + osStrCmp = "linux-386" } else if os == "linux" && arch == "amd64" { osStrCmp = "linux-amd64" } else if os == "linux" && arch == "arm64" { @@ -125,7 +125,7 @@ func TestPartialPlatformPrepareCommand(t *testing.T) { Name: "test", Command: "echo -n os-arch", PlatformCommand: []PlatformCommand{ - {OperatingSystem: "linux", Architecture: "i386", Command: "echo -n linux-i386"}, + {OperatingSystem: "linux", Architecture: "386", Command: "echo -n linux-386"}, {OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"}, }, }, @@ -134,7 +134,7 @@ func TestPartialPlatformPrepareCommand(t *testing.T) { os := runtime.GOOS arch := runtime.GOARCH if os == "linux" { - osStrCmp = "linux-i386" + osStrCmp = "linux-386" } else if os == "windows" && arch == "amd64" { osStrCmp = "win-64" } else { @@ -166,7 +166,7 @@ func TestNoMatchPrepareCommand(t *testing.T) { Metadata: &Metadata{ Name: "test", PlatformCommand: []PlatformCommand{ - {OperatingSystem: "no-os", Architecture: "amd64", Command: "echo -n linux-i386"}, + {OperatingSystem: "no-os", Architecture: "amd64", Command: "echo -n linux-386"}, }, }, } @@ -193,7 +193,7 @@ func TestLoadDir(t *testing.T) { Version: "0.1.0", Usage: "usage", Description: "description", - Command: "$HELM_PLUGIN_SELF/hello.sh", + Command: "$HELM_PLUGIN_DIR/hello.sh", IgnoreFlags: true, Hooks: map[string]string{ Install: "echo installing...", @@ -329,6 +329,26 @@ func TestSetupEnv(t *testing.T) { } } +func TestSetupEnvWithSpace(t *testing.T) { + name := "sureshdsk" + base := filepath.Join("testdata/helm home/helm/plugins", name) + + s := cli.New() + s.PluginsDirectory = "testdata/helm home/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 diff --git a/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml b/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml index 2b972da59..b857b55ee 100644 --- a/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml +++ b/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml @@ -3,7 +3,7 @@ version: "0.1.0" usage: "usage" description: |- description -command: "$HELM_PLUGIN_SELF/hello.sh" +command: "$HELM_PLUGIN_DIR/hello.sh" ignoreFlags: true hooks: install: "echo installing..." diff --git a/pkg/postrender/exec.go b/pkg/postrender/exec.go index 1de70b024..167e737d6 100644 --- a/pkg/postrender/exec.go +++ b/pkg/postrender/exec.go @@ -27,23 +27,24 @@ import ( type execRender struct { binaryPath string + args []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) { +func NewExec(binaryPath string, args ...string) (PostRenderer, error) { fullPath, err := getFullPath(binaryPath) if err != nil { return nil, err } - return &execRender{fullPath}, nil + return &execRender{fullPath, args}, nil } // Run the configured binary for the post render func (p *execRender) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) { - cmd := exec.Command(p.binaryPath) + cmd := exec.Command(p.binaryPath, p.args...) stdin, err := cmd.StdinPipe() if err != nil { return nil, err diff --git a/pkg/postrender/exec_test.go b/pkg/postrender/exec_test.go index ef0956949..19a6ec6c4 100644 --- a/pkg/postrender/exec_test.go +++ b/pkg/postrender/exec_test.go @@ -18,7 +18,6 @@ package postrender import ( "bytes" - "io/ioutil" "os" "path/filepath" "runtime" @@ -26,19 +25,20 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "helm.sh/helm/v3/internal/test/ensure" ) const testingScript = `#!/bin/sh +if [ $# -eq 0 ]; then sed s/FOOTEST/BARTEST/g <&0 +else +sed s/FOOTEST/"$*"/g <&0 +fi ` 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() + testpath := setupTestingScript(t) fullPath, err := getFullPath(testpath) is.NoError(err) @@ -46,8 +46,7 @@ func TestGetFullPath(t *testing.T) { }) t.Run("relative path resolves correctly", func(t *testing.T) { - testpath, cleanup := setupTestingScript(t) - defer cleanup() + testpath := setupTestingScript(t) currentDir, err := os.Getwd() require.NoError(t, err) @@ -59,8 +58,7 @@ func TestGetFullPath(t *testing.T) { }) t.Run("binary in PATH resolves correctly", func(t *testing.T) { - testpath, cleanup := setupTestingScript(t) - defer cleanup() + testpath := setupTestingScript(t) realPath := os.Getenv("PATH") os.Setenv("PATH", filepath.Dir(testpath)) @@ -113,8 +111,7 @@ func TestExecRun(t *testing.T) { t.Skip("skipping on windows") } is := assert.New(t) - testpath, cleanup := setupTestingScript(t) - defer cleanup() + testpath := setupTestingScript(t) renderer, err := NewExec(testpath) require.NoError(t, err) @@ -124,12 +121,44 @@ func TestExecRun(t *testing.T) { is.Contains(output.String(), "BARTEST") } -func setupTestingScript(t *testing.T) (filepath string, cleanup func()) { +func TestNewExecWithOneArgsRun(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 := setupTestingScript(t) + + renderer, err := NewExec(testpath, "ARG1") + require.NoError(t, err) + + output, err := renderer.Run(bytes.NewBufferString("FOOTEST")) + is.NoError(err) + is.Contains(output.String(), "ARG1") +} + +func TestNewExecWithTwoArgsRun(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 := setupTestingScript(t) + + renderer, err := NewExec(testpath, "ARG1", "ARG2") + require.NoError(t, err) + + output, err := renderer.Run(bytes.NewBufferString("FOOTEST")) + is.NoError(err) + is.Contains(output.String(), "ARG1 ARG2") +} + +func setupTestingScript(t *testing.T) (filepath string) { t.Helper() - tempdir := ensure.TempDir(t) + tempdir := t.TempDir() - f, err := ioutil.TempFile(tempdir, "post-render-test.sh") + f, err := os.CreateTemp(tempdir, "post-render-test.sh") if err != nil { t.Fatalf("unable to create tempfile for testing: %s", err) } @@ -149,7 +178,5 @@ func setupTestingScript(t *testing.T) (filepath string, cleanup func()) { t.Fatalf("unable to close tempfile after writing: %s", err) } - return f.Name(), func() { - os.RemoveAll(tempdir) - } + return f.Name() } diff --git a/pkg/provenance/doc.go b/pkg/provenance/doc.go index 3d2d0ea97..0c7ae0618 100644 --- a/pkg/provenance/doc.go +++ b/pkg/provenance/doc.go @@ -13,7 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package provenance provides tools for establishing the authenticity of a chart. +/* +Package provenance provides tools for establishing the authenticity of a chart. In Helm, provenance is established via several factors. The primary factor is the cryptographic signature of a chart. Chart authors may sign charts, which in turn diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index 5d16779f1..7f89ef3f5 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -20,15 +20,14 @@ import ( "crypto" "encoding/hex" "io" - "io/ioutil" "os" "path/filepath" "strings" "github.com/pkg/errors" - "golang.org/x/crypto/openpgp" - "golang.org/x/crypto/openpgp/clearsign" - "golang.org/x/crypto/openpgp/packet" + "golang.org/x/crypto/openpgp" //nolint + "golang.org/x/crypto/openpgp/clearsign" //nolint + "golang.org/x/crypto/openpgp/packet" //nolint "sigs.k8s.io/yaml" hapi "helm.sh/helm/v3/pkg/chart" @@ -42,9 +41,13 @@ var defaultPGPConfig = packet.Config{ // SumCollection represents a collection of file and image checksums. // // Files are of the form: +// // FILENAME: "sha256:SUM" +// // Images are of the form: +// // "IMAGE:TAG": "sha256:SUM" +// // Docker optionally supports sha512, and if this is the case, the hash marker // will be 'sha512' instead of 'sha256'. type SumCollection struct { @@ -216,7 +219,7 @@ func (s *Signatory) ClearSign(chartpath string) (string, error) { b, err := messageBlock(chartpath) if err != nil { - return "", nil + return "", err } // Sign the buffer @@ -224,9 +227,24 @@ func (s *Signatory) ClearSign(chartpath string) (string, error) { if err != nil { return "", err } + _, err = io.Copy(w, b) - w.Close() - return out.String(), err + + if err != nil { + // NB: We intentionally don't call `w.Close()` here! `w.Close()` is the method which + // actually does the PGP signing, and therefore is the part which uses the private key. + // In other words, if we call Close here, there's a risk that there's an attempt to use the + // private key to sign garbage data (since we know that io.Copy failed, `w` won't contain + // anything useful). + return "", errors.Wrap(err, "failed to write to clearsign encoder") + } + + err = w.Close() + if err != nil { + return "", errors.Wrap(err, "failed to either sign or armor message block") + } + + return out.String(), nil } // Verify checks a signature and verifies that it is legit for a chart. @@ -278,7 +296,7 @@ func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) { } func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) { - data, err := ioutil.ReadFile(filename) + data, err := os.ReadFile(filename) if err != nil { return nil, err } diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index 1f4d2d232..17f727ea7 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -16,13 +16,15 @@ limitations under the License. package provenance import ( - "io/ioutil" + "crypto" + "fmt" + "io" "os" "path/filepath" "strings" "testing" - pgperrors "golang.org/x/crypto/openpgp/errors" + pgperrors "golang.org/x/crypto/openpgp/errors" //nolint ) const ( @@ -230,6 +232,36 @@ func TestClearSign(t *testing.T) { } } +// failSigner always fails to sign and returns an error +type failSigner struct{} + +func (s failSigner) Public() crypto.PublicKey { + return nil +} + +func (s failSigner) Sign(_ io.Reader, _ []byte, _ crypto.SignerOpts) ([]byte, error) { + return nil, fmt.Errorf("always fails") +} + +func TestClearSignError(t *testing.T) { + signer, err := NewFromFiles(testKeyfile, testPubfile) + if err != nil { + t.Fatal(err) + } + + // ensure that signing always fails + signer.Entity.PrivateKey.PrivateKey = failSigner{} + + sig, err := signer.ClearSign(testChartfile) + if err == nil { + t.Fatal("didn't get an error from ClearSign but expected one") + } + + if sig != "" { + t.Fatalf("expected an empty signature after failed ClearSign but got %q", sig) + } +} + func TestDecodeSignature(t *testing.T) { // Unlike other tests, this does a round-trip test, ensuring that a signature // generated by the library can also be verified by the library. @@ -244,7 +276,7 @@ func TestDecodeSignature(t *testing.T) { t.Fatal(err) } - f, err := ioutil.TempFile("", "helm-test-sig-") + f, err := os.CreateTemp("", "helm-test-sig-") if err != nil { t.Fatal(err) } @@ -301,7 +333,7 @@ func TestVerify(t *testing.T) { // readSumFile reads a file containing a sum generated by the UNIX shasum tool. func readSumFile(sumfile string) (string, error) { - data, err := ioutil.ReadFile(sumfile) + data, err := os.ReadFile(sumfile) if err != nil { return "", err } diff --git a/pkg/pusher/doc.go b/pkg/pusher/doc.go new file mode 100644 index 000000000..df89ab112 --- /dev/null +++ b/pkg/pusher/doc.go @@ -0,0 +1,21 @@ +/* +Copyright The 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 pusher provides a generalized tool for uploading data by scheme. +This provides a method by which the plugin system can load arbitrary protocol +handlers based upon a URL scheme. +*/ +package pusher diff --git a/pkg/pusher/ocipusher.go b/pkg/pusher/ocipusher.go new file mode 100644 index 000000000..94154d389 --- /dev/null +++ b/pkg/pusher/ocipusher.go @@ -0,0 +1,152 @@ +/* +Copyright The 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 pusher + +import ( + "fmt" + "net" + "net/http" + "os" + "path" + "strings" + "time" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/internal/tlsutil" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/registry" +) + +// OCIPusher is the default OCI backend handler +type OCIPusher struct { + opts options +} + +// Push performs a Push from repo.Pusher. +func (pusher *OCIPusher) Push(chartRef, href string, options ...Option) error { + for _, opt := range options { + opt(&pusher.opts) + } + return pusher.push(chartRef, href) +} + +func (pusher *OCIPusher) push(chartRef, href string) error { + stat, err := os.Stat(chartRef) + if err != nil { + if os.IsNotExist(err) { + return errors.Errorf("%s: no such file", chartRef) + } + return err + } + if stat.IsDir() { + return errors.New("cannot push directory, must provide chart archive (.tgz)") + } + + meta, err := loader.Load(chartRef) + if err != nil { + return err + } + + client := pusher.opts.registryClient + if client == nil { + c, err := pusher.newRegistryClient() + if err != nil { + return err + } + client = c + } + + chartBytes, err := os.ReadFile(chartRef) + if err != nil { + return err + } + + var pushOpts []registry.PushOption + provRef := fmt.Sprintf("%s.prov", chartRef) + if _, err := os.Stat(provRef); err == nil { + provBytes, err := os.ReadFile(provRef) + if err != nil { + return err + } + pushOpts = append(pushOpts, registry.PushOptProvData(provBytes)) + } + + ref := fmt.Sprintf("%s:%s", + path.Join(strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)), meta.Metadata.Name), + meta.Metadata.Version) + + _, err = client.Push(chartBytes, ref, pushOpts...) + return err +} + +// NewOCIPusher constructs a valid OCI client as a Pusher +func NewOCIPusher(ops ...Option) (Pusher, error) { + var client OCIPusher + + for _, opt := range ops { + opt(&client.opts) + } + + return &client, nil +} + +func (pusher *OCIPusher) newRegistryClient() (*registry.Client, error) { + if (pusher.opts.certFile != "" && pusher.opts.keyFile != "") || pusher.opts.caFile != "" || pusher.opts.insecureSkipTLSverify { + tlsConf, err := tlsutil.NewClientTLS(pusher.opts.certFile, pusher.opts.keyFile, pusher.opts.caFile, pusher.opts.insecureSkipTLSverify) + if err != nil { + return nil, errors.Wrap(err, "can't create TLS config for client") + } + + registryClient, err := registry.NewClient( + registry.ClientOptHTTPClient(&http.Client{ + // From https://github.com/google/go-containerregistry/blob/31786c6cbb82d6ec4fb8eb79cd9387905130534e/pkg/v1/remote/options.go#L87 + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + // By default we wrap the transport in retries, so reduce the + // default dial timeout to 5s to avoid 5x 30s of connection + // timeouts when doing the "ping" on certain http registries. + Timeout: 5 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: tlsConf, + }, + }), + registry.ClientOptEnableCache(true), + ) + if err != nil { + return nil, err + } + return registryClient, nil + } + + opts := []registry.ClientOption{registry.ClientOptEnableCache(true)} + if pusher.opts.plainHTTP { + opts = append(opts, registry.ClientOptPlainHTTP()) + } + + registryClient, err := registry.NewClient(opts...) + if err != nil { + return nil, err + } + return registryClient, nil +} diff --git a/pkg/pusher/ocipusher_test.go b/pkg/pusher/ocipusher_test.go new file mode 100644 index 000000000..11842b4ae --- /dev/null +++ b/pkg/pusher/ocipusher_test.go @@ -0,0 +1,96 @@ +/* +Copyright The 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 pusher + +import ( + "path/filepath" + "testing" + + "helm.sh/helm/v3/pkg/registry" +) + +func TestNewOCIPusher(t *testing.T) { + p, err := NewOCIPusher() + if err != nil { + t.Fatal(err) + } + + if _, ok := p.(*OCIPusher); !ok { + t.Fatal("Expected NewOCIPusher to produce an *OCIPusher") + } + + cd := "../../testdata" + join := filepath.Join + ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem") + insecureSkipTLSverify := false + plainHTTP := false + + // Test with options + p, err = NewOCIPusher( + WithTLSClientConfig(pub, priv, ca), + WithInsecureSkipTLSVerify(insecureSkipTLSverify), + WithPlainHTTP(plainHTTP), + ) + if err != nil { + t.Fatal(err) + } + + op, ok := p.(*OCIPusher) + if !ok { + t.Fatal("Expected NewOCIPusher to produce an *OCIPusher") + } + + if op.opts.certFile != pub { + t.Errorf("Expected NewOCIPusher to contain %q as the public key file, got %q", pub, op.opts.certFile) + } + + if op.opts.keyFile != priv { + t.Errorf("Expected NewOCIPusher to contain %q as the private key file, got %q", priv, op.opts.keyFile) + } + + if op.opts.caFile != ca { + t.Errorf("Expected NewOCIPusher to contain %q as the CA file, got %q", ca, op.opts.caFile) + } + + if op.opts.plainHTTP != plainHTTP { + t.Errorf("Expected NewOCIPusher to have plainHTTP as %t, got %t", plainHTTP, op.opts.plainHTTP) + } + + if op.opts.insecureSkipTLSverify != insecureSkipTLSverify { + t.Errorf("Expected NewOCIPusher to have insecureSkipVerifyTLS as %t, got %t", insecureSkipTLSverify, op.opts.insecureSkipTLSverify) + } + + // Test if setting registryClient is being passed to the ops + registryClient, err := registry.NewClient() + if err != nil { + t.Fatal(err) + } + + p, err = NewOCIPusher( + WithRegistryClient(registryClient), + ) + if err != nil { + t.Fatal(err) + } + op, ok = p.(*OCIPusher) + if !ok { + t.Fatal("expected NewOCIPusher to produce an *OCIPusher") + } + + if op.opts.registryClient != registryClient { + t.Errorf("Expected NewOCIPusher to contain %p as RegistryClient, got %p", registryClient, op.opts.registryClient) + } +} diff --git a/pkg/pusher/pusher.go b/pkg/pusher/pusher.go new file mode 100644 index 000000000..c99d97b35 --- /dev/null +++ b/pkg/pusher/pusher.go @@ -0,0 +1,122 @@ +/* +Copyright The 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 pusher + +import ( + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/registry" +) + +// options are generic parameters to be provided to the pusher during instantiation. +// +// Pushers may or may not ignore these parameters as they are passed in. +type options struct { + registryClient *registry.Client + certFile string + keyFile string + caFile string + insecureSkipTLSverify bool + plainHTTP bool +} + +// Option allows specifying various settings configurable by the user for overriding the defaults +// used when performing Push operations with the Pusher. +type Option func(*options) + +// WithRegistryClient sets the registryClient option. +func WithRegistryClient(client *registry.Client) Option { + return func(opts *options) { + opts.registryClient = client + } +} + +// 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 + } +} + +// WithInsecureSkipTLSVerify determines if a TLS Certificate will be checked +func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) Option { + return func(opts *options) { + opts.insecureSkipTLSverify = insecureSkipTLSVerify + } +} + +func WithPlainHTTP(plainHTTP bool) Option { + return func(opts *options) { + opts.plainHTTP = plainHTTP + } +} + +// Pusher is an interface to support upload to the specified URL. +type Pusher interface { + // Push file content by url string + Push(chartRef, url string, options ...Option) error +} + +// Constructor is the function for every pusher which creates a specific instance +// according to the configuration +type Constructor func(options ...Option) (Pusher, error) + +// Provider represents any pusher and the schemes that it supports. +type Provider struct { + Schemes []string + New Constructor +} + +// Provides returns true if the given scheme is supported by this Provider. +func (p Provider) Provides(scheme string) bool { + for _, i := range p.Schemes { + if i == scheme { + return true + } + } + return false +} + +// Providers is a collection of Provider objects. +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) (Pusher, error) { + for _, pp := range p { + if pp.Provides(scheme) { + return pp.New() + } + } + return nil, errors.Errorf("scheme %q not supported", scheme) +} + +var ociProvider = Provider{ + Schemes: []string{registry.OCIScheme}, + New: NewOCIPusher, +} + +// All finds all of the registered pushers as a list of Provider instances. +// Currently, just the built-in pushers are collected. +func All(settings *cli.EnvSettings) Providers { + result := Providers{ociProvider} + return result +} diff --git a/pkg/pusher/pusher_test.go b/pkg/pusher/pusher_test.go new file mode 100644 index 000000000..d43e6c9ec --- /dev/null +++ b/pkg/pusher/pusher_test.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 pusher + +import ( + "testing" + + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/registry" +) + +func TestProvider(t *testing.T) { + p := Provider{ + []string{"one", "three"}, + func(_ ...Option) (Pusher, error) { return nil, nil }, + } + + if !p.Provides("three") { + t.Error("Expected provider to provide three") + } +} + +func TestProviders(t *testing.T) { + ps := Providers{ + {[]string{"one", "three"}, func(_ ...Option) (Pusher, error) { return nil, nil }}, + {[]string{"two", "four"}, func(_ ...Option) (Pusher, error) { return nil, nil }}, + } + + if _, err := ps.ByScheme("one"); err != nil { + t.Error(err) + } + if _, err := ps.ByScheme("four"); err != nil { + t.Error(err) + } + + if _, err := ps.ByScheme("five"); err == nil { + t.Error("Did not expect handler for five") + } +} + +func TestAll(t *testing.T) { + env := cli.New() + all := All(env) + if len(all) != 1 { + t.Errorf("expected 1 provider (OCI), got %d", len(all)) + } +} + +func TestByScheme(t *testing.T) { + env := cli.New() + g := All(env) + if _, err := g.ByScheme(registry.OCIScheme); err != nil { + t.Error(err) + } +} diff --git a/pkg/registry/client.go b/pkg/registry/client.go new file mode 100644 index 000000000..829f6b494 --- /dev/null +++ b/pkg/registry/client.go @@ -0,0 +1,711 @@ +/* +Copyright The 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/pkg/registry" + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "sort" + "strings" + + "github.com/Masterminds/semver/v3" + "github.com/containerd/containerd/remotes" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "oras.land/oras-go/pkg/auth" + dockerauth "oras.land/oras-go/pkg/auth/docker" + "oras.land/oras-go/pkg/content" + "oras.land/oras-go/pkg/oras" + "oras.land/oras-go/pkg/registry" + registryremote "oras.land/oras-go/pkg/registry/remote" + registryauth "oras.land/oras-go/pkg/registry/remote/auth" + + "helm.sh/helm/v3/internal/version" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/helmpath" +) + +// See https://github.com/helm/helm/issues/10166 +const registryUnderscoreMessage = ` +OCI artifact references (e.g. tags) do not support the plus sign (+). To support +storing semantic versions, Helm adopts the convention of changing plus (+) to +an underscore (_) in chart version tags when pushing to a registry and back to +a plus (+) when pulling from a registry.` + +type ( + // Client works with OCI-compliant registries + Client struct { + debug bool + enableCache bool + // path to repository config file e.g. ~/.docker/config.json + credentialsFile string + out io.Writer + authorizer auth.Client + registryAuthorizer *registryauth.Client + resolver func(ref registry.Reference) (remotes.Resolver, error) + httpClient *http.Client + plainHTTP bool + } + + // ClientOption allows specifying various settings configurable by the user for overriding the defaults + // used when creating a new default client + ClientOption func(*Client) +) + +// NewClient returns a new registry client with config +func NewClient(options ...ClientOption) (*Client, error) { + client := &Client{ + out: io.Discard, + } + for _, option := range options { + option(client) + } + if client.credentialsFile == "" { + client.credentialsFile = helmpath.ConfigPath(CredentialsFileBasename) + } + if client.authorizer == nil { + authClient, err := dockerauth.NewClientWithDockerFallback(client.credentialsFile) + if err != nil { + return nil, err + } + client.authorizer = authClient + } + + resolverFn := client.resolver // copy for avoiding recursive call + client.resolver = func(ref registry.Reference) (remotes.Resolver, error) { + if resolverFn != nil { + // validate if the resolverFn returns a valid resolver + if resolver, err := resolverFn(ref); resolver != nil && err == nil { + return resolver, nil + } + } + + headers := http.Header{} + headers.Set("User-Agent", version.GetUserAgent()) + dockerClient, ok := client.authorizer.(*dockerauth.Client) + if ok { + username, password, err := dockerClient.Credential(ref.Registry) + if err != nil { + return nil, fmt.Errorf("unable to retrieve credentials: %w", err) + } + authHeader(username, password, &headers) + } + + opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)} + if client.httpClient != nil { + opts = append(opts, auth.WithResolverClient(client.httpClient)) + } + if client.plainHTTP { + opts = append(opts, auth.WithResolverPlainHTTP()) + } + resolver, err := client.authorizer.ResolverWithOpts(opts...) + if err != nil { + return nil, err + } + return resolver, nil + } + + // allocate a cache if option is set + var cache registryauth.Cache + if client.enableCache { + cache = registryauth.DefaultCache + } + if client.registryAuthorizer == nil { + client.registryAuthorizer = ®istryauth.Client{ + Client: client.httpClient, + Header: http.Header{ + "User-Agent": {version.GetUserAgent()}, + }, + Cache: cache, + Credential: func(ctx context.Context, reg string) (registryauth.Credential, error) { + dockerClient, ok := client.authorizer.(*dockerauth.Client) + if !ok { + return registryauth.EmptyCredential, errors.New("unable to obtain docker client") + } + username, password, err := dockerClient.Credential(reg) + if err != nil { + return registryauth.EmptyCredential, errors.New("unable to retrieve credentials") + } + + // A blank returned username and password value is a bearer token + if username == "" && password != "" { + return registryauth.Credential{ + RefreshToken: password, + }, nil + } + + return registryauth.Credential{ + Username: username, + Password: password, + }, nil + + }, + } + + } + return client, nil +} + +// 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 + } +} + +// ClientOptEnableCache returns a function that sets the enableCache setting on a client options set +func ClientOptEnableCache(enableCache bool) ClientOption { + return func(client *Client) { + client.enableCache = enableCache + } +} + +// 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 + } +} + +// ClientOptCredentialsFile returns a function that sets the credentialsFile setting on a client options set +func ClientOptCredentialsFile(credentialsFile string) ClientOption { + return func(client *Client) { + client.credentialsFile = credentialsFile + } +} + +// ClientOptHTTPClient returns a function that sets the httpClient setting on a client options set +func ClientOptHTTPClient(httpClient *http.Client) ClientOption { + return func(client *Client) { + client.httpClient = httpClient + } +} + +func ClientOptPlainHTTP() ClientOption { + return func(c *Client) { + c.plainHTTP = true + } +} + +// ClientOptResolver returns a function that sets the resolver setting on a client options set +func ClientOptResolver(resolver remotes.Resolver) ClientOption { + return func(client *Client) { + client.resolver = func(ref registry.Reference) (remotes.Resolver, error) { + return resolver, nil + } + } +} + +type ( + // LoginOption allows specifying various settings on login + LoginOption func(*loginOperation) + + loginOperation struct { + username string + password string + insecure bool + certFile string + keyFile string + caFile string + } +) + +// Login logs into a registry +func (c *Client) Login(host string, options ...LoginOption) error { + operation := &loginOperation{} + for _, option := range options { + option(operation) + } + authorizerLoginOpts := []auth.LoginOption{ + auth.WithLoginContext(ctx(c.out, c.debug)), + auth.WithLoginHostname(host), + auth.WithLoginUsername(operation.username), + auth.WithLoginSecret(operation.password), + auth.WithLoginUserAgent(version.GetUserAgent()), + auth.WithLoginTLS(operation.certFile, operation.keyFile, operation.caFile), + } + if operation.insecure { + authorizerLoginOpts = append(authorizerLoginOpts, auth.WithLoginInsecure()) + } + if err := c.authorizer.LoginWithOpts(authorizerLoginOpts...); err != nil { + return err + } + fmt.Fprintln(c.out, "Login Succeeded") + return nil +} + +// LoginOptBasicAuth returns a function that sets the username/password settings on login +func LoginOptBasicAuth(username string, password string) LoginOption { + return func(operation *loginOperation) { + operation.username = username + operation.password = password + } +} + +// LoginOptInsecure returns a function that sets the insecure setting on login +func LoginOptInsecure(insecure bool) LoginOption { + return func(operation *loginOperation) { + operation.insecure = insecure + } +} + +// LoginOptTLSClientConfig returns a function that sets the TLS settings on login. +func LoginOptTLSClientConfig(certFile, keyFile, caFile string) LoginOption { + return func(operation *loginOperation) { + operation.certFile = certFile + operation.keyFile = keyFile + operation.caFile = caFile + } +} + +type ( + // LogoutOption allows specifying various settings on logout + LogoutOption func(*logoutOperation) + + logoutOperation struct{} +) + +// Logout logs out of a registry +func (c *Client) Logout(host string, opts ...LogoutOption) error { + operation := &logoutOperation{} + for _, opt := range opts { + opt(operation) + } + if err := c.authorizer.Logout(ctx(c.out, c.debug), host); err != nil { + return err + } + fmt.Fprintf(c.out, "Removing login credentials for %s\n", host) + return nil +} + +type ( + // PullOption allows specifying various settings on pull + PullOption func(*pullOperation) + + // PullResult is the result returned upon successful pull. + PullResult struct { + Manifest *DescriptorPullSummary `json:"manifest"` + Config *DescriptorPullSummary `json:"config"` + Chart *DescriptorPullSummaryWithMeta `json:"chart"` + Prov *DescriptorPullSummary `json:"prov"` + Ref string `json:"ref"` + } + + DescriptorPullSummary struct { + Data []byte `json:"-"` + Digest string `json:"digest"` + Size int64 `json:"size"` + } + + DescriptorPullSummaryWithMeta struct { + DescriptorPullSummary + Meta *chart.Metadata `json:"meta"` + } + + pullOperation struct { + withChart bool + withProv bool + ignoreMissingProv bool + } +) + +// Pull downloads a chart from a registry +func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { + parsedRef, err := parseReference(ref) + if err != nil { + return nil, err + } + + operation := &pullOperation{ + withChart: true, // By default, always download the chart layer + } + for _, option := range options { + option(operation) + } + if !operation.withChart && !operation.withProv { + return nil, errors.New( + "must specify at least one layer to pull (chart/prov)") + } + memoryStore := content.NewMemory() + allowedMediaTypes := []string{ + ConfigMediaType, + } + minNumDescriptors := 1 // 1 for the config + if operation.withChart { + minNumDescriptors++ + allowedMediaTypes = append(allowedMediaTypes, ChartLayerMediaType, LegacyChartLayerMediaType) + } + if operation.withProv { + if !operation.ignoreMissingProv { + minNumDescriptors++ + } + allowedMediaTypes = append(allowedMediaTypes, ProvLayerMediaType) + } + + var descriptors, layers []ocispec.Descriptor + remotesResolver, err := c.resolver(parsedRef) + if err != nil { + return nil, err + } + registryStore := content.Registry{Resolver: remotesResolver} + + manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, parsedRef.String(), memoryStore, "", + oras.WithPullEmptyNameAllowed(), + oras.WithAllowedMediaTypes(allowedMediaTypes), + oras.WithLayerDescriptors(func(l []ocispec.Descriptor) { + layers = l + })) + if err != nil { + return nil, err + } + + descriptors = append(descriptors, manifest) + descriptors = append(descriptors, layers...) + + numDescriptors := len(descriptors) + if numDescriptors < minNumDescriptors { + return nil, fmt.Errorf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d", + minNumDescriptors, numDescriptors) + } + var configDescriptor *ocispec.Descriptor + var chartDescriptor *ocispec.Descriptor + var provDescriptor *ocispec.Descriptor + for _, descriptor := range descriptors { + d := descriptor + switch d.MediaType { + case ConfigMediaType: + configDescriptor = &d + case ChartLayerMediaType: + chartDescriptor = &d + case ProvLayerMediaType: + provDescriptor = &d + case LegacyChartLayerMediaType: + chartDescriptor = &d + fmt.Fprintf(c.out, "Warning: chart media type %s is deprecated\n", LegacyChartLayerMediaType) + } + } + if configDescriptor == nil { + return nil, fmt.Errorf("could not load config with mediatype %s", ConfigMediaType) + } + if operation.withChart && chartDescriptor == nil { + return nil, fmt.Errorf("manifest does not contain a layer with mediatype %s", + ChartLayerMediaType) + } + var provMissing bool + if operation.withProv && provDescriptor == nil { + if operation.ignoreMissingProv { + provMissing = true + } else { + return nil, fmt.Errorf("manifest does not contain a layer with mediatype %s", + ProvLayerMediaType) + } + } + result := &PullResult{ + Manifest: &DescriptorPullSummary{ + Digest: manifest.Digest.String(), + Size: manifest.Size, + }, + Config: &DescriptorPullSummary{ + Digest: configDescriptor.Digest.String(), + Size: configDescriptor.Size, + }, + Chart: &DescriptorPullSummaryWithMeta{}, + Prov: &DescriptorPullSummary{}, + Ref: parsedRef.String(), + } + var getManifestErr error + if _, manifestData, ok := memoryStore.Get(manifest); !ok { + getManifestErr = errors.Errorf("Unable to retrieve blob with digest %s", manifest.Digest) + } else { + result.Manifest.Data = manifestData + } + if getManifestErr != nil { + return nil, getManifestErr + } + var getConfigDescriptorErr error + if _, configData, ok := memoryStore.Get(*configDescriptor); !ok { + getConfigDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", configDescriptor.Digest) + } else { + result.Config.Data = configData + var meta *chart.Metadata + if err := json.Unmarshal(configData, &meta); err != nil { + return nil, err + } + result.Chart.Meta = meta + } + if getConfigDescriptorErr != nil { + return nil, getConfigDescriptorErr + } + if operation.withChart { + var getChartDescriptorErr error + if _, chartData, ok := memoryStore.Get(*chartDescriptor); !ok { + getChartDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", chartDescriptor.Digest) + } else { + result.Chart.Data = chartData + result.Chart.Digest = chartDescriptor.Digest.String() + result.Chart.Size = chartDescriptor.Size + } + if getChartDescriptorErr != nil { + return nil, getChartDescriptorErr + } + } + if operation.withProv && !provMissing { + var getProvDescriptorErr error + if _, provData, ok := memoryStore.Get(*provDescriptor); !ok { + getProvDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", provDescriptor.Digest) + } else { + result.Prov.Data = provData + result.Prov.Digest = provDescriptor.Digest.String() + result.Prov.Size = provDescriptor.Size + } + if getProvDescriptorErr != nil { + return nil, getProvDescriptorErr + } + } + + fmt.Fprintf(c.out, "Pulled: %s\n", result.Ref) + fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) + + if strings.Contains(result.Ref, "_") { + fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) + fmt.Fprint(c.out, registryUnderscoreMessage+"\n") + } + + return result, nil +} + +// PullOptWithChart returns a function that sets the withChart setting on pull +func PullOptWithChart(withChart bool) PullOption { + return func(operation *pullOperation) { + operation.withChart = withChart + } +} + +// PullOptWithProv returns a function that sets the withProv setting on pull +func PullOptWithProv(withProv bool) PullOption { + return func(operation *pullOperation) { + operation.withProv = withProv + } +} + +// PullOptIgnoreMissingProv returns a function that sets the ignoreMissingProv setting on pull +func PullOptIgnoreMissingProv(ignoreMissingProv bool) PullOption { + return func(operation *pullOperation) { + operation.ignoreMissingProv = ignoreMissingProv + } +} + +type ( + // PushOption allows specifying various settings on push + PushOption func(*pushOperation) + + // PushResult is the result returned upon successful push. + PushResult struct { + Manifest *descriptorPushSummary `json:"manifest"` + Config *descriptorPushSummary `json:"config"` + Chart *descriptorPushSummaryWithMeta `json:"chart"` + Prov *descriptorPushSummary `json:"prov"` + Ref string `json:"ref"` + } + + descriptorPushSummary struct { + Digest string `json:"digest"` + Size int64 `json:"size"` + } + + descriptorPushSummaryWithMeta struct { + descriptorPushSummary + Meta *chart.Metadata `json:"meta"` + } + + pushOperation struct { + provData []byte + strictMode bool + test bool + } +) + +// Push uploads a chart to a registry. +func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResult, error) { + parsedRef, err := parseReference(ref) + if err != nil { + return nil, err + } + + operation := &pushOperation{ + strictMode: true, // By default, enable strict mode + } + for _, option := range options { + option(operation) + } + meta, err := extractChartMeta(data) + if err != nil { + return nil, err + } + if operation.strictMode { + if !strings.HasSuffix(ref, fmt.Sprintf("/%s:%s", meta.Name, meta.Version)) { + return nil, errors.New( + "strict mode enabled, ref basename and tag must match the chart name and version") + } + } + memoryStore := content.NewMemory() + chartDescriptor, err := memoryStore.Add("", ChartLayerMediaType, data) + if err != nil { + return nil, err + } + + configData, err := json.Marshal(meta) + if err != nil { + return nil, err + } + + configDescriptor, err := memoryStore.Add("", ConfigMediaType, configData) + if err != nil { + return nil, err + } + + descriptors := []ocispec.Descriptor{chartDescriptor} + var provDescriptor ocispec.Descriptor + if operation.provData != nil { + provDescriptor, err = memoryStore.Add("", ProvLayerMediaType, operation.provData) + if err != nil { + return nil, err + } + + descriptors = append(descriptors, provDescriptor) + } + + ociAnnotations := generateOCIAnnotations(meta, operation.test) + + manifestData, manifest, err := content.GenerateManifest(&configDescriptor, ociAnnotations, descriptors...) + if err != nil { + return nil, err + } + + if err := memoryStore.StoreManifest(parsedRef.String(), manifest, manifestData); err != nil { + return nil, err + } + remotesResolver, err := c.resolver(parsedRef) + if err != nil { + return nil, err + } + registryStore := content.Registry{Resolver: remotesResolver} + _, err = oras.Copy(ctx(c.out, c.debug), memoryStore, parsedRef.String(), registryStore, "", + oras.WithNameValidation(nil)) + if err != nil { + return nil, err + } + chartSummary := &descriptorPushSummaryWithMeta{ + Meta: meta, + } + chartSummary.Digest = chartDescriptor.Digest.String() + chartSummary.Size = chartDescriptor.Size + result := &PushResult{ + Manifest: &descriptorPushSummary{ + Digest: manifest.Digest.String(), + Size: manifest.Size, + }, + Config: &descriptorPushSummary{ + Digest: configDescriptor.Digest.String(), + Size: configDescriptor.Size, + }, + Chart: chartSummary, + Prov: &descriptorPushSummary{}, // prevent nil references + Ref: parsedRef.String(), + } + if operation.provData != nil { + result.Prov = &descriptorPushSummary{ + Digest: provDescriptor.Digest.String(), + Size: provDescriptor.Size, + } + } + fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref) + fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) + if strings.Contains(parsedRef.Reference, "_") { + fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) + fmt.Fprint(c.out, registryUnderscoreMessage+"\n") + } + + return result, err +} + +// PushOptProvData returns a function that sets the prov bytes setting on push +func PushOptProvData(provData []byte) PushOption { + return func(operation *pushOperation) { + operation.provData = provData + } +} + +// PushOptStrictMode returns a function that sets the strictMode setting on push +func PushOptStrictMode(strictMode bool) PushOption { + return func(operation *pushOperation) { + operation.strictMode = strictMode + } +} + +// PushOptTest returns a function that sets whether test setting on push +func PushOptTest(test bool) PushOption { + return func(operation *pushOperation) { + operation.test = test + } +} + +// Tags provides a sorted list all semver compliant tags for a given repository +func (c *Client) Tags(ref string) ([]string, error) { + parsedReference, err := registry.ParseReference(ref) + if err != nil { + return nil, err + } + + repository := registryremote.Repository{ + Reference: parsedReference, + Client: c.registryAuthorizer, + PlainHTTP: c.plainHTTP, + } + + var registryTags []string + + registryTags, err = registry.Tags(ctx(c.out, c.debug), &repository) + if err != nil { + return nil, err + } + + var tagVersions []*semver.Version + for _, tag := range registryTags { + // Change underscore (_) back to plus (+) for Helm + // See https://github.com/helm/helm/issues/10166 + tagVersion, err := semver.StrictNewVersion(strings.ReplaceAll(tag, "_", "+")) + if err == nil { + tagVersions = append(tagVersions, tagVersion) + } + } + + // Sort the collection + sort.Sort(sort.Reverse(semver.Collection(tagVersions))) + + tags := make([]string, len(tagVersions)) + + for iTv, tv := range tagVersions { + tags[iTv] = tv.String() + } + + return tags, nil + +} diff --git a/pkg/registry/client_http_test.go b/pkg/registry/client_http_test.go new file mode 100644 index 000000000..872d19fc9 --- /dev/null +++ b/pkg/registry/client_http_test.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 registry + +import ( + "fmt" + "os" + "testing" + + "github.com/containerd/containerd/errdefs" + "github.com/stretchr/testify/suite" +) + +type HTTPRegistryClientTestSuite struct { + TestSuite +} + +func (suite *HTTPRegistryClientTestSuite) SetupSuite() { + // init test client + dockerRegistry := setup(&suite.TestSuite, false, false) + + // Start Docker registry + go dockerRegistry.ListenAndServe() +} + +func (suite *HTTPRegistryClientTestSuite) TearDownSuite() { + teardown(&suite.TestSuite) + os.RemoveAll(suite.WorkspaceDir) +} + +func (suite *HTTPRegistryClientTestSuite) Test_1_Push() { + testPush(&suite.TestSuite) +} + +func (suite *HTTPRegistryClientTestSuite) Test_2_Pull() { + testPull(&suite.TestSuite) +} + +func (suite *HTTPRegistryClientTestSuite) Test_3_Tags() { + testTags(&suite.TestSuite) +} + +func (suite *HTTPRegistryClientTestSuite) Test_4_ManInTheMiddle() { + ref := fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost) + + // returns content that does not match the expected digest + _, err := suite.RegistryClient.Pull(ref) + suite.NotNil(err) + suite.True(errdefs.IsFailedPrecondition(err)) +} + +func TestHTTPRegistryClientTestSuite(t *testing.T) { + suite.Run(t, new(HTTPRegistryClientTestSuite)) +} diff --git a/pkg/registry/client_insecure_tls_test.go b/pkg/registry/client_insecure_tls_test.go new file mode 100644 index 000000000..5ba79b2ea --- /dev/null +++ b/pkg/registry/client_insecure_tls_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 registry + +import ( + "os" + "testing" + + "github.com/stretchr/testify/suite" +) + +type InsecureTLSRegistryClientTestSuite struct { + TestSuite +} + +func (suite *InsecureTLSRegistryClientTestSuite) SetupSuite() { + // init test client + dockerRegistry := setup(&suite.TestSuite, true, true) + + // Start Docker registry + go dockerRegistry.ListenAndServe() +} + +func (suite *InsecureTLSRegistryClientTestSuite) TearDownSuite() { + teardown(&suite.TestSuite) + os.RemoveAll(suite.WorkspaceDir) +} + +func (suite *InsecureTLSRegistryClientTestSuite) Test_0_Login() { + err := suite.RegistryClient.Login(suite.DockerRegistryHost, + LoginOptBasicAuth("badverybad", "ohsobad"), + LoginOptInsecure(true)) + suite.NotNil(err, "error logging into registry with bad credentials") + + err = suite.RegistryClient.Login(suite.DockerRegistryHost, + LoginOptBasicAuth(testUsername, testPassword), + LoginOptInsecure(true)) + suite.Nil(err, "no error logging into registry with good credentials") +} + +func (suite *InsecureTLSRegistryClientTestSuite) Test_1_Push() { + testPush(&suite.TestSuite) +} + +func (suite *InsecureTLSRegistryClientTestSuite) Test_2_Pull() { + testPull(&suite.TestSuite) +} + +func (suite *InsecureTLSRegistryClientTestSuite) Test_3_Tags() { + testTags(&suite.TestSuite) +} + +func (suite *InsecureTLSRegistryClientTestSuite) Test_4_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 TestInsecureTLSRegistryClientTestSuite(t *testing.T) { + suite.Run(t, new(InsecureTLSRegistryClientTestSuite)) +} diff --git a/pkg/registry/client_tls_test.go b/pkg/registry/client_tls_test.go new file mode 100644 index 000000000..518cfced4 --- /dev/null +++ b/pkg/registry/client_tls_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 registry + +import ( + "os" + "testing" + + "github.com/stretchr/testify/suite" +) + +type TLSRegistryClientTestSuite struct { + TestSuite +} + +func (suite *TLSRegistryClientTestSuite) SetupSuite() { + // init test client + dockerRegistry := setup(&suite.TestSuite, true, false) + + // Start Docker registry + go dockerRegistry.ListenAndServe() +} + +func (suite *TLSRegistryClientTestSuite) TearDownSuite() { + teardown(&suite.TestSuite) + os.RemoveAll(suite.WorkspaceDir) +} + +func (suite *TLSRegistryClientTestSuite) Test_0_Login() { + err := suite.RegistryClient.Login(suite.DockerRegistryHost, + LoginOptBasicAuth("badverybad", "ohsobad"), + LoginOptTLSClientConfig(tlsCert, tlsKey, tlsCA)) + suite.NotNil(err, "error logging into registry with bad credentials") + + err = suite.RegistryClient.Login(suite.DockerRegistryHost, + LoginOptBasicAuth(testUsername, testPassword), + LoginOptTLSClientConfig(tlsCert, tlsKey, tlsCA)) + suite.Nil(err, "no error logging into registry with good credentials") +} + +func (suite *TLSRegistryClientTestSuite) Test_1_Push() { + testPush(&suite.TestSuite) +} + +func (suite *TLSRegistryClientTestSuite) Test_2_Pull() { + testPull(&suite.TestSuite) +} + +func (suite *TLSRegistryClientTestSuite) Test_3_Tags() { + testTags(&suite.TestSuite) +} + +func (suite *TLSRegistryClientTestSuite) Test_4_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 TestTLSRegistryClientTestSuite(t *testing.T) { + suite.Run(t, new(TLSRegistryClientTestSuite)) +} diff --git a/pkg/registry/constants.go b/pkg/registry/constants.go new file mode 100644 index 000000000..570b6f0d3 --- /dev/null +++ b/pkg/registry/constants.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 registry // import "helm.sh/helm/v3/pkg/registry" + +const ( + // OCIScheme is the URL scheme for OCI-based requests + OCIScheme = "oci" + + // CredentialsFileBasename is the filename for auth credentials file + CredentialsFileBasename = "registry/config.json" + + // ConfigMediaType is the reserved media type for the Helm chart manifest config + ConfigMediaType = "application/vnd.cncf.helm.config.v1+json" + + // ChartLayerMediaType is the reserved media type for Helm chart package content + ChartLayerMediaType = "application/vnd.cncf.helm.chart.content.v1.tar+gzip" + + // ProvLayerMediaType is the reserved media type for Helm chart provenance files + ProvLayerMediaType = "application/vnd.cncf.helm.chart.provenance.v1.prov" + + // LegacyChartLayerMediaType is the legacy reserved media type for Helm chart package content. + LegacyChartLayerMediaType = "application/tar+gzip" +) diff --git a/pkg/registry/testdata/tls/ca.crt b/pkg/registry/testdata/tls/ca.crt new file mode 100644 index 000000000..d5b845acb --- /dev/null +++ b/pkg/registry/testdata/tls/ca.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhzCCAm+gAwIBAgIUEtjKXd8LxpkQf3C5LgdzM1++R3swDQYJKoZIhvcNAQEL +BQAwUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjETMBEG +A1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwMQWNtZSBSb290IENBMB4XDTIzMDYw +ODEwNDkzOFoXDTI0MDYwNzEwNDkzOFowUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgM +AkdEMQswCQYDVQQHDAJTWjETMBEGA1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwM +QWNtZSBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApgrX +Lv3k3trxje2JEoqusYN67Z3byZg69djRatfdboS3JKoTIHtcY7MMLdfhjAK97/wv +BaIMuVNgueu4qH6bea7FCP8XWz2BYBrH2GcKjVrBMkUrlIzjG9gnohkeknJQvQvl +oVbqLgZJn0HQcZtsPDnLwfjWDZrNkFBtvPSIMaRQbmtOFdSqAQjLKezbwlznBCJ5 +qpLsgc67ttDW5QAS+GszWPmypUlw8Ih7m8J95eT9aUESP0DbdraeUktWJQTdqukd +NflLaA2ZoV+uTX+wVE4yyXgSjD3Sd93+XhoSSzDzkzRnLsocRutxrTiNC/1S+qhb +Z72XLk0bvNwQhJjHDQIDAQABo1MwUTAdBgNVHQ4EFgQUoSKAVvuJDGszE361K7IF +RXOVj2YwHwYDVR0jBBgwFoAUoSKAVvuJDGszE361K7IFRXOVj2YwDwYDVR0TAQH/ +BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOqH/JFuT1sqY/zVxCsATE1ze85/o +r6yPw3AuXsFzWtHe/XOFJzvbfOBWfocVLXTDc5933f1Ws/+PcxQKEQCwnUHrEAso +jLPzy+igHc07pi9PqHJ21Sn8FF5JVv+Y6CcZKaF5aEzUISsVjbF2vGK8FotMS9rs +Jw//dDfKhHjO9MHPBdkhOrM31LV6gwYPepno/YYygrJwHGQ5V9sdY8ifRBG6lX2a +xK4N2bl5q3Cpz+iERLNGP2c8OVQwLfSYLpFRSbHS8UiN4z6WqfgYHG7YurvbiMiJ +/AFkUatVJQ5YLmfCz4FMAiaxNtEOkZh5cvL1eCLK7nzvgAPCI33mEp6eoA== +-----END CERTIFICATE----- diff --git a/pkg/registry/testdata/tls/client.crt b/pkg/registry/testdata/tls/client.crt new file mode 100644 index 000000000..5b1daf278 --- /dev/null +++ b/pkg/registry/testdata/tls/client.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWzCCAkOgAwIBAgIUdJ6uRYm6RYesJ3CRoLokemFFgX8wDQYJKoZIhvcNAQEL +BQAwUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjETMBEG +A1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwMQWNtZSBSb290IENBMB4XDTIzMDYw +ODEwNTA0OFoXDTI0MDYwNzEwNTA0OFowWTELMAkGA1UEBhMCQ04xCzAJBgNVBAgM +AkdEMQswCQYDVQQHDAJTWjETMBEGA1UECgwKQWNtZSwgSW5jLjEbMBkGA1UEAwwS +aGVsbS10ZXN0LXJlZ2lzdHJ5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAxuVrOJyfUO71wlqe/ae8pNVf3z+6b7aCYRrKJ4l66RKMPz9uP5lHD9QImCTU +LddER48iRr5nzaUKqNUsPn4tTcdaH9EEra+PDp+YeToyZARO+coxCq8yt1NxXrlb +E/q9Ie9QUlruhthrgr+5DC+qogZA8kcVPOs2+ObqeCCO6QGpECxROO2ysXHyjy2b +nwGCzZRz90M4z0ifXcey9RLzbmEsYymq6RbaeQvdzevgXhzIANktILuB0D3wJ2ae +WWP2CfBrjaPbOBtzdDhyl4T1aqLiUpDELUJLVpf/h6xCh52Q0svpsGVGtyO+npPe +kZ1LSVAnVGS6JlWWhs7RL0eaPwIDAQABoyEwHzAdBgNVHREEFjAUghJoZWxtLXRl +c3QtcmVnaXN0cnkwDQYJKoZIhvcNAQELBQADggEBABbxtODFOAeTJg4Q3SXqJ8Gq +zh3/1DaAEnMGHILYuS9tK5lisTLiUerqeQaHKR6U90HK/P1vVxe7PvwfHBrVsGkR +4YC6nivf8LMySKBQmsPUHjdotNZZ8O1pqd+CMqZe2ZuvzLZ4pPdw25lKjhZ7qI+t +hQ8yotiJALzEUWLJSgP5Y8k4hFfRGSso1oAC+WppQeW6ITqDo1MrzH7gpjnp+CJG +NWM1oAQCB1qIdo6gY386w6yLyUhfHtAVa3vviQ0dkRLiK95He5xZcO11rlDNdmgF +cF6lElkci8gPuH8UkKAT5bP9dAEbHPSjAIvg5O9NviknLiNAdFRKeTri+hqNLhE= +-----END CERTIFICATE----- diff --git a/pkg/registry/testdata/tls/client.key b/pkg/registry/testdata/tls/client.key new file mode 100644 index 000000000..2f6a8aa12 --- /dev/null +++ b/pkg/registry/testdata/tls/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDG5Ws4nJ9Q7vXC +Wp79p7yk1V/fP7pvtoJhGsoniXrpEow/P24/mUcP1AiYJNQt10RHjyJGvmfNpQqo +1Sw+fi1Nx1of0QStr48On5h5OjJkBE75yjEKrzK3U3FeuVsT+r0h71BSWu6G2GuC +v7kML6qiBkDyRxU86zb45up4II7pAakQLFE47bKxcfKPLZufAYLNlHP3QzjPSJ9d +x7L1EvNuYSxjKarpFtp5C93N6+BeHMgA2S0gu4HQPfAnZp5ZY/YJ8GuNo9s4G3N0 +OHKXhPVqouJSkMQtQktWl/+HrEKHnZDSy+mwZUa3I76ek96RnUtJUCdUZLomVZaG +ztEvR5o/AgMBAAECggEBAKTaovRZXPOIHMrqsb0sun8lHEG+YJkXfRlfSw9aNDXa +2cPSn163fN7xr+3rGLKmKkHlsVNRnlgk46Dsj698hbBh+6FDbc1IJhrIzWgthHbB +23PO0rc4X6Dz2JParlLxELJ/2ONp2yqJVxMYNhiTqaqB5HLr1/6WNwo220CWO92D +vLz3rBHO5Vw5b5Y6Kt6MN6ciIHB2k+obhh4GQRJjUhvmmKCzbk1/R1PFYNwhhMN0 +Av6BdwFgngvNzJ8KMxGia7WJSvDYUk0++RRZ1esiZqwWRVCFFkm4Hj+gKJq6Xnz0 +a2nSvlC9k4GJvD9yY9VcDTJY+WsNN3Ny29gIFUeU9IECgYEA4norD3XakMthgOQk +3NE3HSvpZ22xtVgN9uN0b/JXbg7CLlYzn3tabpbQM/4uI6VG3Mk5Pk83QfKnr4W1 +aYO3YTEQ9B4g0eu3t4zfQOibY2+/Jb7Yfv/fH+pjkI26zYDQn61gsFdV9uxF7Pgu +NGNVe/eY+RkxEWsTtb40jcrbCgsCgYEA4NLWAdlrGKWZP5nLvM1hVB8r4WS82c0e +Orfyv2NhiqfRasARC1lQCqwbmCjb0c/eQiW7lJ7iSECc/8xW3HrJBYpG/tCxi9+m +SWxZXzRXDL8bmuoVvYeA/hFZayef5qCc8eiTYGQp6N5ozQHLXuPbNu7n6YSwvoU4 +ANrVBDRXxR0CgYEAmwbfhPS6iVT+yFjjNthrrqdJXQhElgrRfEfUg3DTEj4+A7P0 +IF4y1/KaUIzUjofrSuTfL1zQSW9OA6M2PCTymTAaF9CrzKZbGuTuSaMwAtASe0b5 +MW37EQDD6MZrsZJUvIjU38DY0m6Hqx9zmV7JvFMPPqxU30R5uHWbyderOmMCgYA5 +P3afIe3TaNeNCmyGtwWBli5mRnCQRVrdONnnQjckR3db52xvp15qWUjthfnzgyrl +TRZm0c5s94cC29WCbwGhF4Tcfee35ktBhwV66KkB5efxmonOqSJ/j4tlbcGZyGwu +bTqZ4OeLFJc7HKncj8jSRCNpoxAec22/SfnUCEARQQKBgAnwaN6kmGqIW2EsNOwB +DXCvG4HI9np5xN5Wo2dz7wqGtrt0TVtJ/PNBL3iadDLyPHahwoEVceFrQwqxjPsV +AoSwVDTdX96PKM/v/2ysw1JLf7UMT59mpxFoYiXCPn5Do4D1/25UfMOsJSmFo1Ij +Hkw1bqG8QneuME16BnDQfY3b +-----END PRIVATE KEY----- diff --git a/pkg/registry/testdata/tls/server.crt b/pkg/registry/testdata/tls/server.crt new file mode 100644 index 000000000..5fae09bb9 --- /dev/null +++ b/pkg/registry/testdata/tls/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWzCCAkOgAwIBAgIUdJ6uRYm6RYesJ3CRoLokemFFgX4wDQYJKoZIhvcNAQEL +BQAwUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjETMBEG +A1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwMQWNtZSBSb290IENBMB4XDTIzMDYw +ODEwNTAzM1oXDTI0MDYwNzEwNTAzM1owWTELMAkGA1UEBhMCQ04xCzAJBgNVBAgM +AkdEMQswCQYDVQQHDAJTWjETMBEGA1UECgwKQWNtZSwgSW5jLjEbMBkGA1UEAwwS +aGVsbS10ZXN0LXJlZ2lzdHJ5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA59jg4ml82uyvrg+tXf/0S8WHuayl5fB3k1lIPtOrTt5KBNh6z5XHZDogsQ3m +UEko4gVUvKL0Einm1i5c3C6KFFj0RNib0QpOZtxu54mx2Rxazkge0yjoTMwl/P1o +pvRI6qfRri8LdlqWwU9wBIYmKqEM8jPjxKcCOaR0WyQmEJ6KbayTzsVNHaQxG/f3 +aIDCkp3tFl+LaTJHjGdZN7tvJsZ1wXlQy6gXTJIPXHDTS/uh3Xp8jgqhlnQPIr44 +HikiAp9DMnOBGO4u4cZjCr04cQnLS9knsBAQCjja9J9DnZ5vKatBHF3nOVAtGoBM +o69HcYoX5F10Qg8YOa7QwIYjpQIDAQABoyEwHzAdBgNVHREEFjAUghJoZWxtLXRl +c3QtcmVnaXN0cnkwDQYJKoZIhvcNAQELBQADggEBABMYICc/rzijGhFPFOeSrXyk +xFX9SSrGMl0CzV44sxzJFJ89BrW9bUWf4rLuc2ugqWp78kRKGMKgaytDrmGGuZKy +Qy+xl3DTAoc9FYOBphtcH1QndWdbpKSc2sTKvdeV6SslKwWXlAvcqIain80fWAkn +J+9Fd/rq3sJxCYsYhEf17pDjHDnG5ZUsBAWWzN+YjtSAe4PzT1KdljUPCC1GbF+H +1dx+MwapV+atftzlGjld8H73MXrKRNUSZM5lEFvzCZz48J1Ml6UVnYO+QCybeJtQ +lBT3/wclJ86e0eNkZJI0WTmrqlaNS/J7mbZ+4BhfjuO5PyZbLg8DcWmaKeNtT8M= +-----END CERTIFICATE----- diff --git a/pkg/registry/testdata/tls/server.key b/pkg/registry/testdata/tls/server.key new file mode 100644 index 000000000..da44121a7 --- /dev/null +++ b/pkg/registry/testdata/tls/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDn2ODiaXza7K+u +D61d//RLxYe5rKXl8HeTWUg+06tO3koE2HrPlcdkOiCxDeZQSSjiBVS8ovQSKebW +LlzcLooUWPRE2JvRCk5m3G7nibHZHFrOSB7TKOhMzCX8/Wim9Ejqp9GuLwt2WpbB +T3AEhiYqoQzyM+PEpwI5pHRbJCYQnoptrJPOxU0dpDEb9/dogMKSne0WX4tpMkeM +Z1k3u28mxnXBeVDLqBdMkg9ccNNL+6HdenyOCqGWdA8ivjgeKSICn0Myc4EY7i7h +xmMKvThxCctL2SewEBAKONr0n0Odnm8pq0EcXec5UC0agEyjr0dxihfkXXRCDxg5 +rtDAhiOlAgMBAAECggEBAJ6kfFzwqYpz4lJMT+i+Nz+RzilyxaHtRSUCNrkmxVWW +LTfbmU1pw6IFVFFSnYHaTas60pyxNCkpmtZ7qvbOsZTyuVJSlWwYjUU9GHY+df+F +s2zrVIxQtYO3PVc7Xty+0xYd9xAlCMbXfciQvqmZ0Yvh36Xrc7MgRBmFOkkTFyjO +xaT70D5jwK0QKU8sMY+b9XvvaX59jbRmYAHL0wNcke/E7J4NKEAYfRI+x7kuFhP4 +yDbs9YE0u51cHYAGV4EujZhnv2AwvDnAWs0yHqIbVOIWI9+JRYKmPScr7b1bJfd/ +yy24GXvBu7Ss4TkfsJ/FdGXESr0Gj0ZIPIneDn/vrQECgYEA9jHu4FjTbRff+4tV +3zJJe88+yByjC6Hhj223JmRpCXQrXl2WLAYXl94p7M5NFdkD5QG7jsNUogLb73dV +ekUjuQl7IhJZYcRAXcnlkF+8pKt1duA0uRa22VtlR2wyn8oSnLV/9088Moh35sCP +MjWQDlZ/BW7YUPrOtB14eUCvMjECgYEA8RSpmXZVQdGnIIm6gC3rEhtfHQqAoBn0 +JRvnRXC/LKeVSgVF3ijeT9P/0JQuM9uxubV314nY+fhXsM5kkMZUoXMMSoxE+xPw +cgArpzwsleMn7BQ/UF3GLpdkUgNFI8bolZFbIa54F7YSFNto0NBp3mkceCJwoWmZ +BPIoo4zpV7UCgYEAviK2L8GqF5jWvPhRK300z0+xVu725ObywsijKB1oGYsEa26v +qfRSiFFl46M4WWUu4tBBv/IPDMhUf06UT0fSXPd7h0bQjPb6FvT0PFoT4MEiiNqD +HWbzdE5nm49uUYXIdgqed6tT/Fr07ttMPCStysT2eIWwvmnU9bnE7zALniECgYAr +HM7XqtnEU4HXx8macpu/OTXhM6ec+gc3O644NNl7WtzPx/GesSBQllEBM/6vN3Kp +C1LLMNOkoEzOSZqiaVVpKfHgwwTzAbXWLUGhPpmalGznQxevf5WZb2l5YSxUIZYm +aUAq3dCMLPs+z54G+b51D8cPlNkfhIrg34108hYooQKBgQDWMbc6wY6frvJCmesx +i7F/JHJweqcQdW649RCvtK8M/O062/3vvSNTxqEjPaJOGiD4Cn+D5pYchVujqlTM +8DK77N97NzQvpHm81lpKVIg5sObarvT3RnCSRpOumbX5SCBoBUs+nVC01/zZz79c +AJFLAeHI1RjhB0AFpRDCvZZk6w== +-----END PRIVATE KEY----- diff --git a/pkg/registry/util.go b/pkg/registry/util.go new file mode 100644 index 000000000..6fb1d0cda --- /dev/null +++ b/pkg/registry/util.go @@ -0,0 +1,277 @@ +/* +Copyright The 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/pkg/registry" + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + "net/http" + "strings" + "time" + + helmtime "helm.sh/helm/v3/pkg/time" + + "github.com/Masterminds/semver/v3" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + orascontext "oras.land/oras-go/pkg/context" + "oras.land/oras-go/pkg/registry" + + "helm.sh/helm/v3/internal/tlsutil" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" +) + +var immutableOciAnnotations = []string{ + ocispec.AnnotationVersion, + ocispec.AnnotationTitle, +} + +// IsOCI determines whether or not a URL is to be treated as an OCI URL +func IsOCI(url string) bool { + return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme)) +} + +// ContainsTag determines whether a tag is found in a provided list of tags +func ContainsTag(tags []string, tag string) bool { + for _, t := range tags { + if tag == t { + return true + } + } + return false +} + +func GetTagMatchingVersionOrConstraint(tags []string, versionString string) (string, error) { + var constraint *semver.Constraints + if versionString == "" { + // If string is empty, set wildcard constraint + constraint, _ = semver.NewConstraint("*") + } else { + // when customer input exact version, check whether have exact match + // one first + for _, v := range tags { + if versionString == v { + return v, nil + } + } + + // Otherwise set constraint to the string given + var err error + constraint, err = semver.NewConstraint(versionString) + if err != nil { + return "", err + } + } + + // Otherwise try to find the first available version matching the string, + // in case it is a constraint + for _, v := range tags { + test, err := semver.NewVersion(v) + if err != nil { + continue + } + if constraint.Check(test) { + return v, nil + } + } + + return "", errors.Errorf("Could not locate a version matching provided version string %s", versionString) +} + +// extractChartMeta is used to extract a chart metadata from a byte array +func extractChartMeta(chartData []byte) (*chart.Metadata, error) { + ch, err := loader.LoadArchive(bytes.NewReader(chartData)) + if err != nil { + return nil, err + } + return ch.Metadata, nil +} + +// 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 +} + +// parseReference will parse and validate the reference, and clean tags when +// applicable tags are only cleaned when plus (+) signs are present, and are +// converted to underscores (_) before pushing +// See https://github.com/helm/helm/issues/10166 +func parseReference(raw string) (registry.Reference, error) { + // The sole possible reference modification is replacing plus (+) signs + // present in tags with underscores (_). To do this properly, we first + // need to identify a tag, and then pass it on to the reference parser + // NOTE: Passing immediately to the reference parser will fail since (+) + // signs are an invalid tag character, and simply replacing all plus (+) + // occurrences could invalidate other portions of the URI + parts := strings.Split(raw, ":") + if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") { + tag := parts[len(parts)-1] + + if tag != "" { + // Replace any plus (+) signs with known underscore (_) conversion + newTag := strings.ReplaceAll(tag, "+", "_") + raw = strings.ReplaceAll(raw, tag, newTag) + } + } + + return registry.ParseReference(raw) +} + +// NewRegistryClientWithTLS is a helper function to create a new registry client with TLS enabled. +func NewRegistryClientWithTLS(out io.Writer, certFile, keyFile, caFile string, insecureSkipTLSverify bool, registryConfig string, debug bool) (*Client, error) { + tlsConf, err := tlsutil.NewClientTLS(certFile, keyFile, caFile, insecureSkipTLSverify) + if err != nil { + return nil, fmt.Errorf("can't create TLS config for client: %s", err) + } + // Create a new registry client + registryClient, err := NewClient( + ClientOptDebug(debug), + ClientOptEnableCache(true), + ClientOptWriter(out), + ClientOptCredentialsFile(registryConfig), + ClientOptHTTPClient(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConf, + }, + }), + ) + if err != nil { + return nil, err + } + return registryClient, nil +} + +// generateOCIAnnotations will generate OCI annotations to include within the OCI manifest +func generateOCIAnnotations(meta *chart.Metadata, test bool) map[string]string { + + // Get annotations from Chart attributes + ociAnnotations := generateChartOCIAnnotations(meta, test) + + // Copy Chart annotations +annotations: + for chartAnnotationKey, chartAnnotationValue := range meta.Annotations { + + // Avoid overriding key properties + for _, immutableOciKey := range immutableOciAnnotations { + if immutableOciKey == chartAnnotationKey { + continue annotations + } + } + + // Add chart annotation + ociAnnotations[chartAnnotationKey] = chartAnnotationValue + } + + return ociAnnotations +} + +// getChartOCIAnnotations will generate OCI annotations from the provided chart +func generateChartOCIAnnotations(meta *chart.Metadata, test bool) map[string]string { + chartOCIAnnotations := map[string]string{} + + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, meta.Description) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationTitle, meta.Name) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationVersion, meta.Version) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home) + + if !test { + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, helmtime.Now().UTC().Format(time.RFC3339)) + } + + if len(meta.Sources) > 0 { + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, meta.Sources[0]) + } + + if meta.Maintainers != nil && len(meta.Maintainers) > 0 { + var maintainerSb strings.Builder + + for maintainerIdx, maintainer := range meta.Maintainers { + + if len(maintainer.Name) > 0 { + maintainerSb.WriteString(maintainer.Name) + } + + if len(maintainer.Email) > 0 { + maintainerSb.WriteString(" (") + maintainerSb.WriteString(maintainer.Email) + maintainerSb.WriteString(")") + } + + if maintainerIdx < len(meta.Maintainers)-1 { + maintainerSb.WriteString(", ") + } + + } + + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationAuthors, maintainerSb.String()) + + } + + return chartOCIAnnotations +} + +// addToMap takes an existing map and adds an item if the value is not empty +func addToMap(inputMap map[string]string, newKey string, newValue string) map[string]string { + + // Add item to map if its + if len(strings.TrimSpace(newValue)) > 0 { + inputMap[newKey] = newValue + } + + return inputMap + +} + +// See 2 (end of page 4) https://www.ietf.org/rfc/rfc2617.txt +// "To receive authorization, the client sends the userid and password, +// separated by a single colon (":") character, within a base64 +// encoded string in the credentials." +// It is not meant to be urlencoded. +func basicAuth(username, password string) string { + auth := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) +} + +// authHeader generates an HTTP authorization header based on the provided +// username and password and sets it in the provided HTTP headers pointer. +// +// If both username and password are empty, no header is set. +// If only the password is provided, a "Bearer" token is created and set in +// the Authorization header. +// If both username and password are provided, a "Basic" authentication token +// is created using the basicAuth function, and set in the Authorization header. +func authHeader(username, password string, headers *http.Header) { + if username == "" && password == "" { + return + } + if username == "" { + headers.Set("Authorization", fmt.Sprintf("Bearer %s", password)) + return + } + headers.Set("Authorization", fmt.Sprintf("Basic %s", basicAuth(username, password))) +} diff --git a/pkg/registry/util_test.go b/pkg/registry/util_test.go new file mode 100644 index 000000000..f641801fe --- /dev/null +++ b/pkg/registry/util_test.go @@ -0,0 +1,315 @@ +/* +Copyright The 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/pkg/registry" + +import ( + "net/http" + "reflect" + "testing" + "time" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + "helm.sh/helm/v3/pkg/chart" + helmtime "helm.sh/helm/v3/pkg/time" +) + +func TestGenerateOCIChartAnnotations(t *testing.T) { + + tests := []struct { + name string + chart *chart.Metadata + expect map[string]string + }{ + { + "Baseline chart", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + }, + }, + { + "Simple chart values", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Home: "https://helm.sh", + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.url": "https://helm.sh", + }, + }, + { + "Maintainer without email", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Home: "https://helm.sh", + Maintainers: []*chart.Maintainer{ + { + Name: "John Snow", + }, + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.url": "https://helm.sh", + "org.opencontainers.image.authors": "John Snow", + }, + }, + { + "Maintainer with email", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Home: "https://helm.sh", + Maintainers: []*chart.Maintainer{ + {Name: "John Snow", Email: "john@winterfell.com"}, + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.url": "https://helm.sh", + "org.opencontainers.image.authors": "John Snow (john@winterfell.com)", + }, + }, + { + "Multiple Maintainers", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Home: "https://helm.sh", + Maintainers: []*chart.Maintainer{ + {Name: "John Snow", Email: "john@winterfell.com"}, + {Name: "Jane Snow"}, + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.url": "https://helm.sh", + "org.opencontainers.image.authors": "John Snow (john@winterfell.com), Jane Snow", + }, + }, + { + "Chart with Sources", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Sources: []string{ + "https://github.com/helm/helm", + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.source": "https://github.com/helm/helm", + }, + }, + } + + for _, tt := range tests { + + result := generateChartOCIAnnotations(tt.chart, true) + + if !reflect.DeepEqual(tt.expect, result) { + t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) + } + + } +} + +func TestGenerateOCIAnnotations(t *testing.T) { + + tests := []struct { + name string + chart *chart.Metadata + expect map[string]string + }{ + { + "Baseline chart", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + }, + }, + { + "Simple chart values with custom Annotations", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Annotations: map[string]string{ + "extrakey": "extravlue", + "anotherkey": "anothervalue", + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.description": "OCI Helm Chart", + "extrakey": "extravlue", + "anotherkey": "anothervalue", + }, + }, + { + "Verify Chart Name and Version cannot be overridden from annotations", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Annotations: map[string]string{ + "org.opencontainers.image.title": "badchartname", + "org.opencontainers.image.version": "1.0.0", + "extrakey": "extravlue", + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.description": "OCI Helm Chart", + "extrakey": "extravlue", + }, + }, + } + + for _, tt := range tests { + + result := generateOCIAnnotations(tt.chart, true) + + if !reflect.DeepEqual(tt.expect, result) { + t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) + } + + } +} + +func TestGenerateOCICreatedAnnotations(t *testing.T) { + chart := &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + } + + result := generateOCIAnnotations(chart, false) + + // Check that created annotation exists + if _, ok := result[ocispec.AnnotationCreated]; !ok { + t.Errorf("%s annotation not created", ocispec.AnnotationCreated) + } + + // Verify value of created artifact in RFC3339 format + if _, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { + t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) + } + +} + +func Test_basicAuth(t *testing.T) { + type args struct { + username string + password string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "Basic Auth", + args: args{ + username: "admin", + password: "passw0rd", + }, + want: "YWRtaW46cGFzc3cwcmQ=", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := basicAuth(tt.args.username, tt.args.password); got != tt.want { + t.Errorf("basicAuth() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_authHeader(t *testing.T) { + tests := []struct { + name string + username string + password string + expectedHeader http.Header + }{ + { + name: "basic login header with username and password", + username: "admin", + password: "passw0rd", + expectedHeader: func() http.Header { + header := http.Header{} + header.Set("Authorization", "Basic YWRtaW46cGFzc3cwcmQ=") + return header + }(), + }, + { + name: "bearer login header with no username and password", + username: "", + password: "hunter2", + expectedHeader: func() http.Header { + header := http.Header{} + header.Set("Authorization", "Bearer hunter2") + return header + }(), + }, + { + name: "no change in header with neither username nor password", + username: "", + password: "", + expectedHeader: http.Header{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := &http.Header{} + authHeader(tt.username, tt.password, got) + if !reflect.DeepEqual(*got, tt.expectedHeader) { + t.Errorf("authHeader got %#v wanted %#v", *got, tt.expectedHeader) + } + }) + } +} diff --git a/pkg/registry/utils_test.go b/pkg/registry/utils_test.go new file mode 100644 index 000000000..74aa0dbc0 --- /dev/null +++ b/pkg/registry/utils_test.go @@ -0,0 +1,393 @@ +/* +Copyright The 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" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/distribution/distribution/v3/configuration" + "github.com/distribution/distribution/v3/registry" + _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" + _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" + "github.com/foxcpp/go-mockdns" + "github.com/phayes/freeport" + "github.com/stretchr/testify/suite" + "golang.org/x/crypto/bcrypt" + + "helm.sh/helm/v3/internal/tlsutil" +) + +const ( + tlsServerKey = "./testdata/tls/server.key" + tlsServerCert = "./testdata/tls/server.crt" + tlsCA = "./testdata/tls/ca.crt" + tlsKey = "./testdata/tls/client.key" + tlsCert = "./testdata/tls/client.crt" +) + +var ( + testWorkspaceDir = "helm-registry-test" + testHtpasswdFileBasename = "authtest.htpasswd" + testUsername = "myuser" + testPassword = "mypass" +) + +type TestSuite struct { + suite.Suite + Out io.Writer + DockerRegistryHost string + CompromisedRegistryHost string + WorkspaceDir string + RegistryClient *Client + + // A mock DNS server needed for TLS connection testing. + srv *mockdns.Server +} + +func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry { + suite.WorkspaceDir = testWorkspaceDir + os.RemoveAll(suite.WorkspaceDir) + os.Mkdir(suite.WorkspaceDir, 0700) + + var ( + out bytes.Buffer + err error + ) + suite.Out = &out + credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename) + + // init test client + opts := []ClientOption{ + ClientOptDebug(true), + ClientOptEnableCache(true), + ClientOptWriter(suite.Out), + ClientOptCredentialsFile(credentialsFile), + ClientOptResolver(nil), + } + + if tlsEnabled { + var tlsConf *tls.Config + if insecure { + tlsConf, err = tlsutil.NewClientTLS("", "", "", true) + } else { + tlsConf, err = tlsutil.NewClientTLS(tlsCert, tlsKey, tlsCA, false) + } + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConf, + }, + } + suite.Nil(err, "no error loading tls config") + opts = append(opts, ClientOptHTTPClient(httpClient)) + } else { + opts = append(opts, ClientOptPlainHTTP()) + } + + suite.RegistryClient, err = NewClient(opts...) + 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.WorkspaceDir, testHtpasswdFileBasename) + err = os.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") + + // Change the registry host to another host which is not localhost. + // This is required because Docker enforces HTTP if the registry + // host is localhost/127.0.0.1. + suite.DockerRegistryHost = fmt.Sprintf("helm-test-registry:%d", port) + suite.srv, _ = mockdns.NewServer(map[string]mockdns.Zone{ + "helm-test-registry.": { + A: []string{"127.0.0.1"}, + }, + }, false) + suite.srv.PatchNet(net.DefaultResolver) + + 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{}{}} + + // Basic auth is not possible if we are serving HTTP. + if tlsEnabled { + config.Auth = configuration.Auth{ + "htpasswd": configuration.Parameters{ + "realm": "localhost", + "path": htpasswdPath, + }, + } + } + + // config tls + if tlsEnabled { + // TLS config + // this set tlsConf.ClientAuth = tls.RequireAndVerifyClientCert in the + // server tls config + config.HTTP.TLS.Certificate = tlsServerCert + config.HTTP.TLS.Key = tlsServerKey + // Skip client authentication if the registry is insecure. + if !insecure { + config.HTTP.TLS.ClientCAs = []string{tlsCA} + } + } + dockerRegistry, err := registry.NewRegistry(context.Background(), config) + suite.Nil(err, "no error creating test registry") + + suite.CompromisedRegistryHost = initCompromisedRegistryTestServer() + return dockerRegistry +} + +func teardown(suite *TestSuite) { + if suite.srv != nil { + mockdns.UnpatchNet(net.DefaultResolver) + suite.srv.Close() + } +} + +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( + fmt.Sprintf(`{ "schemaVersion": 2, "config": { + "mediaType": "%s", + "digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133", + "size": 181 + }, + "layers": [ + { + "mediaType": "%s", + "digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", + "size": 1 + } + ] +}`, ConfigMediaType, ChartLayerMediaType))) + } 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", ChartLayerMediaType) + w.WriteHeader(200) + w.Write([]byte("b")) + } else { + w.WriteHeader(500) + } + })) + + u, _ := url.Parse(s.URL) + return fmt.Sprintf("localhost:%s", u.Port()) +} + +func testPush(suite *TestSuite) { + // Bad bytes + ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost) + _, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptTest(true)) + suite.NotNil(err, "error pushing non-chart bytes") + + // Load a test chart + chartData, err := os.ReadFile("../repo/repotest/testdata/examplechart-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err := extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + + // non-strict ref (chart name) + ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true)) + suite.NotNil(err, "error pushing non-strict ref (bad basename)") + + // non-strict ref (chart name), with strict mode disabled + _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptTest(true)) + suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled") + + // non-strict ref (chart version) + ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true)) + suite.NotNil(err, "error pushing non-strict ref (bad tag)") + + // non-strict ref (chart version), with strict mode disabled + _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptTest(true)) + suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled") + + // basic push, good ref + chartData, err = os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err = extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true)) + suite.Nil(err, "no error pushing good ref") + + _, err = suite.RegistryClient.Pull(ref) + suite.Nil(err, "no error pulling a simple chart") + + // Load another test chart + chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err = extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + + // Load prov file + provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") + suite.Nil(err, "no error loading test prov") + + // push with prov + ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) + result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptTest(true)) + suite.Nil(err, "no error pushing good ref with prov") + + _, err = suite.RegistryClient.Pull(ref) + suite.Nil(err, "no error pulling a simple chart") + + // Validate the output + // Note: these digests/sizes etc may change if the test chart/prov files are modified, + // or if the format of the OCI manifest changes + suite.Equal(ref, result.Ref) + suite.Equal(meta.Name, result.Chart.Meta.Name) + suite.Equal(meta.Version, result.Chart.Meta.Version) + suite.Equal(int64(684), result.Manifest.Size) + suite.Equal(int64(99), result.Config.Size) + suite.Equal(int64(973), result.Chart.Size) + suite.Equal(int64(695), result.Prov.Size) + suite.Equal( + "sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6", + result.Manifest.Digest) + suite.Equal( + "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", + result.Config.Digest) + suite.Equal( + "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", + result.Chart.Digest) + suite.Equal( + "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256", + result.Prov.Digest) +} + +func testPull(suite *TestSuite) { + // bad/missing ref + ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost) + _, err := suite.RegistryClient.Pull(ref) + suite.NotNil(err, "error on bad/missing ref") + + // Load test chart (to build ref pushed in previous test) + chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err := extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) + + // Simple pull, chart only + _, err = suite.RegistryClient.Pull(ref) + suite.Nil(err, "no error pulling a simple chart") + + // Simple pull with prov (no prov uploaded) + _, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true)) + suite.NotNil(err, "error pulling a chart with prov when no prov exists") + + // Simple pull with prov, ignoring missing prov + _, err = suite.RegistryClient.Pull(ref, + PullOptWithProv(true), + PullOptIgnoreMissingProv(true)) + suite.Nil(err, + "no error pulling a chart with prov when no prov exists, ignoring missing") + + // Load test chart (to build ref pushed in previous test) + chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err = extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) + + // Load prov file + provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") + suite.Nil(err, "no error loading test prov") + + // no chart and no prov causes error + _, err = suite.RegistryClient.Pull(ref, + PullOptWithChart(false), + PullOptWithProv(false)) + suite.NotNil(err, "error on both no chart and no prov") + + // full pull with chart and prov + result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true)) + suite.Nil(err, "no error pulling a chart with prov") + + // Validate the output + // Note: these digests/sizes etc may change if the test chart/prov files are modified, + // or if the format of the OCI manifest changes + suite.Equal(ref, result.Ref) + suite.Equal(meta.Name, result.Chart.Meta.Name) + suite.Equal(meta.Version, result.Chart.Meta.Version) + suite.Equal(int64(684), result.Manifest.Size) + suite.Equal(int64(99), result.Config.Size) + suite.Equal(int64(973), result.Chart.Size) + suite.Equal(int64(695), result.Prov.Size) + suite.Equal( + "sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6", + result.Manifest.Digest) + suite.Equal( + "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", + result.Config.Digest) + suite.Equal( + "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", + result.Chart.Digest) + suite.Equal( + "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256", + result.Prov.Digest) + suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}", + string(result.Manifest.Data)) + suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}", + string(result.Config.Data)) + suite.Equal(chartData, result.Chart.Data) + suite.Equal(provData, result.Prov.Data) +} + +func testTags(suite *TestSuite) { + // Load test chart (to build ref pushed in previous test) + chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err := extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + ref := fmt.Sprintf("%s/testrepo/%s", suite.DockerRegistryHost, meta.Name) + + // Query for tags and validate length + tags, err := suite.RegistryClient.Tags(ref) + suite.Nil(err, "no error retrieving tags") + suite.Equal(1, len(tags)) +} diff --git a/pkg/release/info.go b/pkg/release/info.go index 0cb2bab64..b030a8a54 100644 --- a/pkg/release/info.go +++ b/pkg/release/info.go @@ -16,6 +16,8 @@ limitations under the License. package release import ( + "k8s.io/apimachinery/pkg/runtime" + "helm.sh/helm/v3/pkg/time" ) @@ -33,4 +35,6 @@ type Info struct { Status Status `json:"status,omitempty"` // Contains the rendered templates/NOTES.txt if available Notes string `json:"notes,omitempty"` + // Contains the deployed resources information + Resources map[string][]runtime.Object `json:"resources,omitempty"` } diff --git a/pkg/releaseutil/kind_sorter.go b/pkg/releaseutil/kind_sorter.go index a340dfc29..b5d75b88b 100644 --- a/pkg/releaseutil/kind_sorter.go +++ b/pkg/releaseutil/kind_sorter.go @@ -29,6 +29,7 @@ type KindSortOrder []string // // Those occurring earlier in the list get installed before those occurring later in the list. var InstallOrder KindSortOrder = []string{ + "PriorityClass", "Namespace", "NetworkPolicy", "ResourceQuota", @@ -61,6 +62,7 @@ var InstallOrder KindSortOrder = []string{ "StatefulSet", "Job", "CronJob", + "IngressClass", "Ingress", "APIService", } @@ -71,6 +73,7 @@ var InstallOrder KindSortOrder = []string{ var UninstallOrder KindSortOrder = []string{ "APIService", "Ingress", + "IngressClass", "Service", "CronJob", "Job", @@ -103,6 +106,7 @@ var UninstallOrder KindSortOrder = []string{ "ResourceQuota", "NetworkPolicy", "Namespace", + "PriorityClass", } // sort manifests by kind. diff --git a/pkg/releaseutil/kind_sorter_test.go b/pkg/releaseutil/kind_sorter_test.go index 71d355210..9e24c4399 100644 --- a/pkg/releaseutil/kind_sorter_test.go +++ b/pkg/releaseutil/kind_sorter_test.go @@ -25,6 +25,10 @@ import ( func TestKindSorter(t *testing.T) { manifests := []Manifest{ + { + Name: "U", + Head: &SimpleHead{Kind: "IngressClass"}, + }, { Name: "E", Head: &SimpleHead{Kind: "SecretList"}, @@ -165,6 +169,10 @@ func TestKindSorter(t *testing.T) { Name: "x", Head: &SimpleHead{Kind: "HorizontalPodAutoscaler"}, }, + { + Name: "F", + Head: &SimpleHead{Kind: "PriorityClass"}, + }, } for _, test := range []struct { @@ -172,8 +180,8 @@ func TestKindSorter(t *testing.T) { order KindSortOrder expected string }{ - {"install", InstallOrder, "aAbcC3deEf1gh2iIjJkKlLmnopqrxstuvw!"}, - {"uninstall", UninstallOrder, "wvmutsxrqponLlKkJjIi2hg1fEed3CcbAa!"}, + {"install", InstallOrder, "FaAbcC3deEf1gh2iIjJkKlLmnopqrxstuUvw!"}, + {"uninstall", UninstallOrder, "wvUmutsxrqponLlKkJjIi2hg1fEed3CcbAaF!"}, } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { diff --git a/pkg/releaseutil/manifest_sorter.go b/pkg/releaseutil/manifest_sorter.go index e83414500..413de30e2 100644 --- a/pkg/releaseutil/manifest_sorter.go +++ b/pkg/releaseutil/manifest_sorter.go @@ -117,19 +117,19 @@ func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering // // To determine hook type, it looks for a YAML structure like this: // -// kind: SomeKind -// apiVersion: v1 -// metadata: -// annotations: -// helm.sh/hook: pre-install +// 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 +// 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 diff --git a/pkg/repo/chartrepo.go b/pkg/repo/chartrepo.go index 67ede93fd..d9022ee6e 100644 --- a/pkg/repo/chartrepo.go +++ b/pkg/repo/chartrepo.go @@ -21,11 +21,10 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io/ioutil" + "io" "log" "net/url" "os" - "path" "path/filepath" "strings" @@ -116,15 +115,11 @@ func (r *ChartRepository) Load() error { // DownloadIndexFile fetches the index from a repository. func (r *ChartRepository) DownloadIndexFile() (string, error) { - parsedURL, err := url.Parse(r.Config.URL) + indexURL, err := ResolveReferenceURL(r.Config.URL, "index.yaml") if err != nil { return "", err } - 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), @@ -136,7 +131,7 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) { return "", err } - index, err := ioutil.ReadAll(resp) + index, err := io.ReadAll(resp) if err != nil { return "", err } @@ -153,12 +148,12 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) { } chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) os.MkdirAll(filepath.Dir(chartsFile), 0755) - ioutil.WriteFile(chartsFile, []byte(charts.String()), 0644) + os.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) + return fname, os.WriteFile(fname, index, 0644) } // Index generates an index for the chart repository and writes an index.yaml file. @@ -175,7 +170,7 @@ func (r *ChartRepository) saveIndexFile() error { if err != nil { return err } - return ioutil.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644) + return os.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644) } func (r *ChartRepository) generateIndex() error { @@ -219,7 +214,7 @@ func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion // 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) { - return FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, false, false, getters) + return FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, insecureSkipTLSverify, false, getters) } // FindChartInAuthAndTLSAndPassRepoURL finds chart in chart repository pointed by repoURL @@ -253,6 +248,10 @@ func FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, if err != nil { return "", errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", repoURL) } + defer func() { + os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name))) + os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name))) + }() // Read the index file for the repository to get chart information and return chart URL repoIndex, err := LoadIndexFile(idx) @@ -286,19 +285,27 @@ func FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, // ResolveReferenceURL resolves refURL relative to baseURL. // If refURL is absolute, it simply returns refURL. func ResolveReferenceURL(baseURL, refURL string) (string, error) { - parsedBaseURL, err := url.Parse(baseURL) + parsedRefURL, err := url.Parse(refURL) if err != nil { - return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL) + return "", errors.Wrapf(err, "failed to parse %s as URL", refURL) } - parsedRefURL, err := url.Parse(refURL) + if parsedRefURL.IsAbs() { + return refURL, nil + } + + parsedBaseURL, err := url.Parse(baseURL) if err != nil { - return "", errors.Wrapf(err, "failed to parse %s as URL", refURL) + return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL) } // We need a trailing slash for ResolveReference to work, but make sure there isn't already one + parsedBaseURL.RawPath = strings.TrimSuffix(parsedBaseURL.RawPath, "/") + "/" parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/" - return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil + + resolvedURL := parsedBaseURL.ResolveReference(parsedRefURL) + resolvedURL.RawQuery = parsedBaseURL.RawQuery + return resolvedURL.String(), nil } func (e *Entry) String() string { diff --git a/pkg/repo/chartrepo_test.go b/pkg/repo/chartrepo_test.go index 85401284e..4d4395c2d 100644 --- a/pkg/repo/chartrepo_test.go +++ b/pkg/repo/chartrepo_test.go @@ -18,19 +18,18 @@ package repo import ( "bytes" - "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" + "runtime" "strings" "testing" "time" "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" @@ -147,10 +146,9 @@ func TestIndexCustomSchemeDownload(t *testing.T) { if err != nil { t.Fatalf("Problem loading chart repository from %s: %v", repoURL, err) } - repo.CachePath = ensure.TempDir(t) - defer os.RemoveAll(repo.CachePath) + repo.CachePath = t.TempDir() - tempIndexFile, err := ioutil.TempFile("", "test-repo") + tempIndexFile, err := os.CreateTemp("", "test-repo") if err != nil { t.Fatalf("Failed to create temp index file: %v", err) } @@ -265,7 +263,7 @@ func verifyIndex(t *testing.T, actual *IndexFile) { // startLocalServerForTests Start the local helm server func startLocalServerForTests(handler http.Handler) (*httptest.Server, error) { if handler == nil { - fileBytes, err := ioutil.ReadFile("testdata/local-index.yaml") + fileBytes, err := os.ReadFile("testdata/local-index.yaml") if err != nil { return nil, err } @@ -280,7 +278,7 @@ func startLocalServerForTests(handler http.Handler) (*httptest.Server, error) { // 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") + fileBytes, err := os.ReadFile("testdata/local-index.yaml") if err != nil { return nil, err } @@ -309,8 +307,15 @@ func TestFindChartInAuthAndTLSAndPassRepoURL(t *testing.T) { // If the insecureSkipTLsverify is false, it will return an error that contains "x509: certificate signed by unknown authority". _, err = FindChartInAuthAndTLSAndPassRepoURL(srv.URL, "", "", "nginx", "0.1.0", "", "", "", false, false, getter.All(&cli.EnvSettings{})) - - if !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { + // Go communicates with the platform and different platforms return different messages. Go itself tests darwin + // differently for its message. On newer versions of Darwin the message includes the "Acme Co" portion while older + // versions of Darwin do not. As there are people developing Helm using both old and new versions of Darwin we test + // for both messages. + if runtime.GOOS == "darwin" { + if !strings.Contains(err.Error(), "x509: “Acme Co” certificate is not trusted") && !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { + t.Errorf("Expected TLS error for function FindChartInAuthAndTLSAndPassRepoURL not found, but got a different error (%v)", err) + } + } else if !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { t.Errorf("Expected TLS error for function FindChartInAuthAndTLSAndPassRepoURL not found, but got a different error (%v)", err) } } @@ -342,7 +347,7 @@ func TestFindChartInRepoURL(t *testing.T) { func TestErrorFindChartInRepoURL(t *testing.T) { g := getter.All(&cli.EnvSettings{ - RepositoryCache: ensure.TempDir(t), + RepositoryCache: t.TempDir(), }) if _, err := FindChartInRepoURL("http://someserver/something", "nginx", "", "", "", "", g); err == nil { @@ -377,27 +382,21 @@ func TestErrorFindChartInRepoURL(t *testing.T) { } func TestResolveReferenceURL(t *testing.T) { - chartURL, err := ResolveReferenceURL("http://localhost:8123/charts/", "nginx-0.2.0.tgz") - if err != nil { - t.Errorf("%s", err) - } - if chartURL != "http://localhost:8123/charts/nginx-0.2.0.tgz" { - t.Errorf("%s", chartURL) - } - - 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://charts.helm.sh/stable/nginx-0.2.0.tgz" { - t.Errorf("%s", chartURL) + for _, tt := range []struct { + baseURL, refURL, chartURL string + }{ + {"http://localhost:8123/charts/", "nginx-0.2.0.tgz", "http://localhost:8123/charts/nginx-0.2.0.tgz"}, + {"http://localhost:8123/charts-with-no-trailing-slash", "nginx-0.2.0.tgz", "http://localhost:8123/charts-with-no-trailing-slash/nginx-0.2.0.tgz"}, + {"http://localhost:8123", "https://charts.helm.sh/stable/nginx-0.2.0.tgz", "https://charts.helm.sh/stable/nginx-0.2.0.tgz"}, + {"http://localhost:8123/charts%2fwith%2fescaped%2fslash", "nginx-0.2.0.tgz", "http://localhost:8123/charts%2fwith%2fescaped%2fslash/nginx-0.2.0.tgz"}, + {"http://localhost:8123/charts?with=queryparameter", "nginx-0.2.0.tgz", "http://localhost:8123/charts/nginx-0.2.0.tgz?with=queryparameter"}, + } { + chartURL, err := ResolveReferenceURL(tt.baseURL, tt.refURL) + if err != nil { + t.Errorf("unexpected error in ResolveReferenceURL(%q, %q): %s", tt.baseURL, tt.refURL, err) + } + if chartURL != tt.chartURL { + t.Errorf("expected ResolveReferenceURL(%q, %q) to equal %q, got %q", tt.baseURL, tt.refURL, tt.chartURL, chartURL) + } } } diff --git a/pkg/repo/doc.go b/pkg/repo/doc.go index 05650100b..fc54bbf7a 100644 --- a/pkg/repo/doc.go +++ b/pkg/repo/doc.go @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package repo implements the Helm Chart Repository. +/* +Package repo implements the Helm Chart Repository. A chart repository is an HTTP server that provides information on charts. A local repository cache is an on-disk representation of a chart repository. @@ -83,9 +84,9 @@ The format of a repository.yaml file is: This file maps three bits of information about a repository: - - The name the user uses to refer to it - - The fully qualified URL to the repository (index.yaml will be appended) - - The name of the local cachefile + - The name the user uses to refer to it + - The fully qualified URL to the repository (index.yaml will be appended) + - The name of the local cachefile The format for both files was changed after Helm v2.0.0-Alpha.4. Helm is not backwards compatible with those earlier versions. diff --git a/pkg/repo/index.go b/pkg/repo/index.go index e86b17349..8a23ba060 100644 --- a/pkg/repo/index.go +++ b/pkg/repo/index.go @@ -18,7 +18,7 @@ package repo import ( "bytes" - "io/ioutil" + "encoding/json" "log" "os" "path" @@ -50,6 +50,8 @@ var ( ErrNoChartVersion = errors.New("no chart version found") // ErrNoChartName indicates that a chart with the given name is not found. ErrNoChartName = errors.New("no chart name found") + // ErrEmptyIndexYaml indicates that the content of index.yaml is empty. + ErrEmptyIndexYaml = errors.New("empty index.yaml file") ) // ChartVersions is a list of versioned chart references. @@ -102,7 +104,7 @@ func NewIndexFile() *IndexFile { // LoadIndexFile takes a file at the given path and returns an IndexFile object func LoadIndexFile(path string) (*IndexFile, error) { - b, err := ioutil.ReadFile(path) + b, err := os.ReadFile(path) if err != nil { return nil, err } @@ -116,6 +118,10 @@ func LoadIndexFile(path string) (*IndexFile, error) { // MustAdd adds a file to the index // This can leave the index in an unsorted state func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) error { + if i.Entries == nil { + return errors.New("entries not initialized") + } + if md.APIVersion == "" { md.APIVersion = chart.APIVersionV1 } @@ -227,6 +233,18 @@ func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode) } +// WriteJSONFile writes an index file in JSON format to the given destination +// path. +// +// The mode on the file is set to 'mode'. +func (i IndexFile) WriteJSONFile(dest string, mode os.FileMode) error { + b, err := json.MarshalIndent(i, "", " ") + if err != nil { + return err + } + return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode) +} + // Merge merges the given index file into this index. // // This merges by name and version. @@ -326,12 +344,21 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) { // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. func loadIndex(data []byte, source string) (*IndexFile, error) { i := &IndexFile{} - if err := yaml.UnmarshalStrict(data, i); err != nil { + + if len(data) == 0 { + return i, ErrEmptyIndexYaml + } + + if err := jsonOrYamlUnmarshal(data, i); err != nil { return i, err } for name, cvs := range i.Entries { for idx := len(cvs) - 1; idx >= 0; idx-- { + if cvs[idx] == nil { + log.Printf("skipping loading invalid entry for chart %q from %s: empty entry", name, source) + continue + } if cvs[idx].APIVersion == "" { cvs[idx].APIVersion = chart.APIVersionV1 } @@ -347,3 +374,17 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { } return i, nil } + +// jsonOrYamlUnmarshal unmarshals the given byte slice containing JSON or YAML +// into the provided interface. +// +// It automatically detects whether the data is in JSON or YAML format by +// checking its validity as JSON. If the data is valid JSON, it will use the +// `encoding/json` package to unmarshal it. Otherwise, it will use the +// `sigs.k8s.io/yaml` package to unmarshal the YAML data. +func jsonOrYamlUnmarshal(b []byte, i interface{}) error { + if json.Valid(b) { + return json.Unmarshal(b, i) + } + return yaml.UnmarshalStrict(b, i) +} diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go index 47f3c6b2e..efb50ba6a 100644 --- a/pkg/repo/index_test.go +++ b/pkg/repo/index_test.go @@ -19,7 +19,7 @@ package repo import ( "bufio" "bytes" - "io/ioutil" + "encoding/json" "net/http" "os" "path/filepath" @@ -38,6 +38,7 @@ const ( annotationstestfile = "testdata/local-index-annotations.yaml" chartmuseumtestfile = "testdata/chartmuseum-index.yaml" unorderedTestfile = "testdata/local-index-unordered.yaml" + jsonTestfile = "testdata/local-index.json" testRepo = "test-repo" indexWithDuplicates = ` apiVersion: v1 @@ -59,6 +60,15 @@ entries: version: 1.0.0 home: https://github.com/something digest: "sha256:1234567890abcdef" +` + indexWithEmptyEntry = ` +apiVersion: v1 +entries: + grafana: + - apiVersion: v2 + name: grafana + foo: + - ` ) @@ -76,6 +86,8 @@ func TestIndexFile(t *testing.T) { {&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"}, + {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.8"}, "setter-0.1.8.tgz", "http://example.com/charts", "sha256:1234567890abc"}, + {&chart.Metadata{APIVersion: "v2", Name: "setter", Version: "0.1.8+beta"}, "setter-0.1.8+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) @@ -114,6 +126,11 @@ func TestIndexFile(t *testing.T) { if err != nil || cv.Metadata.Version != "0.1.9+alpha" { t.Errorf("Expected version: 0.1.9+alpha") } + + cv, err = i.Get("setter", "0.1.8") + if err != nil || cv.Metadata.Version != "0.1.8" { + t.Errorf("Expected version: 0.1.8") + } } func TestLoadIndex(t *testing.T) { @@ -130,6 +147,10 @@ func TestLoadIndex(t *testing.T) { Name: "chartmuseum index file", Filename: chartmuseumtestfile, }, + { + Name: "JSON index file", + Filename: jsonTestfile, + }, } for _, tc := range tests { @@ -152,6 +173,18 @@ func TestLoadIndex_Duplicates(t *testing.T) { } } +func TestLoadIndex_EmptyEntry(t *testing.T) { + if _, err := loadIndex([]byte(indexWithEmptyEntry), "indexWithEmptyEntry"); err != nil { + t.Errorf("unexpected error: %s", err) + } +} + +func TestLoadIndex_Empty(t *testing.T) { + if _, err := loadIndex([]byte(""), "indexWithEmpty"); err == nil { + t.Errorf("Expected an error when index.yaml is empty.") + } +} + func TestLoadIndexFileAnnotations(t *testing.T) { i, err := LoadIndexFile(annotationstestfile) if err != nil { @@ -202,14 +235,15 @@ func TestMerge(t *testing.T) { if len(ind1.Entries) != 2 { t.Errorf("Expected 2 entries, got %d", len(ind1.Entries)) - vs := ind1.Entries["dreadnought"] - if len(vs) != 2 { - t.Errorf("Expected 2 versions, got %d", len(vs)) - } - v := vs[0] - if v.Version != "0.2.0" { - t.Errorf("Expected %q version to be 0.2.0, got %s", v.Name, v.Version) - } + } + + vs := ind1.Entries["dreadnought"] + if len(vs) != 2 { + t.Errorf("Expected 2 versions, got %d", len(vs)) + } + + if v := vs[1]; v.Version != "0.2.0" { + t.Errorf("Expected %q version to be 0.2.0, got %s", v.Name, v.Version) } } @@ -251,7 +285,7 @@ func TestDownloadIndexFile(t *testing.T) { t.Fatalf("error finding created charts file: %#v", err) } - b, err := ioutil.ReadFile(idx) + b, err := os.ReadFile(idx) if err != nil { t.Fatalf("error reading charts file: %#v", err) } @@ -260,7 +294,7 @@ func TestDownloadIndexFile(t *testing.T) { 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") + fileBytes, err := os.ReadFile("testdata/local-index.yaml") if err != nil { t.Fatal(err) } @@ -304,7 +338,7 @@ func TestDownloadIndexFile(t *testing.T) { t.Fatalf("error finding created charts file: %#v", err) } - b, err := ioutil.ReadFile(idx) + b, err := os.ReadFile(idx) if err != nil { t.Fatalf("error reading charts file: %#v", err) } @@ -507,19 +541,54 @@ func TestIndexWrite(t *testing.T) { 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) } - dir, err := ioutil.TempDir("", "helm-tmp") + dir := t.TempDir() + testpath := filepath.Join(dir, "test") + i.WriteFile(testpath, 0600) + + got, err := os.ReadFile(testpath) if err != nil { t.Fatal(err) } - defer os.RemoveAll(dir) + if !strings.Contains(string(got), "clipper-0.1.0.tgz") { + t.Fatal("Index files doesn't contain expected content") + } +} + +func TestIndexJSONWrite(t *testing.T) { + i := NewIndexFile() + 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) + } + dir := t.TempDir() testpath := filepath.Join(dir, "test") - i.WriteFile(testpath, 0600) + i.WriteJSONFile(testpath, 0600) - got, err := ioutil.ReadFile(testpath) + got, err := os.ReadFile(testpath) if err != nil { t.Fatal(err) } + if !json.Valid(got) { + t.Fatal("Index files doesn't contain valid JSON") + } if !strings.Contains(string(got), "clipper-0.1.0.tgz") { t.Fatal("Index files doesn't contain expected content") } } + +func TestAddFileIndexEntriesNil(t *testing.T) { + i := NewIndexFile() + i.APIVersion = chart.APIVersionV1 + i.Entries = nil + for _, x := range []struct { + md *chart.Metadata + filename string + baseURL string + digest string + }{ + {&chart.Metadata{APIVersion: "v2", Name: " ", Version: "8033-5.apinie+s.r"}, "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("expected err to be non-nil when entries not initialized") + } + } +} diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index 6f1e90dad..834d554bd 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -17,7 +17,6 @@ limitations under the License. package repo // import "helm.sh/helm/v3/pkg/repo" import ( - "io/ioutil" "os" "path/filepath" "time" @@ -47,7 +46,7 @@ func NewFile() *File { // 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) + b, err := os.ReadFile(path) if err != nil { return r, errors.Wrapf(err, "couldn't load repositories file (%s)", path) } @@ -100,6 +99,9 @@ func (r *File) Remove(name string) bool { cp := []*Entry{} found := false for _, rf := range r.Repositories { + if rf == nil { + continue + } if rf.Name == name { found = true continue @@ -119,5 +121,5 @@ func (r *File) WriteFile(path string, perm os.FileMode) error { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return err } - return ioutil.WriteFile(path, data, perm) + return os.WriteFile(path, data, perm) } diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go index f87d2c202..c2087ebbe 100644 --- a/pkg/repo/repo_test.go +++ b/pkg/repo/repo_test.go @@ -17,7 +17,6 @@ limitations under the License. package repo import ( - "io/ioutil" "os" "strings" "testing" @@ -115,11 +114,11 @@ func TestRepoFile_Get(t *testing.T) { name := "second" entry := repo.Get(name) - if entry == nil { + if entry == nil { //nolint:staticcheck t.Fatalf("Expected repo entry %q to be found", name) } - if entry.URL != "https://example.com/second" { + if entry.URL != "https://example.com/second" { //nolint:staticcheck t.Errorf("Expected repo URL to be %q but got %q", "https://example.com/second", entry.URL) } @@ -198,12 +197,12 @@ func TestWriteFile(t *testing.T) { }, ) - file, err := ioutil.TempFile("", "helm-repo") + file, err := os.CreateTemp("", "helm-repo") if err != nil { t.Errorf("failed to create test-file (%v)", err) } defer os.Remove(file.Name()) - if err := sampleRepository.WriteFile(file.Name(), 0644); err != nil { + if err := sampleRepository.WriteFile(file.Name(), 0600); err != nil { t.Errorf("failed to write file (%v)", err) } @@ -225,3 +224,34 @@ func TestRepoNotExists(t *testing.T) { t.Errorf("expected prompt `couldn't load repositories file`") } } + +func TestRemoveRepositoryInvalidEntries(t *testing.T) { + sampleRepository := NewFile() + sampleRepository.Add( + &Entry{ + Name: "stable", + URL: "https://example.com/stable/charts", + }, + &Entry{ + Name: "incubator", + URL: "https://example.com/incubator", + }, + &Entry{}, + nil, + &Entry{ + Name: "test", + URL: "https://example.com/test", + }, + ) + + removeRepository := "stable" + found := sampleRepository.Remove(removeRepository) + if !found { + t.Errorf("expected repository %s not found", removeRepository) + } + + found = sampleRepository.Has(removeRepository) + if found { + t.Errorf("repository %s not deleted", removeRepository) + } +} diff --git a/pkg/repo/repotest/doc.go b/pkg/repo/repotest/doc.go index 3bf98aa7e..c01daad64 100644 --- a/pkg/repo/repotest/doc.go +++ b/pkg/repo/repotest/doc.go @@ -13,7 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package repotest provides utilities for testing. +/* +Package repotest provides utilities for testing. The server provides a testing server that can be set up and torn down quickly. */ diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go index 94b5ce7f9..d9a5201aa 100644 --- a/pkg/repo/repotest/server.go +++ b/pkg/repo/repotest/server.go @@ -18,7 +18,6 @@ package repotest import ( "context" "fmt" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -26,20 +25,19 @@ import ( "testing" "time" - 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/distribution/distribution/v3/configuration" + "github.com/distribution/distribution/v3/registry" + _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" // used for docker test registry + _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" // used for docker test registry "github.com/phayes/freeport" "golang.org/x/crypto/bcrypt" "sigs.k8s.io/yaml" - 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" + ociRegistry "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/repo" ) @@ -56,6 +54,23 @@ func NewTempServerWithCleanup(t *testing.T, glob string) (*Server, error) { return srv, err } +// Set up a fake repo with basic auth enabled +func NewTempServerWithCleanupAndBasicAuth(t *testing.T, glob string) *Server { + srv, err := NewTempServerWithCleanup(t, glob) + srv.Stop() + if err != nil { + t.Fatal(err) + } + 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) + } + })) + srv.Start() + return srv +} + type OCIServer struct { *registry.Registry RegistryURL string @@ -86,7 +101,7 @@ func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) { 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) + err = os.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644) if err != nil { t.Fatalf("error creating test htpasswd file") } @@ -134,40 +149,26 @@ func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) { 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.ClientOptEnableCache(true), ociRegistry.ClientOptWriter(os.Stdout), - ociRegistry.ClientOptAuthorizer(&ociRegistry.Authorizer{ - Client: client, - }), - ociRegistry.ClientOptResolver(&ociRegistry.Resolver{ - Resolver: resolver, - }), + ociRegistry.ClientOptCredentialsFile(credentialsFile), ) if err != nil { t.Fatalf("error creating registry client") } - err = registryClient.Login(srv.RegistryURL, srv.TestUsername, srv.TestPassword, false) + err = registryClient.Login( + srv.RegistryURL, + ociRegistry.LoginOptBasicAuth(srv.TestUsername, srv.TestPassword), + ociRegistry.LoginOptInsecure(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("") - } + ref := fmt.Sprintf("%s/u/ocitestuser/oci-dependent-chart:0.1.0", srv.RegistryURL) err = chartutil.ExpandFile(srv.Dir, filepath.Join(srv.Dir, "oci-dependent-chart-0.1.0.tgz")) if err != nil { @@ -185,35 +186,56 @@ func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) { t.Fatal("error removing chart before push") } - err = registryClient.SaveChart(ch, ref) + // save it back to disk.. + absPath, err := chartutil.Save(ch, srv.Dir) if err != nil { - t.Fatal("error saving chart") + t.Fatal("could not create chart archive") } - err = registryClient.PushChart(ref) + // load it into memory... + contentBytes, err := os.ReadFile(absPath) if err != nil { - t.Fatal("error pushing chart") + t.Fatal("could not load chart into memory") } - 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") - } + result, err := registryClient.Push(contentBytes, ref) + if err != nil { + t.Fatalf("error pushing dependent chart: %s", err) + } + t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+ + "Config.Digest: %s, Config.Size: %d, "+ + "Chart.Digest: %s, Chart.Size: %d", + result.Manifest.Digest, result.Manifest.Size, + result.Config.Digest, result.Config.Size, + result.Chart.Digest, result.Chart.Size) - err = registryClient.SaveChart(c, dependingRef) - if err != nil { - t.Fatal("error saving depending chart") - } + srv.Client = registryClient + c := cfg.DependingChart + if c == nil { + return + } - err = registryClient.PushChart(dependingRef) - if err != nil { - t.Fatal("error pushing depending chart") - } + dependingRef := fmt.Sprintf("%s/u/ocitestuser/%s:%s", + srv.RegistryURL, c.Metadata.Name, c.Metadata.Version) + + // load it into memory... + absPath = filepath.Join(srv.Dir, + fmt.Sprintf("%s-%s.tgz", c.Metadata.Name, c.Metadata.Version)) + contentBytes, err = os.ReadFile(absPath) + if err != nil { + t.Fatal("could not load chart into memory") } - srv.Client = registryClient + result, err = registryClient.Push(contentBytes, dependingRef) + if err != nil { + t.Fatalf("error pushing depending chart: %s", err) + } + t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+ + "Config.Digest: %s, Config.Size: %d, "+ + "Chart.Digest: %s, Chart.Size: %d", + result.Manifest.Digest, result.Manifest.Size, + result.Config.Digest, result.Config.Size, + result.Chart.Digest, result.Chart.Size) } // NewTempServer creates a server inside of a temp dir. @@ -226,7 +248,7 @@ func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) { // // Deprecated: use NewTempServerWithCleanup func NewTempServer(glob string) (*Server, error) { - tdir, err := ioutil.TempDir("", "helm-repotest-") + tdir, err := os.MkdirTemp("", "helm-repotest-") if err != nil { return nil, err } @@ -294,11 +316,11 @@ func (s *Server) CopyCharts(origin string) ([]string, error) { for i, f := range files { base := filepath.Base(f) newname := filepath.Join(s.docroot, base) - data, err := ioutil.ReadFile(f) + data, err := os.ReadFile(f) if err != nil { return []string{}, err } - if err := ioutil.WriteFile(newname, data, 0644); err != nil { + if err := os.WriteFile(newname, data, 0644); err != nil { return []string{}, err } copied[i] = newname @@ -322,7 +344,7 @@ func (s *Server) CreateIndex() error { } ifile := filepath.Join(s.docroot, "index.yaml") - return ioutil.WriteFile(ifile, d, 0644) + return os.WriteFile(ifile, d, 0644) } func (s *Server) Start() { @@ -337,6 +359,7 @@ func (s *Server) Start() { func (s *Server) StartTLS() { cd := "../../testdata" ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem") + insecure := false s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if s.middleware != nil { @@ -344,11 +367,10 @@ func (s *Server) StartTLS() { } http.FileServer(http.Dir(s.Root())).ServeHTTP(w, r) })) - tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca) + tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca, insecure) if err != nil { panic(err) } - tlsConf.BuildNameToCertificate() tlsConf.ServerName = "helm.sh" s.srv.TLS = tlsConf s.srv.StartTLS() @@ -363,7 +385,7 @@ func (s *Server) StartTLS() { CAFile: filepath.Join("../../testdata", "rootca.crt"), }) - if err := r.WriteFile(repoConfig, 0644); err != nil { + if err := r.WriteFile(repoConfig, 0600); err != nil { panic(err) } } @@ -378,6 +400,7 @@ func (s *Server) Stop() { // URL returns the URL of the server. // // Example: +// // http://localhost:1776 func (s *Server) URL() string { return s.srv.URL @@ -399,5 +422,5 @@ func setTestingRepository(url, fname string) error { Name: "test", URL: url, }) - return r.WriteFile(fname, 0644) + return r.WriteFile(fname, 0640) } diff --git a/pkg/repo/repotest/server_test.go b/pkg/repo/repotest/server_test.go index 1ad979fdc..a7d7f5b95 100644 --- a/pkg/repo/repotest/server_test.go +++ b/pkg/repo/repotest/server_test.go @@ -16,9 +16,8 @@ limitations under the License. package repotest import ( - "io/ioutil" + "io" "net/http" - "os" "path/filepath" "testing" @@ -31,10 +30,9 @@ import ( // Young'n, in these here parts, we test our tests. func TestServer(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) - rootDir := ensure.TempDir(t) - defer os.RemoveAll(rootDir) + rootDir := t.TempDir() srv := NewServer(rootDir) defer srv.Stop() @@ -68,7 +66,7 @@ func TestServer(t *testing.T) { t.Fatal(err) } - data, err := ioutil.ReadAll(res.Body) + data, err := io.ReadAll(res.Body) res.Body.Close() if err != nil { t.Fatal(err) @@ -99,7 +97,7 @@ func TestServer(t *testing.T) { } func TestNewTempServer(t *testing.T) { - defer ensure.HelmHome(t)() + ensure.HelmHome(t) srv, err := NewTempServerWithCleanup(t, "testdata/examplechart-0.1.0.tgz") if err != nil { diff --git a/pkg/repo/testdata/local-index.json b/pkg/repo/testdata/local-index.json new file mode 100644 index 000000000..25296d5ca --- /dev/null +++ b/pkg/repo/testdata/local-index.json @@ -0,0 +1,53 @@ +{ + "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/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 94c278875..0f3ec38a0 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -78,6 +78,7 @@ func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { cfgmaps.Log("get: failed to decode data %q: %s", key, err) return nil, err } + r.Labels = filterSystemLabels(obj.ObjectMeta.Labels) // return the release object return r, nil } @@ -106,7 +107,7 @@ func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Releas continue } - rls.Labels = item.ObjectMeta.Labels + rls.Labels = filterSystemLabels(item.ObjectMeta.Labels) if filter(rls) { results = append(results, rls) @@ -145,6 +146,7 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err cfgmaps.Log("query: failed to decode release: %s", err) continue } + rls.Labels = filterSystemLabels(item.ObjectMeta.Labels) results = append(results, rls) } return results, nil @@ -157,6 +159,7 @@ func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { var lbs labels lbs.init() + lbs.fromMap(rls.Labels) lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix()))) // create a new configmap to hold the release @@ -184,6 +187,7 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { var lbs labels lbs.init() + lbs.fromMap(rls.Labels) lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix()))) // create a new configmap object to hold the release @@ -220,13 +224,12 @@ func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { // // The following labels are used within each configmap: // -// "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. -// +// "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) (*v1.ConfigMap, error) { const owner = "helm" @@ -240,6 +243,9 @@ func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigM lbs.init() } + // apply custom labels + lbs.fromMap(rls.Labels) + // apply labels lbs.set("name", rls.Name) lbs.set("owner", owner) diff --git a/pkg/storage/driver/mock_test.go b/pkg/storage/driver/mock_test.go index c0236ece8..7a1541a02 100644 --- a/pkg/storage/driver/mock_test.go +++ b/pkg/storage/driver/mock_test.go @@ -40,6 +40,10 @@ func releaseStub(name string, vers int, namespace string, status rspb.Status) *r Version: vers, Namespace: namespace, Info: &rspb.Info{Status: status}, + Labels: map[string]string{ + "key1": "val1", + "key2": "val2", + }, } } diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index 2e8530d0c..224026b07 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -72,6 +72,7 @@ func (secrets *Secrets) Get(key string) (*rspb.Release, error) { } // found the secret, decode the base64 data string r, err := decodeRelease(string(obj.Data["release"])) + r.Labels = filterSystemLabels(obj.ObjectMeta.Labels) return r, errors.Wrapf(err, "get: failed to decode data %q", key) } @@ -98,7 +99,7 @@ func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, continue } - rls.Labels = item.ObjectMeta.Labels + rls.Labels = filterSystemLabels(item.ObjectMeta.Labels) if filter(rls) { results = append(results, rls) @@ -136,6 +137,7 @@ func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) secrets.Log("query: failed to decode release: %s", err) continue } + rls.Labels = filterSystemLabels(item.ObjectMeta.Labels) results = append(results, rls) } return results, nil @@ -148,6 +150,7 @@ func (secrets *Secrets) Create(key string, rls *rspb.Release) error { var lbs labels lbs.init() + lbs.fromMap(rls.Labels) lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix()))) // create a new secret to hold the release @@ -173,6 +176,7 @@ func (secrets *Secrets) Update(key string, rls *rspb.Release) error { var lbs labels lbs.init() + lbs.fromMap(rls.Labels) lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix()))) // create a new secret object to hold the release @@ -202,13 +206,12 @@ func (secrets *Secrets) Delete(key string) (rls *rspb.Release, err error) { // // The following labels are used within each secret: // -// "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. -// +// "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) (*v1.Secret, error) { const owner = "helm" @@ -222,6 +225,9 @@ func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*v1.Secret, er lbs.init() } + // apply custom labels + lbs.fromMap(rls.Labels) + // apply labels lbs.set("name", rls.Name) lbs.set("owner", owner) diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index c8a6ae04f..743886e80 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -49,6 +49,7 @@ const postgreSQLDialect = "postgres" const SQLDriverName = "SQL" const sqlReleaseTableName = "releases_v1" +const sqlCustomLabelsTableName = "custom_labels_v1" const ( sqlReleaseTableKeyColumn = "key" @@ -61,6 +62,17 @@ const ( sqlReleaseTableOwnerColumn = "owner" sqlReleaseTableCreatedAtColumn = "createdAt" sqlReleaseTableModifiedAtColumn = "modifiedAt" + + sqlCustomLabelsTableReleaseKeyColumn = "releaseKey" + sqlCustomLabelsTableReleaseNamespaceColumn = "releaseNamespace" + sqlCustomLabelsTableKeyColumn = "key" + sqlCustomLabelsTableValueColumn = "value" +) + +// Following limits based on k8s labels limits - https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set +const ( + sqlCustomLabelsTableKeyMaxLenght = 253 + 1 + 63 + sqlCustomLabelsTableValueMaxLenght = 63 ) const ( @@ -82,8 +94,42 @@ func (s *SQL) Name() string { return SQLDriverName } +// Check if all migrations al +func (s *SQL) checkAlreadyApplied(migrations []*migrate.Migration) bool { + // make map (set) of ids for fast search + migrationsIds := make(map[string]struct{}) + for _, migration := range migrations { + migrationsIds[migration.Id] = struct{}{} + } + + // get list of applied migrations + migrate.SetDisableCreateTable(true) + records, err := migrate.GetMigrationRecords(s.db.DB, postgreSQLDialect) + migrate.SetDisableCreateTable(false) + if err != nil { + s.Log("checkAlreadyApplied: failed to get migration records: %v", err) + return false + } + + for _, record := range records { + if _, ok := migrationsIds[record.Id]; ok { + s.Log("checkAlreadyApplied: found previous migration (Id: %v) applied at %v", record.Id, record.AppliedAt) + delete(migrationsIds, record.Id) + } + } + + // check if all migrations appliyed + if len(migrationsIds) != 0 { + for id := range migrationsIds { + s.Log("checkAlreadyApplied: find unapplied migration (id: %v)", id) + } + return false + } + return true +} + 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{ { @@ -91,7 +137,7 @@ func (s *SQL) ensureDBSetup() error { Up: []string{ fmt.Sprintf(` CREATE TABLE %s ( - %s VARCHAR(67), + %s VARCHAR(90), %s VARCHAR(64) NOT NULL, %s TEXT NOT NULL, %s VARCHAR(64) NOT NULL, @@ -109,9 +155,9 @@ func (s *SQL) ensureDBSetup() error { 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, @@ -150,9 +196,50 @@ func (s *SQL) ensureDBSetup() error { `, sqlReleaseTableName), }, }, + { + Id: "custom_labels", + Up: []string{ + fmt.Sprintf(` + CREATE TABLE %s ( + %s VARCHAR(64), + %s VARCHAR(67), + %s VARCHAR(%d), + %s VARCHAR(%d) + ); + CREATE INDEX ON %s (%s, %s); + + GRANT ALL ON %s TO PUBLIC; + ALTER TABLE %s ENABLE ROW LEVEL SECURITY; + `, + sqlCustomLabelsTableName, + sqlCustomLabelsTableReleaseKeyColumn, + sqlCustomLabelsTableReleaseNamespaceColumn, + sqlCustomLabelsTableKeyColumn, + sqlCustomLabelsTableKeyMaxLenght, + sqlCustomLabelsTableValueColumn, + sqlCustomLabelsTableValueMaxLenght, + sqlCustomLabelsTableName, + sqlCustomLabelsTableReleaseKeyColumn, + sqlCustomLabelsTableReleaseNamespaceColumn, + sqlCustomLabelsTableName, + sqlCustomLabelsTableName, + ), + }, + Down: []string{ + fmt.Sprintf(` + DELETE TABLE %s; + `, sqlCustomLabelsTableName), + }, + }, }, } + // Check that init migration already applied + if s.checkAlreadyApplied(migrations.Migrations) { + return nil + } + + // Populate the database with the relations we need if they don't exist yet _, err := migrate.Exec(s.db.DB, postgreSQLDialect, migrations, migrate.Up) return err } @@ -180,6 +267,13 @@ type SQLReleaseWrapper struct { ModifiedAt int `db:"modifiedAt"` } +type SQLReleaseCustomLabelWrapper struct { + ReleaseKey string `db:"release_key"` + ReleaseNamespace string `db:"release_namespace"` + Key string `db:"key"` + Value string `db:"value"` +} + // NewSQL initializes a new sql driver. func NewSQL(connectionString string, logger func(string, ...interface{}), namespace string) (*SQL, error) { db, err := sqlx.Connect(postgreSQLDialect, connectionString) @@ -230,13 +324,18 @@ func (s *SQL) Get(key string) (*rspb.Release, error) { return nil, err } + if release.Labels, err = s.getReleaseCustomLabels(key, s.namespace); err != nil { + s.Log("failed to get release %s/%s custom labels: %v", s.namespace, 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). + Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn). From(sqlReleaseTableName). Where(sq.Eq{sqlReleaseTableOwnerColumn: sqlReleaseDefaultOwner}) @@ -264,6 +363,12 @@ func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { s.Log("list: failed to decode release: %v: %v", record, err) continue } + + if release.Labels, err = s.getReleaseCustomLabels(record.Key, record.Namespace); err != nil { + s.Log("failed to get release %s/%s custom labels: %v", record.Namespace, record.Key, err) + return nil, err + } + if filter(release) { releases = append(releases, release) } @@ -275,7 +380,7 @@ func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { // 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). + Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn). From(sqlReleaseTableName) keys := make([]string, 0, len(labels)) @@ -321,6 +426,12 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { s.Log("list: failed to decode release: %v: %v", record, err) continue } + + if release.Labels, err = s.getReleaseCustomLabels(record.Key, record.Namespace); err != nil { + s.Log("failed to get release %s/%s custom labels: %v", record.Namespace, record.Key, err) + return nil, err + } + releases = append(releases, release) } @@ -403,6 +514,36 @@ func (s *SQL) Create(key string, rls *rspb.Release) error { s.Log("failed to store release %s in SQL database: %v", key, err) return err } + + // Filtering labels before insert cause in SQL storage driver system releases are stored in separate columns of release table + for k, v := range filterSystemLabels(rls.Labels) { + insertLabelsQuery, args, err := s.statementBuilder. + Insert(sqlCustomLabelsTableName). + Columns( + sqlCustomLabelsTableReleaseKeyColumn, + sqlCustomLabelsTableReleaseNamespaceColumn, + sqlCustomLabelsTableKeyColumn, + sqlCustomLabelsTableValueColumn, + ). + Values( + key, + namespace, + k, + v, + ).ToSql() + + if err != nil { + defer transaction.Rollback() + s.Log("failed to build insert query: %v", err) + return err + } + + if _, err := transaction.Exec(insertLabelsQuery, args...); err != nil { + defer transaction.Rollback() + s.Log("failed to write Labels: %v", err) + return err + } + } defer transaction.Commit() return nil @@ -487,10 +628,56 @@ func (s *SQL) Delete(key string) (*rspb.Release, error) { Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). ToSql() if err != nil { - s.Log("failed to build select query: %v", err) + s.Log("failed to build delete query: %v", err) return nil, err } _, err = transaction.Exec(deleteQuery, args...) + if err != nil { + s.Log("failed perform delete query: %v", err) + return release, err + } + + if release.Labels, err = s.getReleaseCustomLabels(key, s.namespace); err != nil { + s.Log("failed to get release %s/%s custom labels: %v", s.namespace, key, err) + return nil, err + } + + deleteCustomLabelsQuery, args, err := s.statementBuilder. + Delete(sqlCustomLabelsTableName). + Where(sq.Eq{sqlCustomLabelsTableReleaseKeyColumn: key}). + Where(sq.Eq{sqlCustomLabelsTableReleaseNamespaceColumn: s.namespace}). + ToSql() + + if err != nil { + s.Log("failed to build delete Labels query: %v", err) + return nil, err + } + _, err = transaction.Exec(deleteCustomLabelsQuery, args...) return release, err } + +// Get release custom labels from database +func (s *SQL) getReleaseCustomLabels(key string, namespace string) (map[string]string, error) { + query, args, err := s.statementBuilder. + Select(sqlCustomLabelsTableKeyColumn, sqlCustomLabelsTableValueColumn). + From(sqlCustomLabelsTableName). + Where(sq.Eq{sqlCustomLabelsTableReleaseKeyColumn: key, + sqlCustomLabelsTableReleaseNamespaceColumn: s.namespace}). + ToSql() + if err != nil { + return nil, err + } + + var labelsList = []SQLReleaseCustomLabelWrapper{} + if err := s.db.Select(&labelsList, query, args...); err != nil { + return nil, err + } + + labelsMap := make(map[string]string) + for _, i := range labelsList { + labelsMap[i.Key] = i.Value + } + + return filterSystemLabels(labelsMap), nil +} diff --git a/pkg/storage/driver/sql_test.go b/pkg/storage/driver/sql_test.go index 87b6315b8..067b3cf47 100644 --- a/pkg/storage/driver/sql_test.go +++ b/pkg/storage/driver/sql_test.go @@ -21,6 +21,7 @@ import ( "time" sqlmock "github.com/DATA-DOG/go-sqlmock" + migrate "github.com/rubenv/sql-migrate" rspb "helm.sh/helm/v3/pkg/release" ) @@ -62,6 +63,8 @@ func TestSQLGet(t *testing.T) { ), ).RowsWillBeClosed() + mockGetReleaseCustomLabels(mock, key, namespace, rel.Labels) + got, err := sqlDriver.Get(key) if err != nil { t.Fatalf("Failed to get release: %v", err) @@ -77,38 +80,42 @@ func TestSQLGet(t *testing.T) { } 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)) + releases := []*rspb.Release{} + releases = append(releases, releaseStub("key-1", 1, "default", rspb.StatusUninstalled)) + releases = append(releases, releaseStub("key-2", 1, "default", rspb.StatusUninstalled)) + releases = append(releases, releaseStub("key-3", 1, "default", rspb.StatusDeployed)) + releases = append(releases, releaseStub("key-4", 1, "default", rspb.StatusDeployed)) + releases = append(releases, releaseStub("key-5", 1, "default", rspb.StatusSuperseded)) + releases = append(releases, 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", + "SELECT %s, %s, %s FROM %s WHERE %s = $1 AND %s = $2", + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn, sqlReleaseTableName, sqlReleaseTableOwnerColumn, sqlReleaseTableNamespaceColumn, ) + rows := mock.NewRows([]string{ + sqlReleaseTableBodyColumn, + }) + for _, r := range releases { + body, _ := encodeRelease(r) + rows.AddRow(body) + } 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() + WillReturnRows(rows).RowsWillBeClosed() + + for _, r := range releases { + mockGetReleaseCustomLabels(mock, "", r.Namespace, r.Labels) + } } // list all deleted releases @@ -181,6 +188,23 @@ func TestSqlCreate(t *testing.T) { 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)) + + labelsQuery := fmt.Sprintf( + "INSERT INTO %s (%s,%s,%s,%s) VALUES ($1,$2,$3,$4)", + sqlCustomLabelsTableName, + sqlCustomLabelsTableReleaseKeyColumn, + sqlCustomLabelsTableReleaseNamespaceColumn, + sqlCustomLabelsTableKeyColumn, + sqlCustomLabelsTableValueColumn, + ) + + mock.MatchExpectationsInOrder(false) + for k, v := range filterSystemLabels(rel.Labels) { + mock. + ExpectExec(regexp.QuoteMeta(labelsQuery)). + WithArgs(key, rel.Namespace, k, v). + WillReturnResult(sqlmock.NewResult(1, 1)) + } mock.ExpectCommit() if err := sqlDriver.Create(key, rel); err != nil { @@ -316,7 +340,9 @@ func TestSqlQuery(t *testing.T) { sqlDriver, mock := newTestFixtureSQL(t) query := fmt.Sprintf( - "SELECT %s FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3 AND %s = $4", + "SELECT %s, %s, %s FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3 AND %s = $4", + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn, sqlReleaseTableName, sqlReleaseTableNameColumn, @@ -345,8 +371,12 @@ func TestSqlQuery(t *testing.T) { ), ).RowsWillBeClosed() + mockGetReleaseCustomLabels(mock, "", deployedRelease.Namespace, deployedRelease.Labels) + query = fmt.Sprintf( - "SELECT %s FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3", + "SELECT %s, %s, %s FROM %s WHERE %s = $1 AND %s = $2 AND %s = $3", + sqlReleaseTableKeyColumn, + sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn, sqlReleaseTableName, sqlReleaseTableNameColumn, @@ -367,6 +397,9 @@ func TestSqlQuery(t *testing.T) { ), ).RowsWillBeClosed() + mockGetReleaseCustomLabels(mock, "", supersededRelease.Namespace, supersededRelease.Labels) + mockGetReleaseCustomLabels(mock, "", deployedRelease.Namespace, deployedRelease.Labels) + _, err := sqlDriver.Query(labelSetUnknown) if err == nil { t.Errorf("Expected error {%v}, got nil", ErrReleaseNotFound) @@ -447,6 +480,20 @@ func TestSqlDelete(t *testing.T) { ExpectExec(regexp.QuoteMeta(deleteQuery)). WithArgs(key, namespace). WillReturnResult(sqlmock.NewResult(0, 1)) + + mockGetReleaseCustomLabels(mock, key, namespace, rel.Labels) + + deleteLabelsQuery := fmt.Sprintf( + "DELETE FROM %s WHERE %s = $1 AND %s = $2", + sqlCustomLabelsTableName, + sqlCustomLabelsTableReleaseKeyColumn, + sqlCustomLabelsTableReleaseNamespaceColumn, + ) + mock. + ExpectExec(regexp.QuoteMeta(deleteLabelsQuery)). + WithArgs(key, namespace). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() deletedRelease, err := sqlDriver.Delete(key) @@ -461,3 +508,74 @@ func TestSqlDelete(t *testing.T) { t.Errorf("Expected release {%v}, got {%v}", rel, deletedRelease) } } + +func mockGetReleaseCustomLabels(mock sqlmock.Sqlmock, key string, namespace string, labels map[string]string) { + query := fmt.Sprintf( + regexp.QuoteMeta("SELECT %s, %s FROM %s WHERE %s = $1 AND %s = $2"), + sqlCustomLabelsTableKeyColumn, + sqlCustomLabelsTableValueColumn, + sqlCustomLabelsTableName, + sqlCustomLabelsTableReleaseKeyColumn, + sqlCustomLabelsTableReleaseNamespaceColumn, + ) + + eq := mock.ExpectQuery(query). + WithArgs(key, namespace) + + returnRows := mock.NewRows([]string{ + sqlCustomLabelsTableKeyColumn, + sqlCustomLabelsTableValueColumn, + }) + for k, v := range labels { + returnRows.AddRow(k, v) + } + eq.WillReturnRows(returnRows).RowsWillBeClosed() +} + +func TestSqlChechkAppliedMigrations(t *testing.T) { + cases := []struct { + migrationsToApply []*migrate.Migration + appliedMigrationsIds []string + expectedResult bool + errorExplanation string + }{ + { + migrationsToApply: []*migrate.Migration{{Id: "init1"}, {Id: "init2"}, {Id: "init3"}}, + appliedMigrationsIds: []string{"1", "2", "init1", "3", "init2", "4", "5"}, + expectedResult: false, + errorExplanation: "Has found one migration id \"init3\" as applied, that was not applied", + }, + { + migrationsToApply: []*migrate.Migration{{Id: "init1"}, {Id: "init2"}, {Id: "init3"}}, + appliedMigrationsIds: []string{"1", "2", "init1", "3", "init2", "4", "init3", "5"}, + expectedResult: true, + errorExplanation: "Has not found one or more migration ids, that was applied", + }, + { + migrationsToApply: []*migrate.Migration{{Id: "init"}}, + appliedMigrationsIds: []string{"1", "2", "3", "inits", "4", "tinit", "5"}, + expectedResult: false, + errorExplanation: "Has found single \"init\", that was not applied", + }, + { + migrationsToApply: []*migrate.Migration{{Id: "init"}}, + appliedMigrationsIds: []string{"1", "2", "init", "3", "init2", "4", "init3", "5"}, + expectedResult: true, + errorExplanation: "Has not found single migration id \"init\", that was applied", + }, + } + for i, c := range cases { + sqlDriver, mock := newTestFixtureSQL(t) + rows := sqlmock.NewRows([]string{"id", "applied_at"}) + for _, id := range c.appliedMigrationsIds { + rows.AddRow(id, time.Time{}) + } + mock. + ExpectQuery(""). + WillReturnRows(rows) + mock.ExpectCommit() + if sqlDriver.checkAlreadyApplied(c.migrationsToApply) != c.expectedResult { + t.Errorf("Test case: %v, Expected: %v, Have: %v, Explanation: %v", i, c.expectedResult, !c.expectedResult, c.errorExplanation) + } + } +} diff --git a/pkg/storage/driver/util.go b/pkg/storage/driver/util.go index e5b846163..7bda5ec96 100644 --- a/pkg/storage/driver/util.go +++ b/pkg/storage/driver/util.go @@ -21,7 +21,7 @@ import ( "compress/gzip" "encoding/base64" "encoding/json" - "io/ioutil" + "io" rspb "helm.sh/helm/v3/pkg/release" ) @@ -30,6 +30,8 @@ var b64 = base64.StdEncoding var magicGzip = []byte{0x1f, 0x8b, 0x08} +var systemLabels = []string{"name", "owner", "status", "version", "createdAt", "modifiedAt"} + // encodeRelease encodes a release returning a base64 encoded // gzipped string representation, or error. func encodeRelease(rls *rspb.Release) (string, error) { @@ -63,13 +65,13 @@ func decodeRelease(data string) (*rspb.Release, error) { // For backwards compatibility with releases that were stored before // compression was introduced we skip decompression if the // gzip magic header is not found - if bytes.Equal(b[0:3], magicGzip) { + if len(b) > 3 && bytes.Equal(b[0:3], magicGzip) { r, err := gzip.NewReader(bytes.NewReader(b)) if err != nil { return nil, err } defer r.Close() - b2, err := ioutil.ReadAll(r) + b2, err := io.ReadAll(r) if err != nil { return nil, err } @@ -83,3 +85,38 @@ func decodeRelease(data string) (*rspb.Release, error) { } return &rls, nil } + +// Checks if label is system +func isSystemLabel(key string) bool { + for _, v := range GetSystemLabels() { + if key == v { + return true + } + } + return false +} + +// Removes system labels from labels map +func filterSystemLabels(lbs map[string]string) map[string]string { + result := make(map[string]string) + for k, v := range lbs { + if !isSystemLabel(k) { + result[k] = v + } + } + return result +} + +// Checks if labels array contains system labels +func ContainsSystemLabels(lbs map[string]string) bool { + for k := range lbs { + if isSystemLabel(k) { + return true + } + } + return false +} + +func GetSystemLabels() []string { + return systemLabels +} diff --git a/pkg/storage/driver/util_test.go b/pkg/storage/driver/util_test.go new file mode 100644 index 000000000..d16043924 --- /dev/null +++ b/pkg/storage/driver/util_test.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 driver + +import ( + "reflect" + "testing" +) + +func TestGetSystemLabel(t *testing.T) { + if output := GetSystemLabels(); !reflect.DeepEqual(systemLabels, output) { + t.Errorf("Expected {%v}, got {%v}", systemLabels, output) + } +} + +func TestIsSystemLabel(t *testing.T) { + tests := map[string]bool{ + "name": true, + "owner": true, + "test": false, + "NaMe": false, + } + for label, result := range tests { + if output := isSystemLabel(label); output != result { + t.Errorf("Output %t not equal to expected %t", output, result) + } + } +} + +func TestFilterSystemLabels(t *testing.T) { + var tests = [][2]map[string]string{ + {nil, map[string]string{}}, + {map[string]string{}, map[string]string{}}, + {map[string]string{ + "name": "name", + "owner": "owner", + "status": "status", + "version": "version", + "createdAt": "createdAt", + "modifiedAt": "modifiedAt", + }, map[string]string{}}, + {map[string]string{ + "StaTus": "status", + "name": "name", + "owner": "owner", + "key": "value", + }, map[string]string{ + "StaTus": "status", + "key": "value", + }}, + {map[string]string{ + "key1": "value1", + "key2": "value2", + }, map[string]string{ + "key1": "value1", + "key2": "value2", + }}, + } + for _, test := range tests { + if output := filterSystemLabels(test[0]); !reflect.DeepEqual(test[1], output) { + t.Errorf("Expected {%v}, got {%v}", test[1], output) + } + } +} + +func TestContainsSystemLabels(t *testing.T) { + var tests = []struct { + input map[string]string + output bool + }{ + {nil, false}, + {map[string]string{}, false}, + {map[string]string{ + "name": "name", + "owner": "owner", + "status": "status", + "version": "version", + "createdAt": "createdAt", + "modifiedAt": "modifiedAt", + }, true}, + {map[string]string{ + "StaTus": "status", + "name": "name", + "owner": "owner", + "key": "value", + }, true}, + {map[string]string{ + "key1": "value1", + "key2": "value2", + }, false}, + } + for _, test := range tests { + if output := ContainsSystemLabels(test.input); !reflect.DeepEqual(test.output, output) { + t.Errorf("Expected {%v}, got {%v}", test.output, output) + } + } +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index d836a412a..0a18b34a0 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -55,8 +55,8 @@ func (s *Storage) Get(name string, version int) (*rspb.Release, error) { } // Create creates a new storage entry holding the release. An -// error is returned if the storage driver failed to store the -// release, or a release with identical an key already exists. +// error is returned if the storage driver fails to store the +// release, or a release with an identical key already exists. func (s *Storage) Create(rls *rspb.Release) error { s.Log("creating release %q", makeKey(rls.Name, rls.Version)) if s.MaxHistory > 0 { @@ -177,7 +177,7 @@ func (s *Storage) removeLeastRecent(name string, max int) error { relutil.SortByRevision(h) lastDeployed, err := s.Deployed(name) - if err != nil { + if err != nil && !errors.Is(err, driver.ErrNoDeployedReleases) { return err } diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index ac79ca2fd..058b077e8 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -278,8 +278,40 @@ func TestStorageHistory(t *testing.T) { } } -func TestStorageRemoveLeastRecentWithError(t *testing.T) { - storage := Init(driver.NewMemory()) +var errMaxHistoryMockDriverSomethingHappened = errors.New("something happened") + +type MaxHistoryMockDriver struct { + Driver driver.Driver +} + +func NewMaxHistoryMockDriver(d driver.Driver) *MaxHistoryMockDriver { + return &MaxHistoryMockDriver{Driver: d} +} +func (d *MaxHistoryMockDriver) Create(key string, rls *rspb.Release) error { + return d.Driver.Create(key, rls) +} +func (d *MaxHistoryMockDriver) Update(key string, rls *rspb.Release) error { + return d.Driver.Update(key, rls) +} +func (d *MaxHistoryMockDriver) Delete(key string) (*rspb.Release, error) { + return nil, errMaxHistoryMockDriverSomethingHappened +} +func (d *MaxHistoryMockDriver) Get(key string) (*rspb.Release, error) { + return d.Driver.Get(key) +} +func (d *MaxHistoryMockDriver) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { + return d.Driver.List(filter) +} +func (d *MaxHistoryMockDriver) Query(labels map[string]string) ([]*rspb.Release, error) { + return d.Driver.Query(labels) +} +func (d *MaxHistoryMockDriver) Name() string { + return d.Driver.Name() +} + +func TestMaxHistoryErrorHandling(t *testing.T) { + //func TestStorageRemoveLeastRecentWithError(t *testing.T) { + storage := Init(NewMaxHistoryMockDriver(driver.NewMemory())) storage.Log = t.Logf storage.MaxHistory = 1 @@ -297,7 +329,7 @@ func TestStorageRemoveLeastRecentWithError(t *testing.T) { setup() rls2 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() - wantErr := driver.ErrNoDeployedReleases + wantErr := errMaxHistoryMockDriverSomethingHappened 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) @@ -444,6 +476,65 @@ func TestStorageLast(t *testing.T) { } } +// TestUpgradeInitiallyFailedRelease tests a case when there are no deployed release yet, but history limit has been +// reached: the has-no-deployed-releases error should not occur in such case. +func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) { + storage := Init(driver.NewMemory()) + storage.MaxHistory = 4 + + const name = "angry-bird" + + // setup storage with test releases + setup := func() { + // release records + rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusFailed}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusFailed}.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)") + + hist, err := storage.History(name) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + wantHistoryLen := 4 + if len(hist) != wantHistoryLen { + t.Fatalf("expected history of release %q to contain %d releases, got %d", name, wantHistoryLen, len(hist)) + } + } + + setup() + + rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusFailed}.ToRelease() + err := storage.Create(rls5) + if err != nil { + t.Fatalf("Failed to create a new release version: %s", err) + } + + hist, err := storage.History(name) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + for i, rel := range hist { + wantVersion := i + 2 + if rel.Version != wantVersion { + t.Fatalf("Expected history release %d version to equal %d, got %d", i+1, wantVersion, rel.Version) + } + + wantStatus := rspb.StatusFailed + if rel.Info.Status != wantStatus { + t.Fatalf("Expected history release %d status to equal %q, got %q", i+1, wantStatus, rel.Info.Status) + } + } +} + type ReleaseTestData struct { Name string Version int diff --git a/pkg/strvals/doc.go b/pkg/strvals/doc.go index f17290587..e9931300c 100644 --- a/pkg/strvals/doc.go +++ b/pkg/strvals/doc.go @@ -13,7 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package strvals provides tools for working with strval lines. +/* +Package strvals provides tools for working with strval lines. Helm supports a compressed format for YAML settings which we call strvals. The format is roughly like this: diff --git a/pkg/strvals/literal_parser.go b/pkg/strvals/literal_parser.go new file mode 100644 index 000000000..f75655811 --- /dev/null +++ b/pkg/strvals/literal_parser.go @@ -0,0 +1,244 @@ +/* +Copyright The 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 strvals + +import ( + "bytes" + "fmt" + "io" + "strconv" + + "github.com/pkg/errors" +) + +// ParseLiteral parses a set line interpreting the value as a literal string. +// +// A set line is of the form name1=value1 +func ParseLiteral(s string) (map[string]interface{}, error) { + vals := map[string]interface{}{} + scanner := bytes.NewBufferString(s) + t := newLiteralParser(scanner, vals) + err := t.parse() + return vals, err +} + +// ParseLiteralInto parses a strvals line and merges the result into dest. +// The value is interpreted as a literal string. +// +// If the strval string has a key that exists in dest, it overwrites the +// dest version. +func ParseLiteralInto(s string, dest map[string]interface{}) error { + scanner := bytes.NewBufferString(s) + t := newLiteralParser(scanner, dest) + return t.parse() +} + +// literalParser is a simple parser that takes a strvals line and parses +// it into a map representation. +// +// Values are interpreted as a literal string. +// +// 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 literalParser struct { + sc *bytes.Buffer + data map[string]interface{} +} + +func newLiteralParser(sc *bytes.Buffer, data map[string]interface{}) *literalParser { + return &literalParser{sc: sc, data: data} +} + +func (t *literalParser) parse() error { + for { + err := t.key(t.data, 0) + if err == nil { + continue + } + if err == io.EOF { + return nil + } + return err + } +} + +func runesUntilLiteral(in io.RuneReader, stop map[rune]bool) ([]rune, rune, error) { + v := []rune{} + for { + switch r, _, e := in.ReadRune(); { + case e != nil: + return v, r, e + case inMap(r, stop): + return v, r, nil + default: + v = append(v, r) + } + } +} + +func (t *literalParser) key(data map[string]interface{}, nestedNameLevel int) (reterr error) { + defer func() { + if r := recover(); r != nil { + reterr = fmt.Errorf("unable to parse key: %s", r) + } + }() + stop := runeSet([]rune{'=', '[', '.'}) + for { + switch key, lastRune, err := runesUntilLiteral(t.sc, stop); { + case err != nil: + if len(key) == 0 { + return err + } + return errors.Errorf("key %q has no value", string(key)) + + case lastRune == '=': + // found end of key: swallow the '=' and get the value + value, err := t.val() + if err == nil && err != io.EOF { + return err + } + set(data, string(key), string(value)) + return nil + + case lastRune == '.': + // Check value name is within the maximum nested name level + nestedNameLevel++ + if nestedNameLevel > MaxNestedNameLevel { + return fmt.Errorf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel) + } + + // first, create or find the target map in the given data + inner := map[string]interface{}{} + if _, ok := data[string(key)]; ok { + inner = data[string(key)].(map[string]interface{}) + } + + // recurse on sub-tree with remaining data + err := t.key(inner, nestedNameLevel) + if err == nil && len(inner) == 0 { + return errors.Errorf("key map %q has no value", string(key)) + } + if len(inner) != 0 { + set(data, string(key), inner) + } + return err + + case lastRune == '[': + // We are in a list index context, so we need to set an index. + i, err := t.keyIndex() + if err != nil { + return errors.Wrap(err, "error parsing index") + } + kk := string(key) + + // find or create target list + list := []interface{}{} + if _, ok := data[kk]; ok { + list = data[kk].([]interface{}) + } + + // now we need to get the value after the ] + list, err = t.listItem(list, i, nestedNameLevel) + set(data, kk, list) + return err + } + } +} + +func (t *literalParser) keyIndex() (int, error) { + // First, get the key. + stop := runeSet([]rune{']'}) + v, _, err := runesUntilLiteral(t.sc, stop) + if err != nil { + return 0, err + } + + // v should be the index + return strconv.Atoi(string(v)) +} + +func (t *literalParser) listItem(list []interface{}, i, nestedNameLevel int) ([]interface{}, error) { + if i < 0 { + return list, fmt.Errorf("negative %d index not allowed", i) + } + stop := runeSet([]rune{'[', '.', '='}) + + switch key, lastRune, err := runesUntilLiteral(t.sc, stop); { + case len(key) > 0: + return list, errors.Errorf("unexpected data at end of array index: %q", key) + + case err != nil: + return list, err + + case lastRune == '=': + value, err := t.val() + if err != nil && err != io.EOF { + return list, err + } + return setIndex(list, i, string(value)) + + case lastRune == '.': + // we have a nested object. Send to t.key + inner := map[string]interface{}{} + if len(list) > i { + 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 + err := t.key(inner, nestedNameLevel) + if err != nil { + return list, err + } + return setIndex(list, i, inner) + + case lastRune == '[': + // now we have a nested list. Read the index and handle. + nextI, err := t.keyIndex() + if err != nil { + 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(crtList, nextI, nestedNameLevel) + if err != nil { + return list, err + } + return setIndex(list, i, list2) + + default: + return nil, errors.Errorf("parse error: unexpected token %v", lastRune) + } +} + +func (t *literalParser) val() ([]rune, error) { + stop := runeSet([]rune{}) + v, _, err := runesUntilLiteral(t.sc, stop) + return v, err +} diff --git a/pkg/strvals/literal_parser_test.go b/pkg/strvals/literal_parser_test.go new file mode 100644 index 000000000..4e74423d6 --- /dev/null +++ b/pkg/strvals/literal_parser_test.go @@ -0,0 +1,480 @@ +/* +Copyright The 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 strvals + +import ( + "fmt" + "testing" + + "sigs.k8s.io/yaml" +) + +func TestParseLiteral(t *testing.T) { + cases := []struct { + str string + expect map[string]interface{} + err bool + }{ + { + str: "name", + err: true, + }, + { + str: "name=", + expect: map[string]interface{}{"name": ""}, + }, + { + str: "name=value", + expect: map[string]interface{}{"name": "value"}, + err: false, + }, + { + str: "long_int_string=1234567890", + 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, + }, + { + str: "name1=null,name2=value2", + expect: map[string]interface{}{"name1": "null,name2=value2"}, + err: false, + }, + { + str: "name1=value,,,tail", + expect: map[string]interface{}{"name1": "value,,,tail"}, + err: false, + }, + { + str: "leading_zeros=00009", + expect: map[string]interface{}{"leading_zeros": "00009"}, + err: false, + }, + { + str: "name=one two three", + expect: map[string]interface{}{"name": "one two three"}, + err: false, + }, + { + str: "outer.inner=value", + expect: map[string]interface{}{"outer": map[string]interface{}{"inner": "value"}}, + err: false, + }, + { + str: "outer.middle.inner=value", + expect: map[string]interface{}{"outer": map[string]interface{}{"middle": map[string]interface{}{"inner": "value"}}}, + err: false, + }, + { + str: "name1.name2", + err: true, + }, + { + str: "name1.name2=", + expect: map[string]interface{}{"name1": map[string]interface{}{"name2": ""}}, + err: false, + }, + { + str: "name1.=name2", + err: true, + }, + { + str: "name1.,name2", + err: true, + }, + { + str: "name1={value1,value2}", + expect: map[string]interface{}{"name1": "{value1,value2}"}, + }, + + // List support + { + str: "list[0]=foo", + expect: map[string]interface{}{"list": []string{"foo"}}, + err: false, + }, + { + str: "list[0].foo=bar", + expect: map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{"foo": "bar"}, + }, + }, + err: false, + }, + { + str: "list[-30].hello=world", + err: true, + }, + { + str: "list[3]=bar", + expect: map[string]interface{}{"list": []interface{}{nil, nil, nil, "bar"}}, + err: false, + }, + { + str: "illegal[0]name.foo=bar", + err: true, + }, + { + str: "noval[0]", + expect: map[string]interface{}{"noval": []interface{}{}}, + err: false, + }, + { + str: "noval[0]=", + expect: map[string]interface{}{"noval": []interface{}{""}}, + err: false, + }, + { + str: "nested[0][0]=1", + expect: map[string]interface{}{"nested": []interface{}{[]interface{}{"1"}}}, + err: false, + }, + { + str: "nested[1][1]=1", + expect: map[string]interface{}{"nested": []interface{}{nil, []interface{}{nil, "1"}}}, + err: false, + }, + { + str: "name1.name2[0].foo=bar", + expect: map[string]interface{}{ + "name1": map[string]interface{}{ + "name2": []map[string]interface{}{{"foo": "bar"}}, + }, + }, + }, + { + str: "name1.name2[1].foo=bar", + expect: map[string]interface{}{ + "name1": map[string]interface{}{ + "name2": []map[string]interface{}{nil, {"foo": "bar"}}, + }, + }, + }, + { + str: "name1.name2[1].foo=bar", + expect: map[string]interface{}{ + "name1": map[string]interface{}{ + "name2": []map[string]interface{}{nil, {"foo": "bar"}}, + }, + }, + }, + { + str: "]={}].", + expect: map[string]interface{}{"]": "{}]."}, + err: false, + }, + + // issue test cases: , = $ ( ) { } . \ \\ + { + str: "name=val,val", + expect: map[string]interface{}{"name": "val,val"}, + err: false, + }, + { + str: "name=val.val", + expect: map[string]interface{}{"name": "val.val"}, + err: false, + }, + { + str: "name=val=val", + expect: map[string]interface{}{"name": "val=val"}, + err: false, + }, + { + str: "name=val$val", + expect: map[string]interface{}{"name": "val$val"}, + err: false, + }, + { + str: "name=(value", + expect: map[string]interface{}{"name": "(value"}, + err: false, + }, + { + str: "name=value)", + expect: map[string]interface{}{"name": "value)"}, + err: false, + }, + { + str: "name=(value)", + expect: map[string]interface{}{"name": "(value)"}, + err: false, + }, + { + str: "name={value", + expect: map[string]interface{}{"name": "{value"}, + err: false, + }, + { + str: "name=value}", + expect: map[string]interface{}{"name": "value}"}, + err: false, + }, + { + str: "name={value}", + expect: map[string]interface{}{"name": "{value}"}, + err: false, + }, + { + str: "name={value1,value2}", + expect: map[string]interface{}{"name": "{value1,value2}"}, + err: false, + }, + { + str: `name=val\val`, + expect: map[string]interface{}{"name": `val\val`}, + err: false, + }, + { + str: `name=val\\val`, + expect: map[string]interface{}{"name": `val\\val`}, + err: false, + }, + { + str: `name=val\\\val`, + expect: map[string]interface{}{"name": `val\\\val`}, + err: false, + }, + { + str: `name={val,.?*v\0a!l)some`, + expect: map[string]interface{}{"name": `{val,.?*v\0a!l)some`}, + err: false, + }, + { + str: `name=em%GT)tqUDqz,i-\h+Mbqs-!:.m\\rE=mkbM#rR}@{-k@`, + expect: map[string]interface{}{"name": `em%GT)tqUDqz,i-\h+Mbqs-!:.m\\rE=mkbM#rR}@{-k@`}, + }, + } + + for _, tt := range cases { + got, err := ParseLiteral(tt.str) + if err != nil { + if !tt.err { + t.Fatalf("%s: %s", tt.str, err) + } + continue + } + + if tt.err { + t.Errorf("%s: Expected error. Got nil", tt.str) + } + + y1, err := yaml.Marshal(tt.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", tt.str, y1, y2) + } + } +} + +func TestParseLiteralInto(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,outer.inner3=value3,outer.inner4=4", + "inner2": "value2", + }}, + 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 := ParseLiteralInto(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 := ParseLiteralInto(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 TestParseLiteralNestedLevels(t *testing.T) { + var keyMultipleNestedLevels string + + for i := 1; i <= MaxNestedNameLevel+2; i++ { + tmpStr := fmt.Sprintf("name%d", i) + if i <= MaxNestedNameLevel+1 { + tmpStr = tmpStr + "." + } + keyMultipleNestedLevels += tmpStr + } + + tests := []struct { + str string + expect map[string]interface{} + err bool + errStr string + }{ + { + "outer.middle.inner=value", + map[string]interface{}{"outer": map[string]interface{}{"middle": map[string]interface{}{"inner": "value"}}}, + false, + "", + }, + { + str: keyMultipleNestedLevels + "=value", + err: true, + errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel), + }, + } + + for _, tt := range tests { + got, err := ParseLiteral(tt.str) + if err != nil { + if tt.err { + if tt.errStr != "" { + if err.Error() != tt.errStr { + t.Errorf("Expected error: %s. Got error: %s", tt.errStr, err.Error()) + } + } + continue + } + t.Fatalf("%s: %s", tt.str, err) + } + + if tt.err { + t.Errorf("%s: Expected error. Got nil", tt.str) + } + + y1, err := yaml.Marshal(tt.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", tt.str, y1, y2) + } + } +} diff --git a/pkg/strvals/parser.go b/pkg/strvals/parser.go index 457b99f94..2828f20c0 100644 --- a/pkg/strvals/parser.go +++ b/pkg/strvals/parser.go @@ -17,10 +17,12 @@ package strvals import ( "bytes" + "encoding/json" "fmt" "io" "strconv" "strings" + "unicode" "github.com/pkg/errors" "sigs.k8s.io/yaml" @@ -29,6 +31,14 @@ import ( // ErrNotList indicates that a non-list was treated as a list. var ErrNotList = errors.New("not a list") +// MaxIndex is the maximum index that will be allowed by setIndex. +// The default value 65536 = 1024 * 64 +var MaxIndex = 65536 + +// MaxNestedNameLevel is the maximum level of nesting for a value name that +// will be allowed. +var MaxNestedNameLevel = 30 + // ToYAML takes a string of arguments and converts to a YAML document. func ToYAML(s string) (string, error) { m, err := Parse(s) @@ -94,6 +104,17 @@ func ParseIntoString(s string, dest map[string]interface{}) error { return t.parse() } +// ParseJSON parses a string with format key1=val1, key2=val2, ... +// where values are json strings (null, or scalars, or arrays, or objects). +// An empty val is treated as null. +// +// If a key exists in dest, the new value overwrites the dest version. +func ParseJSON(s string, dest map[string]interface{}) error { + scanner := bytes.NewBufferString(s) + t := newJSONParser(scanner, dest) + return t.parse() +} + // ParseIntoFile parses a filevals line and merges the result into dest. // // This method always returns a string as the value. @@ -113,9 +134,10 @@ type RunesValueReader func([]rune) (interface{}, error) // 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{} - reader RunesValueReader + sc *bytes.Buffer + data map[string]interface{} + reader RunesValueReader + isjsonval bool } func newParser(sc *bytes.Buffer, data map[string]interface{}, stringBool bool) *parser { @@ -125,13 +147,17 @@ func newParser(sc *bytes.Buffer, data map[string]interface{}, stringBool bool) * return &parser{sc: sc, data: data, reader: stringConverter} } +func newJSONParser(sc *bytes.Buffer, data map[string]interface{}) *parser { + return &parser{sc: sc, data: data, reader: nil, isjsonval: true} +} + 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 { for { - err := t.key(t.data) + err := t.key(t.data, 0) if err == nil { continue } @@ -150,7 +176,7 @@ func runeSet(r []rune) map[rune]bool { return s } -func (t *parser) key(data map[string]interface{}) (reterr error) { +func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr error) { defer func() { if r := recover(); r != nil { reterr = fmt.Errorf("unable to parse key: %s", r) @@ -180,10 +206,37 @@ func (t *parser) key(data map[string]interface{}) (reterr error) { } // Now we need to get the value after the ]. - list, err = t.listItem(list, i) + list, err = t.listItem(list, i, nestedNameLevel) set(data, kk, list) return err case last == '=': + if t.isjsonval { + empval, err := t.emptyVal() + if err != nil { + return err + } + if empval { + set(data, string(k), nil) + return nil + } + // parse jsonvals by using Go’s JSON standard library + // Decode is preferred to Unmarshal in order to parse just the json parts of the list key1=jsonval1,key2=jsonval2,... + // Since Decode has its own buffer that consumes more characters (from underlying t.sc) than the ones actually decoded, + // we invoke Decode on a separate reader built with a copy of what is left in t.sc. After Decode is executed, we + // discard in t.sc the chars of the decoded json value (the number of those characters is returned by InputOffset). + var jsonval interface{} + dec := json.NewDecoder(strings.NewReader(t.sc.String())) + if err = dec.Decode(&jsonval); err != nil { + return err + } + set(data, string(k), jsonval) + if _, err = io.CopyN(io.Discard, t.sc, dec.InputOffset()); err != nil { + return err + } + // skip possible blanks and comma + _, err = t.emptyVal() + return err + } //End of key. Consume =, Get value. // FIXME: Get value list first vl, e := t.valList() @@ -205,12 +258,17 @@ func (t *parser) key(data map[string]interface{}) (reterr error) { default: return e } - case last == ',': // No value given. Set the value to empty string. Return error. set(data, string(k), "") return errors.Errorf("key %q has no value (cannot end with ,)", string(k)) case last == '.': + // Check value name is within the maximum nested name level + nestedNameLevel++ + if nestedNameLevel > MaxNestedNameLevel { + return fmt.Errorf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel) + } + // First, create or find the target map. inner := map[string]interface{}{} if _, ok := data[string(k)]; ok { @@ -218,11 +276,13 @@ func (t *parser) key(data map[string]interface{}) (reterr error) { } // Recurse - e := t.key(inner) - if len(inner) == 0 { + e := t.key(inner, nestedNameLevel) + if e == nil && len(inner) == 0 { return errors.Errorf("key map %q has no value", string(k)) } - set(data, string(k), inner) + if len(inner) != 0 { + set(data, string(k), inner) + } return e } } @@ -249,6 +309,9 @@ func setIndex(list []interface{}, index int, val interface{}) (l2 []interface{}, if index < 0 { return list, fmt.Errorf("negative %d index not allowed", index) } + if index > MaxIndex { + return list, fmt.Errorf("index of %d is greater than maximum supported index of %d", index, MaxIndex) + } if len(list) <= index { newlist := make([]interface{}, index+1) copy(newlist, list) @@ -269,7 +332,7 @@ func (t *parser) keyIndex() (int, error) { return strconv.Atoi(string(v)) } -func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) { +func (t *parser) listItem(list []interface{}, i, nestedNameLevel int) ([]interface{}, error) { if i < 0 { return list, fmt.Errorf("negative %d index not allowed", i) } @@ -280,6 +343,34 @@ func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) { case err != nil: return list, err case last == '=': + if t.isjsonval { + empval, err := t.emptyVal() + if err != nil { + return list, err + } + if empval { + return setIndex(list, i, nil) + } + // parse jsonvals by using Go’s JSON standard library + // Decode is preferred to Unmarshal in order to parse just the json parts of the list key1=jsonval1,key2=jsonval2,... + // Since Decode has its own buffer that consumes more characters (from underlying t.sc) than the ones actually decoded, + // we invoke Decode on a separate reader built with a copy of what is left in t.sc. After Decode is executed, we + // discard in t.sc the chars of the decoded json value (the number of those characters is returned by InputOffset). + var jsonval interface{} + dec := json.NewDecoder(strings.NewReader(t.sc.String())) + if err = dec.Decode(&jsonval); err != nil { + return list, err + } + if list, err = setIndex(list, i, jsonval); err != nil { + return list, err + } + if _, err = io.CopyN(io.Discard, t.sc, dec.InputOffset()); err != nil { + return list, err + } + // skip possible blanks and comma + _, err = t.emptyVal() + return list, err + } vl, e := t.valList() switch e { case nil: @@ -314,7 +405,7 @@ func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) { } } // Now we need to get the value after the ]. - list2, err := t.listItem(crtList, nextI) + list2, err := t.listItem(crtList, nextI, nestedNameLevel) if err != nil { return list, err } @@ -333,7 +424,7 @@ func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) { } // Recurse - e := t.key(inner) + e := t.key(inner, nestedNameLevel) if e != nil { return list, e } @@ -343,6 +434,28 @@ func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) { } } +// check for an empty value +// read and consume optional spaces until comma or EOF (empty val) or any other char (not empty val) +// comma and spaces are consumed, while any other char is not cosumed +func (t *parser) emptyVal() (bool, error) { + for { + r, _, e := t.sc.ReadRune() + if e == io.EOF { + return true, nil + } + if e != nil { + return false, e + } + if r == ',' { + return true, nil + } + if !unicode.IsSpace(r) { + t.sc.UnreadRune() + return false, nil + } + } +} + func (t *parser) val() ([]rune, error) { stop := runeSet([]rune{','}) v, _, err := runesUntil(t.sc, stop) diff --git a/pkg/strvals/parser_test.go b/pkg/strvals/parser_test.go index cef98ba0a..925aa97c6 100644 --- a/pkg/strvals/parser_test.go +++ b/pkg/strvals/parser_test.go @@ -16,6 +16,7 @@ limitations under the License. package strvals import ( + "fmt" "testing" "sigs.k8s.io/yaml" @@ -62,6 +63,14 @@ func TestSetIndex(t *testing.T) { val: 4, err: true, }, + { + name: "large", + initial: []interface{}{0, 1, 2, 3, 4, 5}, + expect: []interface{}{0, 1, 2, 3, 4, 5}, + add: MaxIndex + 1, + val: 4, + err: true, + }, } for _, tt := range tests { @@ -567,6 +576,107 @@ func TestParseIntoString(t *testing.T) { } } +func TestParseJSON(t *testing.T) { + tests := []struct { + input string + got map[string]interface{} + expect map[string]interface{} + err bool + }{ + { // set json scalars values, and replace one existing key + input: "outer.inner1=\"1\",outer.inner3=3,outer.inner4=true,outer.inner5=\"true\"", + got: map[string]interface{}{ + "outer": map[string]interface{}{ + "inner1": "overwrite", + "inner2": "value2", + }, + }, + expect: map[string]interface{}{ + "outer": map[string]interface{}{ + "inner1": "1", + "inner2": "value2", + "inner3": 3, + "inner4": true, + "inner5": "true", + }, + }, + err: false, + }, + { // set json objects and arrays, and replace one existing key + input: "outer.inner1={\"a\":\"1\",\"b\":2,\"c\":[1,2,3]},outer.inner3=[\"new value 1\",\"new value 2\"],outer.inner4={\"aa\":\"1\",\"bb\":2,\"cc\":[1,2,3]},outer.inner5=[{\"A\":\"1\",\"B\":2,\"C\":[1,2,3]}]", + got: map[string]interface{}{ + "outer": map[string]interface{}{ + "inner1": map[string]interface{}{ + "x": "overwrite", + }, + "inner2": "value2", + "inner3": []interface{}{ + "overwrite", + }, + }, + }, + expect: map[string]interface{}{ + "outer": map[string]interface{}{ + "inner1": map[string]interface{}{"a": "1", "b": 2, "c": []interface{}{1, 2, 3}}, + "inner2": "value2", + "inner3": []interface{}{"new value 1", "new value 2"}, + "inner4": map[string]interface{}{"aa": "1", "bb": 2, "cc": []interface{}{1, 2, 3}}, + "inner5": []interface{}{map[string]interface{}{"A": "1", "B": 2, "C": []interface{}{1, 2, 3}}}, + }, + }, + err: false, + }, + { // null assigment, and no value assigned (equivalent to null) + input: "outer.inner1=,outer.inner3={\"aa\":\"1\",\"bb\":2,\"cc\":[1,2,3]},outer.inner3.cc[1]=null", + got: map[string]interface{}{ + "outer": map[string]interface{}{ + "inner1": map[string]interface{}{ + "x": "overwrite", + }, + "inner2": "value2", + }, + }, + expect: map[string]interface{}{ + "outer": map[string]interface{}{ + "inner1": nil, + "inner2": "value2", + "inner3": map[string]interface{}{"aa": "1", "bb": 2, "cc": []interface{}{1, nil, 3}}, + }, + }, + err: false, + }, + { // syntax error + input: "outer.inner1={\"a\":\"1\",\"b\":2,\"c\":[1,2,3]},outer.inner3=[\"new value 1\",\"new value 2\"],outer.inner4={\"aa\":\"1\",\"bb\":2,\"cc\":[1,2,3]},outer.inner5={\"A\":\"1\",\"B\":2,\"C\":[1,2,3]}]", + got: nil, + expect: nil, + err: true, + }, + } + for _, tt := range tests { + if err := ParseJSON(tt.input, tt.got); err != nil { + if tt.err { + continue + } + t.Fatalf("%s: %s", tt.input, err) + } + if tt.err { + t.Fatalf("%s: Expected error. Got nil", tt.input) + } + y1, err := yaml.Marshal(tt.expect) + if err != nil { + t.Fatalf("Error serializing expected value: %s", 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 TestParseFile(t *testing.T) { input := "name1=path1" expect := map[string]interface{}{ @@ -645,3 +755,64 @@ func TestToYAML(t *testing.T) { t.Errorf("Expected %q, got %q", expect, o) } } + +func TestParseSetNestedLevels(t *testing.T) { + var keyMultipleNestedLevels string + for i := 1; i <= MaxNestedNameLevel+2; i++ { + tmpStr := fmt.Sprintf("name%d", i) + if i <= MaxNestedNameLevel+1 { + tmpStr = tmpStr + "." + } + keyMultipleNestedLevels += tmpStr + } + tests := []struct { + str string + expect map[string]interface{} + err bool + errStr string + }{ + { + "outer.middle.inner=value", + map[string]interface{}{"outer": map[string]interface{}{"middle": map[string]interface{}{"inner": "value"}}}, + false, + "", + }, + { + str: keyMultipleNestedLevels + "=value", + err: true, + errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d", + MaxNestedNameLevel), + }, + } + + for _, tt := range tests { + got, err := Parse(tt.str) + if err != nil { + if tt.err { + if tt.errStr != "" { + if err.Error() != tt.errStr { + t.Errorf("Expected error: %s. Got error: %s", tt.errStr, err.Error()) + } + } + continue + } + t.Fatalf("%s: %s", tt.str, err) + } + if tt.err { + t.Errorf("%s: Expected error. Got nil", tt.str) + } + + y1, err := yaml.Marshal(tt.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", tt.str, y1, y2) + } + } +} diff --git a/pkg/uploader/chart_uploader.go b/pkg/uploader/chart_uploader.go new file mode 100644 index 000000000..d7e940406 --- /dev/null +++ b/pkg/uploader/chart_uploader.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 uploader + +import ( + "fmt" + "io" + "net/url" + + "github.com/pkg/errors" + + "helm.sh/helm/v3/pkg/pusher" + "helm.sh/helm/v3/pkg/registry" +) + +// ChartUploader handles uploading a chart. +type ChartUploader struct { + // Out is the location to write warning and info messages. + Out io.Writer + // Pusher collection for the operation + Pushers pusher.Providers + // Options provide parameters to be passed along to the Pusher being initialized. + Options []pusher.Option + // RegistryClient is a client for interacting with registries. + RegistryClient *registry.Client +} + +// UploadTo uploads a chart. Depending on the settings, it may also upload a provenance file. +func (c *ChartUploader) UploadTo(ref, remote string) error { + u, err := url.Parse(remote) + if err != nil { + return errors.Errorf("invalid chart URL format: %s", remote) + } + + if u.Scheme == "" { + return fmt.Errorf("scheme prefix missing from remote (e.g. \"%s://\")", registry.OCIScheme) + } + + p, err := c.Pushers.ByScheme(u.Scheme) + if err != nil { + return err + } + + return p.Push(ref, u.String(), c.Options...) +} diff --git a/pkg/uploader/doc.go b/pkg/uploader/doc.go new file mode 100644 index 000000000..112ddbf2c --- /dev/null +++ b/pkg/uploader/doc.go @@ -0,0 +1,21 @@ +/* +Copyright The 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 uploader provides a library for uploading charts. + +This package contains tools for uploading charts to registries. +*/ +package uploader diff --git a/scripts/coverage.sh b/scripts/coverage.sh index bdbfaa991..2d8258866 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -21,7 +21,7 @@ coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX) profile="${coverdir}/cover.out" pushd / -hash goveralls 2>/dev/null || go get github.com/mattn/goveralls +hash goveralls 2>/dev/null || go install github.com/mattn/goveralls@v0.0.11 popd generate_cover_data() { @@ -37,7 +37,7 @@ generate_cover_data() { } push_to_coveralls() { - goveralls -coverprofile="${profile}" -service=circle-ci + goveralls -coverprofile="${profile}" -service=github } generate_cover_data diff --git a/scripts/get b/scripts/get index 8ad10459a..594fd92c3 100755 --- a/scripts/get +++ b/scripts/get @@ -138,10 +138,10 @@ installFile() { HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/$PROJECT_NAME" 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" + runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR/$PROJECT_NAME" echo "$PROJECT_NAME installed into $HELM_INSTALL_DIR/$PROJECT_NAME" if [ -x "$TILLER_TMP_BIN" ]; then - runAsRoot cp "$TILLER_TMP_BIN" "$HELM_INSTALL_DIR" + runAsRoot cp "$TILLER_TMP_BIN" "$HELM_INSTALL_DIR/$TILLER_NAME" 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" @@ -207,6 +207,10 @@ while [[ $# -gt 0 ]]; do shift if [[ $# -ne 0 ]]; then export DESIRED_VERSION="${1}" + if [[ "$1" != "v"* ]]; then + echo "Expected version arg ('${DESIRED_VERSION}') to begin with 'v', fixing..." + export DESIRED_VERSION="v${1}" + fi else echo -e "Please provide the desired version. e.g. --version v2.4.0 or -v latest" exit 0 diff --git a/scripts/get-helm-3 b/scripts/get-helm-3 index cf76cd9d8..b5e53bafc 100755 --- a/scripts/get-helm-3 +++ b/scripts/get-helm-3 @@ -29,6 +29,7 @@ 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)" +HAS_GIT="$(type "git" &> /dev/null && echo true || echo false)" # initArch discovers the architecture for this system. initArch() { @@ -51,7 +52,7 @@ initOS() { case "$OS" in # Minimalist GNU for Windows - mingw*) OS='windows';; + mingw*|cygwin*) OS='windows';; esac } @@ -97,17 +98,27 @@ verifySupported() { exit 1 fi fi + + if [ "${HAS_GIT}" != "true" ]; then + echo "[WARNING] Could not find git. It is required for plugin installation." + 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" + local latest_release_url="https://api.github.com/repos/helm/helm/releases/latest" + local latest_release_response="" 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}') + latest_release_response=$( curl -L --silent --show-error --fail "$latest_release_url" 2>&1 || true ) 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}') + latest_release_response=$( wget "$latest_release_url" -O - 2>&1 || true ) + fi + TAG=$( echo "$latest_release_response" | grep '"tag_name"' | sed -E 's/.*"(v[0-9\.]+)".*/\1/g' ) + if [ "x$TAG" == "x" ]; then + printf "Could not retrieve the latest release tag information from %s: %s\n" "${latest_release_url}" "${latest_release_response}" + exit 1 fi else TAG=$DESIRED_VERSION @@ -294,6 +305,10 @@ while [[ $# -gt 0 ]]; do shift if [[ $# -ne 0 ]]; then export DESIRED_VERSION="${1}" + if [[ "$1" != "v"* ]]; then + echo "Expected version arg ('${DESIRED_VERSION}') to begin with 'v', fixing..." + export DESIRED_VERSION="v${1}" + fi else echo -e "Please provide the desired version. e.g. --version v3.0.0 or -v canary" exit 0 diff --git a/testdata/crt.pem b/testdata/crt.pem index 715cd0f65..c4c471322 100644 --- a/testdata/crt.pem +++ b/testdata/crt.pem @@ -2,12 +2,12 @@ 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 + 48:5a:94:94:51:de:97:11:3b:62:54:dd:ac:85:63:e6:40:5c:4c:f6 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 + Not Before: Aug 24 18:07:59 2022 GMT + Not After : Aug 21 18:07:59 2032 GMT Subject: C=US, ST=CO, L=Boulder, O=Helm, CN=helm.sh Subject Public Key Info: Public Key Algorithm: rsaEncryption @@ -36,26 +36,26 @@ Certificate: 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 + d9:95:3b:98:01:6c:cb:a2:92:d8:f7:a7:52:2c:00:c1:04:cd: + ef:1b:d8:fa:71:71:29:7d:1d:29:42:ea:03:ce:15:c6:d5:ee: + 2d:25:51:7e:96:8b:44:2e:d9:19:1b:95:a6:9c:92:52:2b:88: + d8:76:6e:1b:87:36:8e:3a:b1:c6:aa:a4:7a:4e:a9:8b:8d:c0: + 3c:77:95:81:db:9a:50:f4:fb:cc:62:21:36:36:91:3b:6c:6e: + 37:a8:fa:cc:21:56:f4:31:6f:07:2b:29:0e:1a:06:6c:10:87: + fa:6c:be:e1:29:8c:b9:84:b2:ea:4d:07:e8:2b:ff:f6:24:e6: + a6:95:72:c7:d8:02:53:c2:c0:68:d3:fc:e9:72:a5:da:6c:39: + 5a:6b:17:71:86:40:96:ac:94:dd:21:45:9e:aa:85:8a:73:4c: + 8c:3f:0d:2b:d0:8b:04:ef:61:bb:8e:06:6b:86:46:30:a3:64: + 6b:97:01:8b:46:56:7d:42:33:f5:e0:ea:fd:80:b4:8a:50:a8: + 20:2c:f9:ad:61:05:da:ff:b9:b5:da:9c:d6:0e:47:44:0c:9a: + 8f:11:e0:66:f8:76:0c:0f:43:99:6b:af:44:3c:5c:cb:30:98: + 6a:24:f7:ea:23:db:cf:23:35:dd:6c:2e:9d:0a:b0:82:77:b8: + dc:90:5f:78 -----BEGIN CERTIFICATE----- -MIIDRDCCAiygAwIBAgIUVTFTm0FyBdyQSb1IE3xZnlpTXoYwDQYJKoZIhvcNAQEL +MIIDRDCCAiygAwIBAgIUSFqUlFHelxE7YlTdrIVj5kBcTPYwDQYJKoZIhvcNAQEL BQAwTTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMRAwDgYDVQQHDAdCb3VsZGVy -MQ0wCwYDVQQKDARIZWxtMRAwDgYDVQQDDAdoZWxtLnNoMB4XDTE5MTEwMTIyNTE0 -OVoXDTI5MTAyOTIyNTE0OVowTTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMRAw +MQ0wCwYDVQQKDARIZWxtMRAwDgYDVQQDDAdoZWxtLnNoMB4XDTIyMDgyNDE4MDc1 +OVoXDTMyMDgyMTE4MDc1OVowTTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMRAw DgYDVQQHDAdCb3VsZGVyMQ0wCwYDVQQKDARIZWxtMRAwDgYDVQQDDAdoZWxtLnNo MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyIlVDQvx2ubAcH3TJ824 qIGLfKSJ5dGxeAEd30SIC/zWgTU90Ttej7uTs34o2+3/oBM6cKP+lGsL/vtjALDL @@ -64,10 +64,10 @@ 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++ +CwUAA4IBAQDZlTuYAWzLopLY96dSLADBBM3vG9j6cXEpfR0pQuoDzhXG1e4tJVF+ +lotELtkZG5WmnJJSK4jYdm4bhzaOOrHGqqR6TqmLjcA8d5WB25pQ9PvMYiE2NpE7 +bG43qPrMIVb0MW8HKykOGgZsEIf6bL7hKYy5hLLqTQfoK//2JOamlXLH2AJTwsBo +0/zpcqXabDlaaxdxhkCWrJTdIUWeqoWKc0yMPw0r0IsE72G7jgZrhkYwo2RrlwGL +RlZ9QjP14Or9gLSKUKggLPmtYQXa/7m12pzWDkdEDJqPEeBm+HYMD0OZa69EPFzL +MJhqJPfqI9vPIzXdbC6dCrCCd7jckF94 -----END CERTIFICATE----- diff --git a/testdata/rootca.crt b/testdata/rootca.crt index 892104365..874cdbc1d 100644 --- a/testdata/rootca.crt +++ b/testdata/rootca.crt @@ -1,19 +1,21 @@ -----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== +MIIDezCCAmOgAwIBAgIUQTwAoToO0ZxUZZCSWuJI4/ROB+4wDQYJKoZIhvcNAQEL +BQAwTTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMRAwDgYDVQQHDAdCb3VsZGVy +MQ0wCwYDVQQKDARIZWxtMRAwDgYDVQQDDAdoZWxtLnNoMB4XDTIyMDgyNDE4MDYx +MVoXDTI4MDQwMjE4MDYxMVowTTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMRAw +DgYDVQQHDAdCb3VsZGVyMQ0wCwYDVQQKDARIZWxtMRAwDgYDVQQDDAdoZWxtLnNo +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4Z4zHBdV+ID8PdPYRpZp +I8QXhDiMV/kgUSWTqfWMxW9n9X7Tg2jTnypKqX3aIxiHBi3+/VryWRTosZReZI6t +Xv1iuIDbyJuoWskZlZowwsRNA6n7IBFVmUZvRWJk3ThOgXRcOetojH9HG3LnRjtf +HPqmBxq3ZAwDjYw3YzbN3UO2CkXjIc8eEXo/UaUtPFWCuwJNSKAgYTS12Rr1/Ydx +9q9u5+fKZoS9WWdRhxu3sHRshs9ekkr1vIhaS06n7YCAO6TCngo+UDi+JG53kqEc +LV9R31sbc3618QLZTSa6NKMzdu/bnZ15ID0c2HNSUTHExa8XE85mEc87HgMKoZy2 +hQIDAQABo1MwUTAdBgNVHQ4EFgQUicAFxDIXaZuRdpc3D265zOceBDQwHwYDVR0j +BBgwFoAUicAFxDIXaZuRdpc3D265zOceBDQwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAQEAyIndA2vsHWhn+PqxAnaCai0xAJ6awye7CAWKsLmT3rC2 +zR+EI5dCJgPJ0zrltQyngWz1IgUGoC4klgj/37lY5cG8/HYBJ37IAPya+pVukQuL +qqe2RCWqi4XZUPFRHjbJbHoM3AMsFeYOWJy+bTCMKyyYqUO0S7OM77ID9k7gcJFj +TZ6fvWvRqWFQCLJpQh95kt5wOkAKyttPf5Qkh37fLHtyrwkpbJCj+Yv3kcdKBYpw +kYLbK6DqqbgIKJHRbpu5xGOhKZ0/jnHJRvGAE6g6OKOXJQ/ydIZauoXKQ7hpcV43 +UAIXGjdbKVoPyLNgMueviW8+64GKqllWONPbBai5jQ== -----END CERTIFICATE----- diff --git a/testdata/rootca.key b/testdata/rootca.key index e3c1ce51e..14a2a0c0c 100644 --- a/testdata/rootca.key +++ b/testdata/rootca.key @@ -1,27 +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== +MIIEogIBAAKCAQEA4Z4zHBdV+ID8PdPYRpZpI8QXhDiMV/kgUSWTqfWMxW9n9X7T +g2jTnypKqX3aIxiHBi3+/VryWRTosZReZI6tXv1iuIDbyJuoWskZlZowwsRNA6n7 +IBFVmUZvRWJk3ThOgXRcOetojH9HG3LnRjtfHPqmBxq3ZAwDjYw3YzbN3UO2CkXj +Ic8eEXo/UaUtPFWCuwJNSKAgYTS12Rr1/Ydx9q9u5+fKZoS9WWdRhxu3sHRshs9e +kkr1vIhaS06n7YCAO6TCngo+UDi+JG53kqEcLV9R31sbc3618QLZTSa6NKMzdu/b +nZ15ID0c2HNSUTHExa8XE85mEc87HgMKoZy2hQIDAQABAoIBACFgRNFQBnDHrAj9 +cM4obA9Vb+EoeGJ/QS+f7nNDFvsSGv/vLh0PgdbW68qdCosMktTwMvuJ27Yf6Lh0 +aW5YyP73XwZKUbkghcxAWZ+O+s2lOntjRvocdlxBVi6eeqtbLAnsi8QptgKqxXsj +CWGTYOOplKwSYLTVLiVfa8YqklO77HHKQCMpCU7KsDbNpvhpme345nrAkAGX4Sd+ +STNTM3jdmyzC4jFycMz2eaSbJZjFefn9OkiAL+RNlm4dFo/l9sJIAaIZ5gPV3Jzl ++uDRFO0eW5oE/mHmfS450yOMPwl/mf4GxRbq2JNTBFSroYaz+n/p3Ii+3U5oWmi3 +D9C/EkECgYEA9CiCM5Vc5yPyq4UWjxRD6vedv0Ihur7x7bo1zxTdMBc6feRnJFp2 +HTz33gTY+mhyjstVshj+58rmIR7Ns0bLBJ5v0GyorxhnqhgfsWn9fiKR0lb79DpS +0APrnMdsz0/5NbK45b7qui6p4aDfRxr+EsUlwTUfbEjISn9/YgBk+rECgYEA7I9+ +S1sXBkRuBEyga8X77m/ZyF0ucqyJGxpXfsvR3udgWB3uyV5mEs4pnpLm0SPowuRl +8RUGBF9IUfMwvqcQkGN9qy+f0fpSZmLm0nFOyKD2aE/7A3JlMhY0KsSj2odUotzU +rTXqtlS87zsQl7t028B3r1Cw+y10qLcw3Se0BhUCgYAP5oN0MIn4U5L+MJCjiMJT +jwSq6/eeXckLnlDax5UQCLM6d6Fv8KQ4izvpLY+j3yF2wy81hgMzvTb3eTYUMswN +5POLM0hY/tHhdei6eRiVGlM8y4VlBldWTKsPbr1bUu373UPFUoWe0mMl2oAv9UYO +muA2kOsW9jZ1A5CcJUJuQQKBgDEnuASMjwI8Yef+zC7Y2vq2vzhFNIubknnRRXER +hTCeP4TP43hwZyFtOXS77b5zicBFmXE4/yEVc3+j2vMi3+xA4DIcGUeWjly8HF6K +MOa7m7gdNnmG4cRAnOJuLeYQzONyo7bCR11PylqjmVUOHMA1BCmnyL7IuT79oeey +glPpAoGAICOwp+bh1nqPt+nINO1q/zCCdl9hVakGVkQkuCiDK8wLW3R/vNrBtTf+ +PDM87BasvZkzA2VBcTgtDCcnP/aNDLyy2FDKIUyVtcpfheHgxjlT1txGHBUXJf6z +rS1fGWIYbpMb3RSCtGJTa1hyDJdN424nYUD3phL4SPx2Cn5eAPs= -----END RSA PRIVATE KEY-----